mirror of
https://github.com/ytdl-org/youtube-dl
synced 2025-10-25 01:28:36 +09:00
Compare commits
1066 Commits
2020.03.01
...
df-fmt-ext
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72c431725a | ||
|
|
080c5d48ed | ||
|
|
ba1399d54d | ||
|
|
195f22f679 | ||
|
|
fc2beab0e7 | ||
|
|
1a4fbe8462 | ||
|
|
c2f9be3e63 | ||
|
|
604762a9f8 | ||
|
|
47e70fff8b | ||
|
|
de39d1281c | ||
|
|
27ed77aabb | ||
|
|
c4b19a8816 | ||
|
|
087ddc2371 | ||
|
|
65ccb0dd4e | ||
|
|
a874871801 | ||
|
|
b7c25959f0 | ||
|
|
f102e3dc4e | ||
|
|
a19855f0f5 | ||
|
|
ce5d36486e | ||
|
|
d25cf62086 | ||
|
|
502cefa41f | ||
|
|
0faa45d6c0 | ||
|
|
447edc48e6 | ||
|
|
ee8560d01e | ||
|
|
7135277fec | ||
|
|
7bbd5b13d4 | ||
|
|
c91cbf6072 | ||
|
|
11b284c81f | ||
|
|
c94a459a24 | ||
|
|
6e2626f092 | ||
|
|
c282e5f8d7 | ||
|
|
2ced5a7912 | ||
|
|
82e4eca711 | ||
|
|
1b1442887e | ||
|
|
22127b271c | ||
|
|
d35557a75d | ||
|
|
9493ffdb8b | ||
|
|
7009bb9f31 | ||
|
|
218c423bc0 | ||
|
|
55c823634d | ||
|
|
4050e10a4c | ||
|
|
ed5c44e7b7 | ||
|
|
0f6422590e | ||
|
|
4c6fba3765 | ||
|
|
d619dd712f | ||
|
|
573b13410e | ||
|
|
66e58dccc2 | ||
|
|
556862bc91 | ||
|
|
a8d5316aaf | ||
|
|
fd3f3bebd0 | ||
|
|
46b8ae2f52 | ||
|
|
538ec65ba7 | ||
|
|
b0a60ce203 | ||
|
|
e52e8b8111 | ||
|
|
d231b56717 | ||
|
|
e6a836d54c | ||
|
|
deee741fb1 | ||
|
|
adb5294177 | ||
|
|
5f5c127ece | ||
|
|
090acd58c1 | ||
|
|
a03b9775d5 | ||
|
|
8a158a936c | ||
|
|
11665dd236 | ||
|
|
cc179df346 | ||
|
|
0700fde640 | ||
|
|
811c480f7b | ||
|
|
3aa94d7945 | ||
|
|
ef044be34b | ||
|
|
530f4582d0 | ||
|
|
1baa0f5f66 | ||
|
|
9aa8e5340f | ||
|
|
04fd3289d3 | ||
|
|
52c3751df7 | ||
|
|
187a48aee2 | ||
|
|
be35e5343a | ||
|
|
c3deca86ae | ||
|
|
c7965b9fc2 | ||
|
|
e988fa4523 | ||
|
|
e27d8d819f | ||
|
|
ebc627847c | ||
|
|
a0068bd6be | ||
|
|
b764dbe773 | ||
|
|
871645a4a4 | ||
|
|
1f50a07771 | ||
|
|
9e5ca66f16 | ||
|
|
17d295a1ec | ||
|
|
49c5293014 | ||
|
|
6508688e88 | ||
|
|
4194d253c0 | ||
|
|
f8e543c906 | ||
|
|
c4d1738316 | ||
|
|
1f13ccfd7f | ||
|
|
923292ba64 | ||
|
|
782bfd26db | ||
|
|
3472227074 | ||
|
|
bf23bc0489 | ||
|
|
85bf26c1d0 | ||
|
|
d8adca1b66 | ||
|
|
d02064218b | ||
|
|
b1297308fb | ||
|
|
8088ce036a | ||
|
|
29f7bfc4d7 | ||
|
|
74f8cc48af | ||
|
|
8ff961d10f | ||
|
|
266b6ef185 | ||
|
|
825d3426c5 | ||
|
|
47b0c8697a | ||
|
|
734dfbb4e3 | ||
|
|
ddc080a562 | ||
|
|
16a3fe2ba6 | ||
|
|
c820a284a2 | ||
|
|
58babe9af7 | ||
|
|
6d4932f023 | ||
|
|
92d73ef393 | ||
|
|
91278f4b6b | ||
|
|
73e1ab6125 | ||
|
|
584715a803 | ||
|
|
e00b0eab1e | ||
|
|
005339d637 | ||
|
|
23ad6402a6 | ||
|
|
9642344965 | ||
|
|
568c7005d5 | ||
|
|
5cb4833f40 | ||
|
|
5197336de6 | ||
|
|
01824d275b | ||
|
|
39a98b09a2 | ||
|
|
f0a05a55c2 | ||
|
|
4186e81777 | ||
|
|
b494824286 | ||
|
|
8248133e5e | ||
|
|
27dbf6f0ab | ||
|
|
61d791726f | ||
|
|
0c0876f790 | ||
|
|
7a497f1405 | ||
|
|
5add3f4373 | ||
|
|
78ce962f4f | ||
|
|
41f0043983 | ||
|
|
34c06b16f5 | ||
|
|
1e677567cd | ||
|
|
af9e72507e | ||
|
|
6ca7b77696 | ||
|
|
9d142109f4 | ||
|
|
1ca673bd98 | ||
|
|
e1eae16b56 | ||
|
|
96f87aaa3b | ||
|
|
5f5de51a49 | ||
|
|
39ca35e765 | ||
|
|
d76d59d99d | ||
|
|
2c2c2bd348 | ||
|
|
46e0a729b2 | ||
|
|
57044eaceb | ||
|
|
a3373da70c | ||
|
|
2c4cb134a9 | ||
|
|
bfe72723d8 | ||
|
|
ed99d68bdd | ||
|
|
5014bd67c2 | ||
|
|
e418823350 | ||
|
|
b5242da7d2 | ||
|
|
a803582717 | ||
|
|
7fb9564420 | ||
|
|
379f52a495 | ||
|
|
cb668eb973 | ||
|
|
751c9ae39a | ||
|
|
da32828208 | ||
|
|
2ccee8db74 | ||
|
|
47f2f2fbe9 | ||
|
|
03ab02730f | ||
|
|
4c77a2e538 | ||
|
|
4131703001 | ||
|
|
cc21aebe90 | ||
|
|
57b9a4b4c6 | ||
|
|
3a7ef27cf3 | ||
|
|
a7f61feab2 | ||
|
|
8fe5d54eb7 | ||
|
|
d156bc8d59 | ||
|
|
c2350cac24 | ||
|
|
b224cf39d5 | ||
|
|
5f85eb820c | ||
|
|
bb7ac1ed66 | ||
|
|
fdf91c52a8 | ||
|
|
943070af4a | ||
|
|
82f3993ba3 | ||
|
|
d495292852 | ||
|
|
2ee6c7f110 | ||
|
|
6511b8e8d7 | ||
|
|
f3cd1d9cec | ||
|
|
e13a01061d | ||
|
|
24297a42ef | ||
|
|
1980ff4550 | ||
|
|
dfbbe2902f | ||
|
|
e1a9d0ef78 | ||
|
|
f47627a1c9 | ||
|
|
efeb9e0fbf | ||
|
|
e90a890f01 | ||
|
|
199c645bee | ||
|
|
503a3744ad | ||
|
|
ef03721f47 | ||
|
|
1e8aaa1d15 | ||
|
|
6423d7054e | ||
|
|
eb5080286a | ||
|
|
286e01ce30 | ||
|
|
8536dcafd8 | ||
|
|
552b139911 | ||
|
|
2202cef0e4 | ||
|
|
a726009987 | ||
|
|
03afef7538 | ||
|
|
b797c1cc75 | ||
|
|
04be55307a | ||
|
|
504e4d804d | ||
|
|
1786cd3fe4 | ||
|
|
b8645c1f58 | ||
|
|
fe05191b8c | ||
|
|
0204838163 | ||
|
|
a0df8a0617 | ||
|
|
d1b9a5e2ef | ||
|
|
ff04d43c46 | ||
|
|
d2f72c40db | ||
|
|
e33dfb445c | ||
|
|
94520568b3 | ||
|
|
273964d190 | ||
|
|
346dd3b5e8 | ||
|
|
f5c2c06231 | ||
|
|
57eaaff5cf | ||
|
|
999329cf6b | ||
|
|
c6ab792990 | ||
|
|
0db79d8181 | ||
|
|
7e8b3f9439 | ||
|
|
ac19c3ac80 | ||
|
|
c4a451bcdd | ||
|
|
5ad69d3d0e | ||
|
|
32290307a4 | ||
|
|
dab83a2597 | ||
|
|
41920fc80e | ||
|
|
9f6c03a006 | ||
|
|
596b26606c | ||
|
|
f20b505b46 | ||
|
|
cfee2dfe83 | ||
|
|
30a3a4c70f | ||
|
|
a00a7e0cad | ||
|
|
54558e0baa | ||
|
|
7c52395479 | ||
|
|
ea87ed8394 | ||
|
|
d01e261a15 | ||
|
|
79e4ccfc4b | ||
|
|
06159135ef | ||
|
|
4fb25ff5a3 | ||
|
|
1b0a13f33c | ||
|
|
27e5a4464d | ||
|
|
545d6cb9d0 | ||
|
|
006eea564d | ||
|
|
281b8e3443 | ||
|
|
c0c5134c57 | ||
|
|
72a2c0a9ed | ||
|
|
445db582a2 | ||
|
|
6b116f0c03 | ||
|
|
70d0d4f9be | ||
|
|
6b315d96bc | ||
|
|
25b1287323 | ||
|
|
760c911299 | ||
|
|
162bf9e10a | ||
|
|
6beb1ac65b | ||
|
|
3ae9c0f410 | ||
|
|
e165f5641f | ||
|
|
aee6feb02a | ||
|
|
654b4f4ff2 | ||
|
|
1df2596f81 | ||
|
|
04d4a3b136 | ||
|
|
392c467f95 | ||
|
|
c5aa8f36bf | ||
|
|
3748863070 | ||
|
|
ca304beb15 | ||
|
|
e789bb1aa4 | ||
|
|
14f29f087e | ||
|
|
b97fb2edac | ||
|
|
28bab774a0 | ||
|
|
8f493de9fb | ||
|
|
207bc35d34 | ||
|
|
955894e72f | ||
|
|
287e50b56b | ||
|
|
da762c4e32 | ||
|
|
87a8bde777 | ||
|
|
49fc0a567f | ||
|
|
cc777dcaa0 | ||
|
|
c785911870 | ||
|
|
605e7b5e47 | ||
|
|
8562218350 | ||
|
|
76da1c954a | ||
|
|
c2fbfb49da | ||
|
|
d1069d33b4 | ||
|
|
eafcadea26 | ||
|
|
a40002444e | ||
|
|
5208ae92fc | ||
|
|
8117d613ac | ||
|
|
00b4d72d1e | ||
|
|
21ccd0d7f4 | ||
|
|
7e79ba7dd6 | ||
|
|
fa6bf0a711 | ||
|
|
f912d6c8cf | ||
|
|
357bfe251d | ||
|
|
3be098010f | ||
|
|
9955bb4a27 | ||
|
|
ebfd66c4b1 | ||
|
|
b509d24b2f | ||
|
|
1860d0f41c | ||
|
|
60845121ca | ||
|
|
1182f9567b | ||
|
|
ef414343e5 | ||
|
|
43d986acd8 | ||
|
|
9c644a6419 | ||
|
|
fc2c6d5323 | ||
|
|
64ed3af328 | ||
|
|
bae7dbf78b | ||
|
|
15c24b0346 | ||
|
|
477bff6906 | ||
|
|
1a1ccd9a6e | ||
|
|
7dc513487f | ||
|
|
c6a14755bb | ||
|
|
7f064d50db | ||
|
|
b8b622fbeb | ||
|
|
ec64ec9651 | ||
|
|
f68692b004 | ||
|
|
8c9766f4bf | ||
|
|
061c030133 | ||
|
|
8f56907afa | ||
|
|
e1adb3ed4f | ||
|
|
e465b25c1f | ||
|
|
7c06216abf | ||
|
|
0002888627 | ||
|
|
3fb14cd214 | ||
|
|
bee6182680 | ||
|
|
38fe5e239a | ||
|
|
678d46f6bb | ||
|
|
3c58f9e0b9 | ||
|
|
ef28e33249 | ||
|
|
9662e4964b | ||
|
|
44603290e5 | ||
|
|
1631fca1ee | ||
|
|
295860ff00 | ||
|
|
8cb4b71909 | ||
|
|
d81421af4b | ||
|
|
7422a2194f | ||
|
|
2090dbdc8c | ||
|
|
0a04e03a02 | ||
|
|
44b2d5f5fc | ||
|
|
aa9118a373 | ||
|
|
36abc16c3c | ||
|
|
919d764600 | ||
|
|
696183e133 | ||
|
|
f90d825a6b | ||
|
|
3037ab00c7 | ||
|
|
21e872b19a | ||
|
|
cf2dbec630 | ||
|
|
b92bb0e02a | ||
|
|
40edffae3d | ||
|
|
9fc5eafb8e | ||
|
|
08c2fbb844 | ||
|
|
3997efb65e | ||
|
|
a7356dffe9 | ||
|
|
e20ec43094 | ||
|
|
70baa7bfae | ||
|
|
8980f53b42 | ||
|
|
a363fb5d28 | ||
|
|
646052e416 | ||
|
|
844e4cbc54 | ||
|
|
56c63c8c02 | ||
|
|
07eb8f1916 | ||
|
|
4b5410c5c8 | ||
|
|
be2e9b76ee | ||
|
|
d8085580f6 | ||
|
|
6d32c6c6d3 | ||
|
|
f94d764993 | ||
|
|
f28f1b4d6e | ||
|
|
360d5f0daa | ||
|
|
cd493c5adc | ||
|
|
a4c7ed6b1e | ||
|
|
7f8b8bc418 | ||
|
|
311ebdd9a5 | ||
|
|
99c68db0a8 | ||
|
|
5fc53690cb | ||
|
|
7a9161578e | ||
|
|
2405854705 | ||
|
|
0cf09c2b41 | ||
|
|
0156ce95c5 | ||
|
|
1641b13232 | ||
|
|
a4bdc3112b | ||
|
|
c7d407bca2 | ||
|
|
7215691ab7 | ||
|
|
fc88e8f0e3 | ||
|
|
cfefb7d854 | ||
|
|
3c07d007ca | ||
|
|
89c5a7d5aa | ||
|
|
2adc0c51cd | ||
|
|
1f0910bc27 | ||
|
|
e22ff4e356 | ||
|
|
83031d749b | ||
|
|
1b731ebcaa | ||
|
|
ab25f3f431 | ||
|
|
07f7aad81c | ||
|
|
1e2575df87 | ||
|
|
b111a64135 | ||
|
|
0e3a968479 | ||
|
|
c11f7cf9bd | ||
|
|
8fa7cc387d | ||
|
|
65eee5a745 | ||
|
|
efef4ddf51 | ||
|
|
159a3d48df | ||
|
|
b46483a6ec | ||
|
|
9c724601ba | ||
|
|
67299f23d8 | ||
|
|
8bf9591a70 | ||
|
|
a800838f5a | ||
|
|
ba15b2fee6 | ||
|
|
56a7ee9033 | ||
|
|
0b4f03a563 | ||
|
|
7b8fa658f8 | ||
|
|
fd95fc33b1 | ||
|
|
c669554ef5 | ||
|
|
11b68df7a4 | ||
|
|
d18f4419a7 | ||
|
|
0f7d413d5b | ||
|
|
286e5d6724 | ||
|
|
395981288b | ||
|
|
55bb3556c8 | ||
|
|
57f2488bbe | ||
|
|
ea399a53eb | ||
|
|
811a183eb6 | ||
|
|
b63981e850 | ||
|
|
186cbaffb9 | ||
|
|
dbf3fa8af6 | ||
|
|
f08c31cf33 | ||
|
|
d8dab85419 | ||
|
|
5519bba3e1 | ||
|
|
142c584063 | ||
|
|
4542e3e555 | ||
|
|
fa8f6d8580 | ||
|
|
3bb7769c40 | ||
|
|
8d286bd5b6 | ||
|
|
cff72b4cc0 | ||
|
|
657221c81d | ||
|
|
62acf5fa2c | ||
|
|
b79977fb6b | ||
|
|
bc7c8f3d4e | ||
|
|
015e19b350 | ||
|
|
54856480d7 | ||
|
|
1dd12708c2 | ||
|
|
f9201cef58 | ||
|
|
26499ba823 | ||
|
|
58f6c2112d | ||
|
|
de026a6acd | ||
|
|
d4564afc70 | ||
|
|
360a5e0f60 | ||
|
|
55a3ca16d3 | ||
|
|
ef50cb3fda | ||
|
|
8673f4344c | ||
|
|
f1487d4fca | ||
|
|
0cd4c402f0 | ||
|
|
9c9b458145 | ||
|
|
9d50f86232 | ||
|
|
7e92f9015e | ||
|
|
aa860b8016 | ||
|
|
b484097b01 | ||
|
|
ab9001dab5 | ||
|
|
879866a230 | ||
|
|
8e5477d036 | ||
|
|
1e8e5d5238 | ||
|
|
d81a213cfb | ||
|
|
7c2d18a13f | ||
|
|
2408e6d26a | ||
|
|
cf862771d7 | ||
|
|
a938f111ed | ||
|
|
4759543f6e | ||
|
|
d0fc289f45 | ||
|
|
70f572585d | ||
|
|
c2d06aef60 | ||
|
|
ff1e765400 | ||
|
|
170e1c1995 | ||
|
|
61e669acff | ||
|
|
2c337f4e85 | ||
|
|
bf6a74c620 | ||
|
|
38a967c98e | ||
|
|
3a61e6d360 | ||
|
|
3d8e32dcc0 | ||
|
|
8f29b2dd38 | ||
|
|
a29e340efa | ||
|
|
b13f29098f | ||
|
|
430c4bc9d0 | ||
|
|
4ae243fc6c | ||
|
|
8f20ad36dc | ||
|
|
799c794947 | ||
|
|
1ae7ae0b96 | ||
|
|
ccc7112291 | ||
|
|
5b24f8f505 | ||
|
|
fcd90d2583 | ||
|
|
8f757c7353 | ||
|
|
be1a3f2d11 | ||
|
|
ecae54a98d | ||
|
|
f318882955 | ||
|
|
c3399cac19 | ||
|
|
9237aaa77f | ||
|
|
766fcdd0fa | ||
|
|
f6ea29e24b | ||
|
|
8a3797a4ab | ||
|
|
745db8899d | ||
|
|
83db801cbf | ||
|
|
964a8eb754 | ||
|
|
ac61f2e058 | ||
|
|
8487e8b98a | ||
|
|
9c484c0019 | ||
|
|
0e96b4b5ce | ||
|
|
a563c97c5c | ||
|
|
e88c9ef62a | ||
|
|
0889eb33e0 | ||
|
|
0021a2b9a1 | ||
|
|
19ec468635 | ||
|
|
491ee7efe4 | ||
|
|
8522bcd97c | ||
|
|
ac71fd5919 | ||
|
|
8e953dcbb1 | ||
|
|
f4afb9a6a8 | ||
|
|
d5b8cf093c | ||
|
|
5c6e84c0ff | ||
|
|
1aaee908b9 | ||
|
|
b2d9fd9c9f | ||
|
|
bc2f83b95e | ||
|
|
85de33b04e | ||
|
|
7dfd966848 | ||
|
|
a25d03d7cb | ||
|
|
cabfd4b1f0 | ||
|
|
7b643d4cd0 | ||
|
|
1f1d01d498 | ||
|
|
21a42e2588 | ||
|
|
2df93a0c4a | ||
|
|
75972e200d | ||
|
|
d0d838638c | ||
|
|
8c17afc471 | ||
|
|
40d66e07df | ||
|
|
ab89a8678b | ||
|
|
4d7d056909 | ||
|
|
c35bc82606 | ||
|
|
2f56caf083 | ||
|
|
4066945919 | ||
|
|
2a84694b1e | ||
|
|
4046ffe1e1 | ||
|
|
d1d0612160 | ||
|
|
7b0f04ed1f | ||
|
|
2e21b06ea2 | ||
|
|
a6f75e6e89 | ||
|
|
bd18824c2a | ||
|
|
bdd044e67b | ||
|
|
f7e95fb2a0 | ||
|
|
9dd674e1d2 | ||
|
|
9c1e164e0c | ||
|
|
c706fbe9fe | ||
|
|
ebdcf70b0d | ||
|
|
5966095e65 | ||
|
|
9ee984fc76 | ||
|
|
53528e1d23 | ||
|
|
c931c4b8dd | ||
|
|
7acd042bbb | ||
|
|
bcfe485e01 | ||
|
|
479cc6d5a1 | ||
|
|
38286ee729 | ||
|
|
1a95953867 | ||
|
|
71febd1c52 | ||
|
|
f1bc56c99b | ||
|
|
64e419bd73 | ||
|
|
782ea947b4 | ||
|
|
f27224d57b | ||
|
|
c007188598 | ||
|
|
af93ecfd88 | ||
|
|
794771a164 | ||
|
|
6f2eaaf73d | ||
|
|
4c7a4dbc4d | ||
|
|
f86b299d0e | ||
|
|
e474996541 | ||
|
|
aed617e311 | ||
|
|
0fa67c1d68 | ||
|
|
365b3cc72d | ||
|
|
a272fe21a8 | ||
|
|
cec1c2f211 | ||
|
|
12053450dc | ||
|
|
46cffb0c47 | ||
|
|
c32a059f52 | ||
|
|
6911312e53 | ||
|
|
f22b5a6b96 | ||
|
|
58e55198c1 | ||
|
|
d61ed9f2f1 | ||
|
|
8bc4c6350e | ||
|
|
cfa4ffa23b | ||
|
|
4f1dc1463d | ||
|
|
17e0f41d34 | ||
|
|
b57b27ff8f | ||
|
|
bbe8cc6662 | ||
|
|
98106accb6 | ||
|
|
af1312bfc3 | ||
|
|
4c7d7215cd | ||
|
|
0370d9eb3d | ||
|
|
1434651d20 | ||
|
|
2c312ab84a | ||
|
|
0ee78d62d5 | ||
|
|
7f3c90ab25 | ||
|
|
1d3cd29730 | ||
|
|
4ef1fc9707 | ||
|
|
f9e6aa1dcf | ||
|
|
f83db9064b | ||
|
|
2da9a86399 | ||
|
|
ecaa535cf4 | ||
|
|
79dd92b1fe | ||
|
|
bd3844c9c2 | ||
|
|
7bf5e3a84a | ||
|
|
08a17dae5b | ||
|
|
924ea66ade | ||
|
|
5b72f5b74f | ||
|
|
bfa345744d | ||
|
|
f966461476 | ||
|
|
b8aea53682 | ||
|
|
c0d9eb7043 | ||
|
|
3ba6aabd25 | ||
|
|
a8b31505ed | ||
|
|
90a271e914 | ||
|
|
172d270607 | ||
|
|
22feed08a1 | ||
|
|
942b8ca3be | ||
|
|
3729c52f9d | ||
|
|
71679eaee8 | ||
|
|
76fe4ba3b2 | ||
|
|
164a4a5756 | ||
|
|
455951985b | ||
|
|
c29500e412 | ||
|
|
1bc1520adc | ||
|
|
022e05dc1f | ||
|
|
b34c9551aa | ||
|
|
84f19c026f | ||
|
|
6bde5492b6 | ||
|
|
6086df4d6a | ||
|
|
c98052c5da | ||
|
|
ab62bc5838 | ||
|
|
bc87ba8424 | ||
|
|
b79df1b68d | ||
|
|
2797c7be45 | ||
|
|
755f186e21 | ||
|
|
2240a1dc4d | ||
|
|
03d3af9768 | ||
|
|
5ce9527e16 | ||
|
|
c527f5ada0 | ||
|
|
ace52668f0 | ||
|
|
9c33eb027e | ||
|
|
679b711395 | ||
|
|
1727541315 | ||
|
|
45b0a0d11b | ||
|
|
e665fcd4da | ||
|
|
aae737d4af | ||
|
|
92a6de861e | ||
|
|
5ff881aee6 | ||
|
|
eae19a4473 | ||
|
|
f70c263ce5 | ||
|
|
92d135921f | ||
|
|
d8008dee4f | ||
|
|
bb38a12157 | ||
|
|
bcc8ef0a5a | ||
|
|
3cb1a5dc73 | ||
|
|
ed40c02c9b | ||
|
|
b5fdceb4d1 | ||
|
|
1e6f7f3b45 | ||
|
|
469ff02f4e | ||
|
|
56f1c5ad38 | ||
|
|
3a6b8f4edf | ||
|
|
3ae02d4a62 | ||
|
|
59fea63dc2 | ||
|
|
22933e02d4 | ||
|
|
98a62d7cbd | ||
|
|
d6c5fdef34 | ||
|
|
4b9051cf39 | ||
|
|
00f5068908 | ||
|
|
432c6b0f48 | ||
|
|
ad5e5788ff | ||
|
|
dc65041c22 | ||
|
|
9f88b07945 | ||
|
|
225646c4ca | ||
|
|
5c6e9f0b6c | ||
|
|
6c370bc149 | ||
|
|
37fd242342 | ||
|
|
aee1f87168 | ||
|
|
b69bb1ed11 | ||
|
|
772cefef8c | ||
|
|
842654b6d0 | ||
|
|
df5e50954b | ||
|
|
a4a2fa8754 | ||
|
|
9da0504a09 | ||
|
|
470cf496f5 | ||
|
|
e029da9add | ||
|
|
e00b8f60d4 | ||
|
|
644c3ef886 | ||
|
|
9d8d0f8b4a | ||
|
|
5a1fbbf8b7 | ||
|
|
e2bdf8bf4f | ||
|
|
c368dc98e0 | ||
|
|
e7eff914cd | ||
|
|
07333d0062 | ||
|
|
5bd7ad2e81 | ||
|
|
3ded751985 | ||
|
|
6956db3606 | ||
|
|
17b01228f8 | ||
|
|
4f1ecca58d | ||
|
|
2717036489 | ||
|
|
d9482c0083 | ||
|
|
791b743765 | ||
|
|
fa604d9083 | ||
|
|
2bf0634d16 | ||
|
|
dccf4932e1 | ||
|
|
91dd25fe1e | ||
|
|
06bf2ac20f | ||
|
|
6ad0d8781e | ||
|
|
f2c704e112 | ||
|
|
5e822c2526 | ||
|
|
cc017e07ca | ||
|
|
082da36416 | ||
|
|
6bf95b15ee | ||
|
|
4c93b2fd15 | ||
|
|
1b26bfd425 | ||
|
|
13ec444a98 | ||
|
|
51579d87e4 | ||
|
|
e147619669 | ||
|
|
e7f93fbd85 | ||
|
|
58f7ada235 | ||
|
|
c67b33888f | ||
|
|
e8c0af04b7 | ||
|
|
5d769860c3 | ||
|
|
02b04785ee | ||
|
|
41c92b8d02 | ||
|
|
fe0c28f956 | ||
|
|
957c65b9ea | ||
|
|
5e95e18ce9 | ||
|
|
e91df0c550 | ||
|
|
c5636e9bca | ||
|
|
2e47264235 | ||
|
|
1c78cb118c | ||
|
|
beab2f88c9 | ||
|
|
0025447369 | ||
|
|
da4304609d | ||
|
|
ea89680aea | ||
|
|
664dd8ba85 | ||
|
|
64554c12e1 | ||
|
|
4ded9c0f00 | ||
|
|
c0820dd52a | ||
|
|
2bb70750a9 | ||
|
|
09d923f2eb | ||
|
|
37d979ad33 | ||
|
|
95ac4de229 | ||
|
|
d3e142b3fa | ||
|
|
132aece1ed | ||
|
|
3e4e338133 | ||
|
|
be19ae11fd | ||
|
|
59d63d8d4a | ||
|
|
cfeba5d17f | ||
|
|
6da0e5e7a2 | ||
|
|
d6ce649f15 | ||
|
|
b449b73dcc | ||
|
|
16c822e91e | ||
|
|
4318170779 | ||
|
|
fb626c0586 | ||
|
|
717d1d2d5a | ||
|
|
9585b376db | ||
|
|
f04cfe24e0 | ||
|
|
20c50c6556 | ||
|
|
f9f9699f2f | ||
|
|
a3cf22e590 | ||
|
|
99de2f38d3 | ||
|
|
9fe50837c3 | ||
|
|
4dc545553f | ||
|
|
686e898fde | ||
|
|
3a78198a96 | ||
|
|
836c810716 | ||
|
|
97c5be383c | ||
|
|
a7ea88537a | ||
|
|
6c35de4c6b | ||
|
|
579d43951d | ||
|
|
e24ebeafd1 | ||
|
|
01c92973dd | ||
|
|
f4415faa46 | ||
|
|
a86ce9d7a1 | ||
|
|
37258c644f | ||
|
|
d0512ac4c5 | ||
|
|
da4eaa15a2 | ||
|
|
191286265d | ||
|
|
3234272818 | ||
|
|
9d2c90354f | ||
|
|
316b10855a | ||
|
|
484fe78737 | ||
|
|
2cd43a00d1 | ||
|
|
dd0f524c69 | ||
|
|
c84f9475b8 | ||
|
|
15f2734791 | ||
|
|
cb6e24f946 | ||
|
|
9d531aa291 | ||
|
|
e9cbb98a0f | ||
|
|
193422e12a | ||
|
|
c4cabf040e | ||
|
|
f23eceebbf | ||
|
|
650bd8f623 | ||
|
|
5d8cb4367d | ||
|
|
82abc13aed | ||
|
|
1e72660c9b | ||
|
|
049f224248 | ||
|
|
0ada1b90b8 | ||
|
|
71ddc222ad | ||
|
|
21292c0649 | ||
|
|
46a265a2da | ||
|
|
e2096776b9 | ||
|
|
a1c88c4819 | ||
|
|
51ca93d751 | ||
|
|
a7e0531999 | ||
|
|
ab0eda99e1 | ||
|
|
ec99f47108 | ||
|
|
b31b5f4434 | ||
|
|
86f2fa1590 | ||
|
|
7bc7fbce23 | ||
|
|
a78e530c14 | ||
|
|
af7bb684c0 | ||
|
|
dd9e0f58f3 | ||
|
|
59e583f7e8 | ||
|
|
daa25d4142 | ||
|
|
25a35cb38a | ||
|
|
2cf8003638 | ||
|
|
cf1a8668e8 | ||
|
|
2dbb45ae82 | ||
|
|
4fe190df70 | ||
|
|
039e715b30 | ||
|
|
32152bab7a | ||
|
|
b1347a5881 | ||
|
|
91e954587f | ||
|
|
9b505185da | ||
|
|
444a68e0ec | ||
|
|
c7178f0f7a | ||
|
|
5c3f7014ef | ||
|
|
d65628ef03 | ||
|
|
8a6c5b0806 | ||
|
|
9360936f26 | ||
|
|
bb2b89e077 | ||
|
|
2d7a29081c | ||
|
|
2864179293 | ||
|
|
284f8306df | ||
|
|
aa613ef7e1 | ||
|
|
cb2b9a22a5 | ||
|
|
5b867c15a8 | ||
|
|
f8c749f12c | ||
|
|
7d509c613b | ||
|
|
b92e95aa01 | ||
|
|
e3cad6bd99 | ||
|
|
6699b6ce41 | ||
|
|
91dcde8a38 | ||
|
|
11f3471c4b | ||
|
|
f4093b34f6 | ||
|
|
a80b23c373 | ||
|
|
2e7fa18bb9 | ||
|
|
fe07e788bf | ||
|
|
6d3bdcf217 | ||
|
|
ec2a2ab441 | ||
|
|
9448a20312 | ||
|
|
3f1748b944 | ||
|
|
fe13087cd1 | ||
|
|
f22fa82d7f | ||
|
|
059fa9aa81 | ||
|
|
650aec4a98 | ||
|
|
2ea9c97432 | ||
|
|
efc589b865 | ||
|
|
1737ea69b9 | ||
|
|
1fb034d029 | ||
|
|
28f9568a84 | ||
|
|
5bbdadd5f8 | ||
|
|
2de2ca6659 | ||
|
|
051071203c | ||
|
|
34299510bb | ||
|
|
b9bceba37c | ||
|
|
6d4733ce7b | ||
|
|
416da574ec | ||
|
|
48c5663c5f | ||
|
|
7d740e7dc7 | ||
|
|
4eda10499e | ||
|
|
605535776a | ||
|
|
1050e0d09f | ||
|
|
d65d89183f | ||
|
|
0c92f1e96b | ||
|
|
adae9e844b | ||
|
|
c5764b3f89 | ||
|
|
0837992a22 | ||
|
|
b55715934b | ||
|
|
bbc3b5b4bb | ||
|
|
1ca5f821c8 | ||
|
|
defc820b70 | ||
|
|
82ef02e936 | ||
|
|
b856b3997c | ||
|
|
cd85a1bb8b | ||
|
|
ce5b904050 | ||
|
|
ad06b99dd4 | ||
|
|
540b9f5164 | ||
|
|
6e65a2a67e | ||
|
|
f8c7bed133 | ||
|
|
cdc55e666f | ||
|
|
86b7c00adc | ||
|
|
e8c5d40bc8 | ||
|
|
ca7ebc4e5e | ||
|
|
bff857a8af | ||
|
|
a31a022efd | ||
|
|
45f6362464 | ||
|
|
97f34a48d7 | ||
|
|
ea74e00b3a | ||
|
|
06cd4cdb25 | ||
|
|
da2069fb22 | ||
|
|
95c9810015 | ||
|
|
b03eebdb6a | ||
|
|
1f7675451c | ||
|
|
aa27253556 | ||
|
|
d51e23d9fc | ||
|
|
6cd452acff | ||
|
|
50e9fcc1fd | ||
|
|
16ee69c1b7 | ||
|
|
67171ed7e9 | ||
|
|
1d9bf655e6 | ||
|
|
62ae19ff76 | ||
|
|
5ed05f26ad | ||
|
|
841b683804 | ||
|
|
f5863a3ea0 | ||
|
|
10709fc7c6 | ||
|
|
a7e348556a | ||
|
|
6cb30ea5ed | ||
|
|
a4ed50bb84 | ||
|
|
570611955f | ||
|
|
e450f6cb63 | ||
|
|
a115e07594 | ||
|
|
718393c632 | ||
|
|
07af16b92e | ||
|
|
e942cfd1a7 | ||
|
|
9a7e5cb88a | ||
|
|
2391941f28 | ||
|
|
9ff6165a81 | ||
|
|
1c748722f9 | ||
|
|
ee0b726cd7 | ||
|
|
dbeafce5d5 | ||
|
|
ed604ce7bc | ||
|
|
7adc7ca547 | ||
|
|
a6211d237b | ||
|
|
7b16239a49 | ||
|
|
37357d21a9 | ||
|
|
b477fc1314 | ||
|
|
d84b21b427 | ||
|
|
48bd042ce7 | ||
|
|
84213ea8d4 | ||
|
|
562de77f41 | ||
|
|
e1723c4bac | ||
|
|
607d204551 | ||
|
|
a5b6102ea8 | ||
|
|
b77888228d | ||
|
|
0b1eaec3bc | ||
|
|
b37e47a3f9 | ||
|
|
ce3735df02 | ||
|
|
a0455d0ffd | ||
|
|
c8b232cc48 | ||
|
|
b4eb0bc7bd | ||
|
|
d5147b65ac | ||
|
|
7b0b53ea69 | ||
|
|
7016e24ebe | ||
|
|
bef4688c72 | ||
|
|
228c1d685b | ||
|
|
efd72b05d2 | ||
|
|
fe515e5c75 | ||
|
|
1db5ab6b34 | ||
|
|
2791e80b60 | ||
|
|
8f841fafcd | ||
|
|
a54c5f83c0 | ||
|
|
cd13343ad8 | ||
|
|
9cd5f54e31 | ||
|
|
9a269547f2 | ||
|
|
bf097a5077 | ||
|
|
52c50a10af | ||
|
|
b334732709 | ||
|
|
384bf91f88 | ||
|
|
fae11394f0 | ||
|
|
adc13b0748 | ||
|
|
327593257c | ||
|
|
9d8f3a12a6 | ||
|
|
b002bc433a | ||
|
|
b74896dad1 | ||
|
|
fa3db38333 | ||
|
|
30fa5c6087 | ||
|
|
6c907eb33f | ||
|
|
f7b42518dc | ||
|
|
ce7db64bf1 | ||
|
|
1328305851 | ||
|
|
6c22cee673 | ||
|
|
6d874fee2a | ||
|
|
676723e0da | ||
|
|
c380cc28c4 | ||
|
|
f7f304910d | ||
|
|
00a41ca4c3 | ||
|
|
66f32ca0e1 | ||
|
|
6ffc3cf74a | ||
|
|
4433bb0245 | ||
|
|
e40c758c2a | ||
|
|
011e75e641 | ||
|
|
2468a6fa64 | ||
|
|
700265bfcf | ||
|
|
c97f5e934f | ||
|
|
38db9a405a | ||
|
|
2cdfe977d7 | ||
|
|
46d0baf941 | ||
|
|
00eb865b3c | ||
|
|
2f19835726 | ||
|
|
533f3e3557 | ||
|
|
75294a5ed0 | ||
|
|
b9e5f87291 | ||
|
|
6b09401b0b | ||
|
|
5caf88ccb4 | ||
|
|
dcc8522fdb | ||
|
|
c9595ee780 | ||
|
|
91bd3bd019 | ||
|
|
13b08034b5 | ||
|
|
6a6e1a0cd8 | ||
|
|
4e7b5bba5f | ||
|
|
52c4c51556 | ||
|
|
8fae1a04eb | ||
|
|
d44a707fdd | ||
|
|
049c0486bb | ||
|
|
30b5121a1c | ||
|
|
b439634f0e | ||
|
|
6e47200b6e | ||
|
|
38fa761a45 | ||
|
|
08a27407c4 | ||
|
|
be7dacf9cf | ||
|
|
4560adc820 | ||
|
|
63dce3094b | ||
|
|
b4eb08bb03 | ||
|
|
2e20cb3636 | ||
|
|
a6c5859d6b | ||
|
|
c76cdf2382 | ||
|
|
787c360467 | ||
|
|
73453430c1 | ||
|
|
158bc5ac03 | ||
|
|
4568a11802 | ||
|
|
4cbce88f8b | ||
|
|
541fe3eaff | ||
|
|
9bfe088594 | ||
|
|
fcaf4d7a06 | ||
|
|
40b6495d40 | ||
|
|
f1a8511f7b | ||
|
|
042b664933 | ||
|
|
68fa15155f | ||
|
|
434f573046 | ||
|
|
cff99c91d1 | ||
|
|
fa9b8c6628 | ||
|
|
ea782aca52 | ||
|
|
43ebf77df3 | ||
|
|
d332ec725d | ||
|
|
f93abcf1da | ||
|
|
0ec9d4e565 | ||
|
|
34525a3885 | ||
|
|
2db9ac228d | ||
|
|
5429d6a9cb | ||
|
|
dc879c5a37 | ||
|
|
12ee431676 | ||
|
|
46cc54ca8f | ||
|
|
1e1c1960aa | ||
|
|
ac379fa236 | ||
|
|
0e30a7b973 | ||
|
|
3b5399ce0f | ||
|
|
1c45ff5572 |
6
.github/ISSUE_TEMPLATE/1_broken_site.md
vendored
6
.github/ISSUE_TEMPLATE/1_broken_site.md
vendored
@@ -18,7 +18,7 @@ title: ''
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of youtube-dl:
|
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of youtube-dl:
|
||||||
- First of, make sure you are using the latest version of youtube-dl. Run `youtube-dl --version` and ensure your version is 2020.03.01. If it's not, see https://yt-dl.org/update on how to update. Issues with outdated version will be REJECTED.
|
- First of, make sure you are using the latest version of youtube-dl. Run `youtube-dl --version` and ensure your version is 2021.12.17. If it's not, see https://yt-dl.org/update on how to update. Issues with outdated version will be REJECTED.
|
||||||
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
|
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
|
||||||
- Make sure that all URLs and arguments with special characters are properly quoted or escaped as explained in http://yt-dl.org/escape.
|
- Make sure that all URLs and arguments with special characters are properly quoted or escaped as explained in http://yt-dl.org/escape.
|
||||||
- Search the bugtracker for similar issues: http://yt-dl.org/search-issues. DO NOT post duplicates.
|
- Search the bugtracker for similar issues: http://yt-dl.org/search-issues. DO NOT post duplicates.
|
||||||
@@ -26,7 +26,7 @@ Carefully read and work through this check list in order to prevent the most com
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
- [ ] I'm reporting a broken site support
|
- [ ] I'm reporting a broken site support
|
||||||
- [ ] I've verified that I'm running youtube-dl version **2020.03.01**
|
- [ ] I've verified that I'm running youtube-dl version **2021.12.17**
|
||||||
- [ ] I've checked that all provided URLs are alive and playable in a browser
|
- [ ] I've checked that all provided URLs are alive and playable in a browser
|
||||||
- [ ] I've checked that all URLs and arguments with special characters are properly quoted or escaped
|
- [ ] I've checked that all URLs and arguments with special characters are properly quoted or escaped
|
||||||
- [ ] I've searched the bugtracker for similar issues including closed ones
|
- [ ] I've searched the bugtracker for similar issues including closed ones
|
||||||
@@ -41,7 +41,7 @@ Add the `-v` flag to your command line you run youtube-dl with (`youtube-dl -v <
|
|||||||
[debug] User config: []
|
[debug] User config: []
|
||||||
[debug] Command-line args: [u'-v', u'http://www.youtube.com/watch?v=BaW_jenozKcj']
|
[debug] Command-line args: [u'-v', u'http://www.youtube.com/watch?v=BaW_jenozKcj']
|
||||||
[debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251
|
[debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251
|
||||||
[debug] youtube-dl version 2020.03.01
|
[debug] youtube-dl version 2021.12.17
|
||||||
[debug] Python version 2.7.11 - Windows-2003Server-5.2.3790-SP2
|
[debug] Python version 2.7.11 - Windows-2003Server-5.2.3790-SP2
|
||||||
[debug] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4
|
[debug] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4
|
||||||
[debug] Proxy map: {}
|
[debug] Proxy map: {}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ labels: 'site-support-request'
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of youtube-dl:
|
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of youtube-dl:
|
||||||
- First of, make sure you are using the latest version of youtube-dl. Run `youtube-dl --version` and ensure your version is 2020.03.01. If it's not, see https://yt-dl.org/update on how to update. Issues with outdated version will be REJECTED.
|
- First of, make sure you are using the latest version of youtube-dl. Run `youtube-dl --version` and ensure your version is 2021.12.17. If it's not, see https://yt-dl.org/update on how to update. Issues with outdated version will be REJECTED.
|
||||||
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
|
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
|
||||||
- Make sure that site you are requesting is not dedicated to copyright infringement, see https://yt-dl.org/copyright-infringement. youtube-dl does not support such sites. In order for site support request to be accepted all provided example URLs should not violate any copyrights.
|
- Make sure that site you are requesting is not dedicated to copyright infringement, see https://yt-dl.org/copyright-infringement. youtube-dl does not support such sites. In order for site support request to be accepted all provided example URLs should not violate any copyrights.
|
||||||
- Search the bugtracker for similar site support requests: http://yt-dl.org/search-issues. DO NOT post duplicates.
|
- Search the bugtracker for similar site support requests: http://yt-dl.org/search-issues. DO NOT post duplicates.
|
||||||
@@ -27,7 +27,7 @@ Carefully read and work through this check list in order to prevent the most com
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
- [ ] I'm reporting a new site support request
|
- [ ] I'm reporting a new site support request
|
||||||
- [ ] I've verified that I'm running youtube-dl version **2020.03.01**
|
- [ ] I've verified that I'm running youtube-dl version **2021.12.17**
|
||||||
- [ ] I've checked that all provided URLs are alive and playable in a browser
|
- [ ] I've checked that all provided URLs are alive and playable in a browser
|
||||||
- [ ] I've checked that none of provided URLs violate any copyrights
|
- [ ] I've checked that none of provided URLs violate any copyrights
|
||||||
- [ ] I've searched the bugtracker for similar site support requests including closed ones
|
- [ ] I've searched the bugtracker for similar site support requests including closed ones
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ title: ''
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of youtube-dl:
|
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of youtube-dl:
|
||||||
- First of, make sure you are using the latest version of youtube-dl. Run `youtube-dl --version` and ensure your version is 2020.03.01. If it's not, see https://yt-dl.org/update on how to update. Issues with outdated version will be REJECTED.
|
- First of, make sure you are using the latest version of youtube-dl. Run `youtube-dl --version` and ensure your version is 2021.12.17. If it's not, see https://yt-dl.org/update on how to update. Issues with outdated version will be REJECTED.
|
||||||
- Search the bugtracker for similar site feature requests: http://yt-dl.org/search-issues. DO NOT post duplicates.
|
- Search the bugtracker for similar site feature requests: http://yt-dl.org/search-issues. DO NOT post duplicates.
|
||||||
- Finally, put x into all relevant boxes (like this [x])
|
- Finally, put x into all relevant boxes (like this [x])
|
||||||
-->
|
-->
|
||||||
|
|
||||||
- [ ] I'm reporting a site feature request
|
- [ ] I'm reporting a site feature request
|
||||||
- [ ] I've verified that I'm running youtube-dl version **2020.03.01**
|
- [ ] I've verified that I'm running youtube-dl version **2021.12.17**
|
||||||
- [ ] I've searched the bugtracker for similar site feature requests including closed ones
|
- [ ] I've searched the bugtracker for similar site feature requests including closed ones
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
6
.github/ISSUE_TEMPLATE/4_bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/4_bug_report.md
vendored
@@ -18,7 +18,7 @@ title: ''
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of youtube-dl:
|
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of youtube-dl:
|
||||||
- First of, make sure you are using the latest version of youtube-dl. Run `youtube-dl --version` and ensure your version is 2020.03.01. If it's not, see https://yt-dl.org/update on how to update. Issues with outdated version will be REJECTED.
|
- First of, make sure you are using the latest version of youtube-dl. Run `youtube-dl --version` and ensure your version is 2021.12.17. If it's not, see https://yt-dl.org/update on how to update. Issues with outdated version will be REJECTED.
|
||||||
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
|
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
|
||||||
- Make sure that all URLs and arguments with special characters are properly quoted or escaped as explained in http://yt-dl.org/escape.
|
- Make sure that all URLs and arguments with special characters are properly quoted or escaped as explained in http://yt-dl.org/escape.
|
||||||
- Search the bugtracker for similar issues: http://yt-dl.org/search-issues. DO NOT post duplicates.
|
- Search the bugtracker for similar issues: http://yt-dl.org/search-issues. DO NOT post duplicates.
|
||||||
@@ -27,7 +27,7 @@ Carefully read and work through this check list in order to prevent the most com
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
- [ ] I'm reporting a broken site support issue
|
- [ ] I'm reporting a broken site support issue
|
||||||
- [ ] I've verified that I'm running youtube-dl version **2020.03.01**
|
- [ ] I've verified that I'm running youtube-dl version **2021.12.17**
|
||||||
- [ ] I've checked that all provided URLs are alive and playable in a browser
|
- [ ] I've checked that all provided URLs are alive and playable in a browser
|
||||||
- [ ] I've checked that all URLs and arguments with special characters are properly quoted or escaped
|
- [ ] I've checked that all URLs and arguments with special characters are properly quoted or escaped
|
||||||
- [ ] I've searched the bugtracker for similar bug reports including closed ones
|
- [ ] I've searched the bugtracker for similar bug reports including closed ones
|
||||||
@@ -43,7 +43,7 @@ Add the `-v` flag to your command line you run youtube-dl with (`youtube-dl -v <
|
|||||||
[debug] User config: []
|
[debug] User config: []
|
||||||
[debug] Command-line args: [u'-v', u'http://www.youtube.com/watch?v=BaW_jenozKcj']
|
[debug] Command-line args: [u'-v', u'http://www.youtube.com/watch?v=BaW_jenozKcj']
|
||||||
[debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251
|
[debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251
|
||||||
[debug] youtube-dl version 2020.03.01
|
[debug] youtube-dl version 2021.12.17
|
||||||
[debug] Python version 2.7.11 - Windows-2003Server-5.2.3790-SP2
|
[debug] Python version 2.7.11 - Windows-2003Server-5.2.3790-SP2
|
||||||
[debug] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4
|
[debug] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4
|
||||||
[debug] Proxy map: {}
|
[debug] Proxy map: {}
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/5_feature_request.md
vendored
4
.github/ISSUE_TEMPLATE/5_feature_request.md
vendored
@@ -19,13 +19,13 @@ labels: 'request'
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of youtube-dl:
|
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of youtube-dl:
|
||||||
- First of, make sure you are using the latest version of youtube-dl. Run `youtube-dl --version` and ensure your version is 2020.03.01. If it's not, see https://yt-dl.org/update on how to update. Issues with outdated version will be REJECTED.
|
- First of, make sure you are using the latest version of youtube-dl. Run `youtube-dl --version` and ensure your version is 2021.12.17. If it's not, see https://yt-dl.org/update on how to update. Issues with outdated version will be REJECTED.
|
||||||
- Search the bugtracker for similar feature requests: http://yt-dl.org/search-issues. DO NOT post duplicates.
|
- Search the bugtracker for similar feature requests: http://yt-dl.org/search-issues. DO NOT post duplicates.
|
||||||
- Finally, put x into all relevant boxes (like this [x])
|
- Finally, put x into all relevant boxes (like this [x])
|
||||||
-->
|
-->
|
||||||
|
|
||||||
- [ ] I'm reporting a feature request
|
- [ ] I'm reporting a feature request
|
||||||
- [ ] I've verified that I'm running youtube-dl version **2020.03.01**
|
- [ ] I've verified that I'm running youtube-dl version **2021.12.17**
|
||||||
- [ ] I've searched the bugtracker for similar feature requests including closed ones
|
- [ ] I've searched the bugtracker for similar feature requests including closed ones
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -7,8 +7,10 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
### Before submitting a *pull request* make sure you have:
|
### Before submitting a *pull request* make sure you have:
|
||||||
- [ ] At least skimmed through [adding new extractor tutorial](https://github.com/ytdl-org/youtube-dl#adding-support-for-a-new-site) and [youtube-dl coding conventions](https://github.com/ytdl-org/youtube-dl#youtube-dl-coding-conventions) sections
|
|
||||||
- [ ] [Searched](https://github.com/ytdl-org/youtube-dl/search?q=is%3Apr&type=Issues) the bugtracker for similar pull requests
|
- [ ] [Searched](https://github.com/ytdl-org/youtube-dl/search?q=is%3Apr&type=Issues) the bugtracker for similar pull requests
|
||||||
|
- [ ] Read [adding new extractor tutorial](https://github.com/ytdl-org/youtube-dl#adding-support-for-a-new-site)
|
||||||
|
- [ ] Read [youtube-dl coding conventions](https://github.com/ytdl-org/youtube-dl#youtube-dl-coding-conventions) and adjusted the code to meet them
|
||||||
|
- [ ] Covered the code with tests (note that PRs without tests will be REJECTED)
|
||||||
- [ ] Checked the code with [flake8](https://pypi.python.org/pypi/flake8)
|
- [ ] Checked the code with [flake8](https://pypi.python.org/pypi/flake8)
|
||||||
|
|
||||||
### In order to be accepted and merged into youtube-dl each piece of code must be in public domain or released under [Unlicense](http://unlicense.org/). Check one of the following options:
|
### In order to be accepted and merged into youtube-dl each piece of code must be in public domain or released under [Unlicense](http://unlicense.org/). Check one of the following options:
|
||||||
|
|||||||
81
.github/workflows/ci.yml
vendored
Normal file
81
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
name: CI
|
||||||
|
on: [push, pull_request]
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
name: Tests
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-18.04]
|
||||||
|
# TODO: python 2.6
|
||||||
|
python-version: [2.7, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, pypy-2.7, pypy-3.6, pypy-3.7]
|
||||||
|
python-impl: [cpython]
|
||||||
|
ytdl-test-set: [core, download]
|
||||||
|
run-tests-ext: [sh]
|
||||||
|
include:
|
||||||
|
# python 3.2 is only available on windows via setup-python
|
||||||
|
- os: windows-2019
|
||||||
|
python-version: 3.2
|
||||||
|
python-impl: cpython
|
||||||
|
ytdl-test-set: core
|
||||||
|
run-tests-ext: bat
|
||||||
|
- os: windows-2019
|
||||||
|
python-version: 3.2
|
||||||
|
python-impl: cpython
|
||||||
|
ytdl-test-set: download
|
||||||
|
run-tests-ext: bat
|
||||||
|
# jython
|
||||||
|
- os: ubuntu-18.04
|
||||||
|
python-impl: jython
|
||||||
|
ytdl-test-set: core
|
||||||
|
run-tests-ext: sh
|
||||||
|
- os: ubuntu-18.04
|
||||||
|
python-impl: jython
|
||||||
|
ytdl-test-set: download
|
||||||
|
run-tests-ext: sh
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
if: ${{ matrix.python-impl == 'cpython' }}
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Set up Java 8
|
||||||
|
if: ${{ matrix.python-impl == 'jython' }}
|
||||||
|
uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: 8
|
||||||
|
- name: Install Jython
|
||||||
|
if: ${{ matrix.python-impl == 'jython' }}
|
||||||
|
run: |
|
||||||
|
wget https://repo1.maven.org/maven2/org/python/jython-installer/2.7.1/jython-installer-2.7.1.jar -O jython-installer.jar
|
||||||
|
java -jar jython-installer.jar -s -d "$HOME/jython"
|
||||||
|
echo "$HOME/jython/bin" >> $GITHUB_PATH
|
||||||
|
- name: Install nose
|
||||||
|
if: ${{ matrix.python-impl != 'jython' }}
|
||||||
|
run: pip install nose
|
||||||
|
- name: Install nose (Jython)
|
||||||
|
if: ${{ matrix.python-impl == 'jython' }}
|
||||||
|
# Working around deprecation of support for non-SNI clients at PyPI CDN (see https://status.python.org/incidents/hzmjhqsdjqgb)
|
||||||
|
run: |
|
||||||
|
wget https://files.pythonhosted.org/packages/99/4f/13fb671119e65c4dce97c60e67d3fd9e6f7f809f2b307e2611f4701205cb/nose-1.3.7-py2-none-any.whl
|
||||||
|
pip install nose-1.3.7-py2-none-any.whl
|
||||||
|
- name: Run tests
|
||||||
|
continue-on-error: ${{ matrix.ytdl-test-set == 'download' || matrix.python-impl == 'jython' }}
|
||||||
|
env:
|
||||||
|
YTDL_TEST_SET: ${{ matrix.ytdl-test-set }}
|
||||||
|
run: ./devscripts/run_tests.${{ matrix.run-tests-ext }}
|
||||||
|
flake8:
|
||||||
|
name: Linter
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: 3.9
|
||||||
|
- name: Install flake8
|
||||||
|
run: pip install flake8
|
||||||
|
- name: Run flake8
|
||||||
|
run: flake8 .
|
||||||
50
.travis.yml
50
.travis.yml
@@ -1,50 +0,0 @@
|
|||||||
language: python
|
|
||||||
python:
|
|
||||||
- "2.6"
|
|
||||||
- "2.7"
|
|
||||||
- "3.2"
|
|
||||||
- "3.3"
|
|
||||||
- "3.4"
|
|
||||||
- "3.5"
|
|
||||||
- "3.6"
|
|
||||||
- "pypy"
|
|
||||||
- "pypy3"
|
|
||||||
dist: trusty
|
|
||||||
env:
|
|
||||||
- YTDL_TEST_SET=core
|
|
||||||
- YTDL_TEST_SET=download
|
|
||||||
jobs:
|
|
||||||
include:
|
|
||||||
- python: 3.7
|
|
||||||
dist: xenial
|
|
||||||
env: YTDL_TEST_SET=core
|
|
||||||
- python: 3.7
|
|
||||||
dist: xenial
|
|
||||||
env: YTDL_TEST_SET=download
|
|
||||||
- python: 3.8
|
|
||||||
dist: xenial
|
|
||||||
env: YTDL_TEST_SET=core
|
|
||||||
- python: 3.8
|
|
||||||
dist: xenial
|
|
||||||
env: YTDL_TEST_SET=download
|
|
||||||
- python: 3.8-dev
|
|
||||||
dist: xenial
|
|
||||||
env: YTDL_TEST_SET=core
|
|
||||||
- python: 3.8-dev
|
|
||||||
dist: xenial
|
|
||||||
env: YTDL_TEST_SET=download
|
|
||||||
- env: JYTHON=true; YTDL_TEST_SET=core
|
|
||||||
- env: JYTHON=true; YTDL_TEST_SET=download
|
|
||||||
- name: flake8
|
|
||||||
python: 3.8
|
|
||||||
dist: xenial
|
|
||||||
install: pip install flake8
|
|
||||||
script: flake8 .
|
|
||||||
fast_finish: true
|
|
||||||
allow_failures:
|
|
||||||
- env: YTDL_TEST_SET=download
|
|
||||||
- env: JYTHON=true; YTDL_TEST_SET=core
|
|
||||||
- env: JYTHON=true; YTDL_TEST_SET=download
|
|
||||||
before_install:
|
|
||||||
- if [ "$JYTHON" == "true" ]; then ./devscripts/install_jython.sh; export PATH="$HOME/jython/bin:$PATH"; fi
|
|
||||||
script: ./devscripts/run_tests.sh
|
|
||||||
1
AUTHORS
1
AUTHORS
@@ -246,3 +246,4 @@ Enes Solak
|
|||||||
Nathan Rossi
|
Nathan Rossi
|
||||||
Thomas van der Berg
|
Thomas van der Berg
|
||||||
Luca Cherubin
|
Luca Cherubin
|
||||||
|
Adrian Heine
|
||||||
@@ -150,10 +150,10 @@ After you have ensured this site is distributing its content legally, you can fo
|
|||||||
# TODO more properties (see youtube_dl/extractor/common.py)
|
# TODO more properties (see youtube_dl/extractor/common.py)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
5. Add an import in [`youtube_dl/extractor/extractors.py`](https://github.com/ytdl-org/youtube-dl/blob/master/youtube_dl/extractor/extractors.py).
|
5. Add an import in [`youtube_dl/extractor/extractors.py`](https://github.com/ytdl-org/youtube-dl/blob/master/youtube_dl/extractor/extractors.py). This makes the extractor available for use, as long as the class ends with `IE`.
|
||||||
6. 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, then rename ``_TEST`` to ``_TESTS`` and make it into a list of dictionaries. 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.
|
6. 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, then rename ``_TEST`` to ``_TESTS`` and make it into a list of dictionaries. 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.
|
||||||
7. Have a look at [`youtube_dl/extractor/common.py`](https://github.com/ytdl-org/youtube-dl/blob/master/youtube_dl/extractor/common.py) for possible helper methods and a [detailed description of what your extractor should and may return](https://github.com/ytdl-org/youtube-dl/blob/7f41a598b3fba1bcab2817de64a08941200aa3c8/youtube_dl/extractor/common.py#L94-L303). Add tests and code for as many as you want.
|
7. Have a look at [`youtube_dl/extractor/common.py`](https://github.com/ytdl-org/youtube-dl/blob/master/youtube_dl/extractor/common.py) for possible helper methods and a [detailed description of what your extractor should and may return](https://github.com/ytdl-org/youtube-dl/blob/7f41a598b3fba1bcab2817de64a08941200aa3c8/youtube_dl/extractor/common.py#L94-L303). Add tests and code for as many as you want.
|
||||||
8. Make sure your code follows [youtube-dl coding conventions](#youtube-dl-coding-conventions) and check the code with [flake8](http://flake8.pycqa.org/en/latest/index.html#quickstart):
|
8. Make sure your code follows [youtube-dl coding conventions](#youtube-dl-coding-conventions) and check the code with [flake8](https://flake8.pycqa.org/en/latest/index.html#quickstart):
|
||||||
|
|
||||||
$ flake8 youtube_dl/extractor/yourextractor.py
|
$ flake8 youtube_dl/extractor/yourextractor.py
|
||||||
|
|
||||||
|
|||||||
561
README.md
561
README.md
@@ -1,4 +1,5 @@
|
|||||||
[](https://travis-ci.org/ytdl-org/youtube-dl)
|
[](https://github.com/ytdl-org/youtube-dl/actions?query=workflow%3ACI)
|
||||||
|
|
||||||
|
|
||||||
youtube-dl - download videos from youtube.com or other video platforms
|
youtube-dl - download videos from youtube.com or other video platforms
|
||||||
|
|
||||||
@@ -53,126 +54,139 @@ Alternatively, refer to the [developer instructions](#developer-instructions) fo
|
|||||||
# OPTIONS
|
# OPTIONS
|
||||||
-h, --help Print this help text and exit
|
-h, --help Print this help text and exit
|
||||||
--version Print program version and exit
|
--version Print program version and exit
|
||||||
-U, --update Update this program to latest version. Make
|
-U, --update Update this program to latest version.
|
||||||
sure that you have sufficient permissions
|
Make sure that you have sufficient
|
||||||
(run with sudo if needed)
|
permissions (run with sudo if needed)
|
||||||
-i, --ignore-errors Continue on download errors, for example to
|
-i, --ignore-errors Continue on download errors, for
|
||||||
skip unavailable videos in a playlist
|
example to skip unavailable videos in a
|
||||||
--abort-on-error Abort downloading of further videos (in the
|
playlist
|
||||||
playlist or the command line) if an error
|
--abort-on-error Abort downloading of further videos (in
|
||||||
occurs
|
the playlist or the command line) if an
|
||||||
--dump-user-agent Display the current browser identification
|
error occurs
|
||||||
|
--dump-user-agent Display the current browser
|
||||||
|
identification
|
||||||
--list-extractors List all supported extractors
|
--list-extractors List all supported extractors
|
||||||
--extractor-descriptions Output descriptions of all supported
|
--extractor-descriptions Output descriptions of all supported
|
||||||
extractors
|
extractors
|
||||||
--force-generic-extractor Force extraction to use the generic
|
--force-generic-extractor Force extraction to use the generic
|
||||||
extractor
|
extractor
|
||||||
--default-search PREFIX Use this prefix for unqualified URLs. For
|
--default-search PREFIX Use this prefix for unqualified URLs.
|
||||||
example "gvsearch2:" downloads two videos
|
For example "gvsearch2:" downloads two
|
||||||
from google videos for youtube-dl "large
|
videos from google videos for youtube-
|
||||||
apple". Use the value "auto" to let
|
dl "large apple". Use the value "auto"
|
||||||
youtube-dl guess ("auto_warning" to emit a
|
to let youtube-dl guess ("auto_warning"
|
||||||
warning when guessing). "error" just throws
|
to emit a warning when guessing).
|
||||||
an error. The default value "fixup_error"
|
"error" just throws an error. The
|
||||||
repairs broken URLs, but emits an error if
|
default value "fixup_error" repairs
|
||||||
this is not possible instead of searching.
|
broken URLs, but emits an error if this
|
||||||
--ignore-config Do not read configuration files. When given
|
is not possible instead of searching.
|
||||||
in the global configuration file
|
--ignore-config Do not read configuration files. When
|
||||||
/etc/youtube-dl.conf: Do not read the user
|
given in the global configuration file
|
||||||
configuration in ~/.config/youtube-
|
/etc/youtube-dl.conf: Do not read the
|
||||||
dl/config (%APPDATA%/youtube-dl/config.txt
|
user configuration in
|
||||||
on Windows)
|
~/.config/youtube-dl/config
|
||||||
--config-location PATH Location of the configuration file; either
|
(%APPDATA%/youtube-dl/config.txt on
|
||||||
the path to the config or its containing
|
Windows)
|
||||||
directory.
|
--config-location PATH Location of the configuration file;
|
||||||
--flat-playlist Do not extract the videos of a playlist,
|
either the path to the config or its
|
||||||
only list them.
|
containing directory.
|
||||||
|
--flat-playlist Do not extract the videos of a
|
||||||
|
playlist, only list them.
|
||||||
--mark-watched Mark videos watched (YouTube only)
|
--mark-watched Mark videos watched (YouTube only)
|
||||||
--no-mark-watched Do not mark videos watched (YouTube only)
|
--no-mark-watched Do not mark videos watched (YouTube
|
||||||
|
only)
|
||||||
--no-color Do not emit color codes in output
|
--no-color Do not emit color codes in output
|
||||||
|
|
||||||
## Network Options:
|
## Network Options:
|
||||||
--proxy URL Use the specified HTTP/HTTPS/SOCKS proxy.
|
--proxy URL Use the specified HTTP/HTTPS/SOCKS
|
||||||
To enable SOCKS proxy, specify a proper
|
proxy. To enable SOCKS proxy, specify a
|
||||||
scheme. For example
|
proper scheme. For example
|
||||||
socks5://127.0.0.1:1080/. Pass in an empty
|
socks5://127.0.0.1:1080/. Pass in an
|
||||||
string (--proxy "") for direct connection
|
empty string (--proxy "") for direct
|
||||||
--socket-timeout SECONDS Time to wait before giving up, in seconds
|
connection
|
||||||
|
--socket-timeout SECONDS Time to wait before giving up, in
|
||||||
|
seconds
|
||||||
--source-address IP Client-side IP address to bind to
|
--source-address IP Client-side IP address to bind to
|
||||||
-4, --force-ipv4 Make all connections via IPv4
|
-4, --force-ipv4 Make all connections via IPv4
|
||||||
-6, --force-ipv6 Make all connections via IPv6
|
-6, --force-ipv6 Make all connections via IPv6
|
||||||
|
|
||||||
## Geo Restriction:
|
## Geo Restriction:
|
||||||
--geo-verification-proxy URL Use this proxy to verify the IP address for
|
--geo-verification-proxy URL Use this proxy to verify the IP address
|
||||||
some geo-restricted sites. The default
|
for some geo-restricted sites. The
|
||||||
proxy specified by --proxy (or none, if the
|
default proxy specified by --proxy (or
|
||||||
option is not present) is used for the
|
none, if the option is not present) is
|
||||||
actual downloading.
|
used for the actual downloading.
|
||||||
--geo-bypass Bypass geographic restriction via faking
|
--geo-bypass Bypass geographic restriction via
|
||||||
X-Forwarded-For HTTP header
|
|
||||||
--no-geo-bypass Do not bypass geographic restriction via
|
|
||||||
faking X-Forwarded-For HTTP header
|
faking X-Forwarded-For HTTP header
|
||||||
--geo-bypass-country CODE Force bypass geographic restriction with
|
--no-geo-bypass Do not bypass geographic restriction
|
||||||
explicitly provided two-letter ISO 3166-2
|
via faking X-Forwarded-For HTTP header
|
||||||
country code
|
--geo-bypass-country CODE Force bypass geographic restriction
|
||||||
--geo-bypass-ip-block IP_BLOCK Force bypass geographic restriction with
|
with explicitly provided two-letter ISO
|
||||||
explicitly provided IP block in CIDR
|
3166-2 country code
|
||||||
notation
|
--geo-bypass-ip-block IP_BLOCK Force bypass geographic restriction
|
||||||
|
with explicitly provided IP block in
|
||||||
|
CIDR notation
|
||||||
|
|
||||||
## Video Selection:
|
## Video Selection:
|
||||||
--playlist-start NUMBER Playlist video to start at (default is 1)
|
--playlist-start NUMBER Playlist video to start at (default is
|
||||||
--playlist-end NUMBER Playlist video to end at (default is last)
|
1)
|
||||||
--playlist-items ITEM_SPEC Playlist video items to download. Specify
|
--playlist-end NUMBER Playlist video to end at (default is
|
||||||
indices of the videos in the playlist
|
last)
|
||||||
separated by commas like: "--playlist-items
|
--playlist-items ITEM_SPEC Playlist video items to download.
|
||||||
1,2,5,8" if you want to download videos
|
Specify indices of the videos in the
|
||||||
indexed 1, 2, 5, 8 in the playlist. You can
|
playlist separated by commas like: "--
|
||||||
specify range: "--playlist-items
|
playlist-items 1,2,5,8" if you want to
|
||||||
1-3,7,10-13", it will download the videos
|
download videos indexed 1, 2, 5, 8 in
|
||||||
at index 1, 2, 3, 7, 10, 11, 12 and 13.
|
the playlist. You can specify range: "
|
||||||
|
--playlist-items 1-3,7,10-13", it will
|
||||||
|
download the videos at index 1, 2, 3,
|
||||||
|
7, 10, 11, 12 and 13.
|
||||||
--match-title REGEX Download only matching titles (regex or
|
--match-title REGEX Download only matching titles (regex or
|
||||||
caseless sub-string)
|
caseless sub-string)
|
||||||
--reject-title REGEX Skip download for matching titles (regex or
|
--reject-title REGEX Skip download for matching titles
|
||||||
caseless sub-string)
|
(regex or caseless sub-string)
|
||||||
--max-downloads NUMBER Abort after downloading NUMBER files
|
--max-downloads NUMBER Abort after downloading NUMBER files
|
||||||
--min-filesize SIZE Do not download any videos smaller than
|
--min-filesize SIZE Do not download any videos smaller than
|
||||||
SIZE (e.g. 50k or 44.6m)
|
SIZE (e.g. 50k or 44.6m)
|
||||||
--max-filesize SIZE Do not download any videos larger than SIZE
|
--max-filesize SIZE Do not download any videos larger than
|
||||||
(e.g. 50k or 44.6m)
|
SIZE (e.g. 50k or 44.6m)
|
||||||
--date DATE Download only videos uploaded in this date
|
--date DATE Download only videos uploaded in this
|
||||||
--datebefore DATE Download only videos uploaded on or before
|
date
|
||||||
this date (i.e. inclusive)
|
--datebefore DATE Download only videos uploaded on or
|
||||||
--dateafter DATE Download only videos uploaded on or after
|
before this date (i.e. inclusive)
|
||||||
this date (i.e. inclusive)
|
--dateafter DATE Download only videos uploaded on or
|
||||||
--min-views COUNT Do not download any videos with less than
|
after this date (i.e. inclusive)
|
||||||
COUNT views
|
--min-views COUNT Do not download any videos with less
|
||||||
--max-views COUNT Do not download any videos with more than
|
than COUNT views
|
||||||
COUNT views
|
--max-views COUNT Do not download any videos with more
|
||||||
--match-filter FILTER Generic video filter. Specify any key (see
|
than COUNT views
|
||||||
the "OUTPUT TEMPLATE" for a list of
|
--match-filter FILTER Generic video filter. Specify any key
|
||||||
available keys) to match if the key is
|
(see the "OUTPUT TEMPLATE" for a list
|
||||||
present, !key to check if the key is not
|
of available keys) to match if the key
|
||||||
present, key > NUMBER (like "comment_count
|
is present, !key to check if the key is
|
||||||
> 12", also works with >=, <, <=, !=, =) to
|
not present, key > NUMBER (like
|
||||||
compare against a number, key = 'LITERAL'
|
"comment_count > 12", also works with
|
||||||
(like "uploader = 'Mike Smith'", also works
|
>=, <, <=, !=, =) to compare against a
|
||||||
with !=) to match against a string literal
|
number, key = 'LITERAL' (like "uploader
|
||||||
and & to require multiple matches. Values
|
= 'Mike Smith'", also works with !=) to
|
||||||
which are not known are excluded unless you
|
match against a string literal and & to
|
||||||
put a question mark (?) after the operator.
|
require multiple matches. Values which
|
||||||
For example, to only match videos that have
|
are not known are excluded unless you
|
||||||
been liked more than 100 times and disliked
|
put a question mark (?) after the
|
||||||
less than 50 times (or the dislike
|
operator. For example, to only match
|
||||||
functionality is not available at the given
|
videos that have been liked more than
|
||||||
service), but who also have a description,
|
100 times and disliked less than 50
|
||||||
use --match-filter "like_count > 100 &
|
times (or the dislike functionality is
|
||||||
|
not available at the given service),
|
||||||
|
but who also have a description, use
|
||||||
|
--match-filter "like_count > 100 &
|
||||||
dislike_count <? 50 & description" .
|
dislike_count <? 50 & description" .
|
||||||
--no-playlist Download only the video, if the URL refers
|
--no-playlist Download only the video, if the URL
|
||||||
to a video and a playlist.
|
refers to a video and a playlist.
|
||||||
--yes-playlist Download the playlist, if the URL refers to
|
--yes-playlist Download the playlist, if the URL
|
||||||
a video and a playlist.
|
refers to a video and a playlist.
|
||||||
--age-limit YEARS Download only videos suitable for the given
|
--age-limit YEARS Download only videos suitable for the
|
||||||
age
|
given age
|
||||||
--download-archive FILE Download only videos not listed in the
|
--download-archive FILE Download only videos not listed in the
|
||||||
archive file. Record the IDs of all
|
archive file. Record the IDs of all
|
||||||
downloaded videos in it.
|
downloaded videos in it.
|
||||||
@@ -180,168 +194,183 @@ Alternatively, refer to the [developer instructions](#developer-instructions) fo
|
|||||||
(experimental)
|
(experimental)
|
||||||
|
|
||||||
## Download Options:
|
## Download Options:
|
||||||
-r, --limit-rate RATE Maximum download rate in bytes per second
|
-r, --limit-rate RATE Maximum download rate in bytes per
|
||||||
(e.g. 50K or 4.2M)
|
second (e.g. 50K or 4.2M)
|
||||||
-R, --retries RETRIES Number of retries (default is 10), or
|
-R, --retries RETRIES Number of retries (default is 10), or
|
||||||
"infinite".
|
"infinite".
|
||||||
--fragment-retries RETRIES Number of retries for a fragment (default
|
--fragment-retries RETRIES Number of retries for a fragment
|
||||||
is 10), or "infinite" (DASH, hlsnative and
|
(default is 10), or "infinite" (DASH,
|
||||||
ISM)
|
hlsnative and ISM)
|
||||||
--skip-unavailable-fragments Skip unavailable fragments (DASH, hlsnative
|
--skip-unavailable-fragments Skip unavailable fragments (DASH,
|
||||||
and ISM)
|
hlsnative and ISM)
|
||||||
--abort-on-unavailable-fragment Abort downloading when some fragment is not
|
--abort-on-unavailable-fragment Abort downloading when some fragment is
|
||||||
available
|
not available
|
||||||
--keep-fragments Keep downloaded fragments on disk after
|
--keep-fragments Keep downloaded fragments on disk after
|
||||||
downloading is finished; fragments are
|
downloading is finished; fragments are
|
||||||
erased by default
|
erased by default
|
||||||
--buffer-size SIZE Size of download buffer (e.g. 1024 or 16K)
|
--buffer-size SIZE Size of download buffer (e.g. 1024 or
|
||||||
(default is 1024)
|
16K) (default is 1024)
|
||||||
--no-resize-buffer Do not automatically adjust the buffer
|
--no-resize-buffer Do not automatically adjust the buffer
|
||||||
size. By default, the buffer size is
|
size. By default, the buffer size is
|
||||||
automatically resized from an initial value
|
automatically resized from an initial
|
||||||
of SIZE.
|
value of SIZE.
|
||||||
--http-chunk-size SIZE Size of a chunk for chunk-based HTTP
|
--http-chunk-size SIZE Size of a chunk for chunk-based HTTP
|
||||||
downloading (e.g. 10485760 or 10M) (default
|
downloading (e.g. 10485760 or 10M)
|
||||||
is disabled). May be useful for bypassing
|
(default is disabled). May be useful
|
||||||
bandwidth throttling imposed by a webserver
|
for bypassing bandwidth throttling
|
||||||
(experimental)
|
imposed by a webserver (experimental)
|
||||||
--playlist-reverse Download playlist videos in reverse order
|
--playlist-reverse Download playlist videos in reverse
|
||||||
--playlist-random Download playlist videos in random order
|
order
|
||||||
|
--playlist-random Download playlist videos in random
|
||||||
|
order
|
||||||
--xattr-set-filesize Set file xattribute ytdl.filesize with
|
--xattr-set-filesize Set file xattribute ytdl.filesize with
|
||||||
expected file size
|
expected file size
|
||||||
--hls-prefer-native Use the native HLS downloader instead of
|
--hls-prefer-native Use the native HLS downloader instead
|
||||||
ffmpeg
|
of ffmpeg
|
||||||
--hls-prefer-ffmpeg Use ffmpeg instead of the native HLS
|
--hls-prefer-ffmpeg Use ffmpeg instead of the native HLS
|
||||||
downloader
|
downloader
|
||||||
--hls-use-mpegts Use the mpegts container for HLS videos,
|
--hls-use-mpegts Use the mpegts container for HLS
|
||||||
allowing to play the video while
|
videos, allowing to play the video
|
||||||
downloading (some players may not be able
|
while downloading (some players may not
|
||||||
to play it)
|
be able to play it)
|
||||||
--external-downloader COMMAND Use the specified external downloader.
|
--external-downloader COMMAND Use the specified external downloader.
|
||||||
Currently supports
|
Currently supports aria2c,avconv,axel,c
|
||||||
aria2c,avconv,axel,curl,ffmpeg,httpie,wget
|
url,ffmpeg,httpie,wget
|
||||||
--external-downloader-args ARGS Give these arguments to the external
|
--external-downloader-args ARGS Give these arguments to the external
|
||||||
downloader
|
downloader
|
||||||
|
|
||||||
## Filesystem Options:
|
## Filesystem Options:
|
||||||
-a, --batch-file FILE File containing URLs to download ('-' for
|
-a, --batch-file FILE File containing URLs to download ('-'
|
||||||
stdin), one URL per line. Lines starting
|
for stdin), one URL per line. Lines
|
||||||
with '#', ';' or ']' are considered as
|
starting with '#', ';' or ']' are
|
||||||
comments and ignored.
|
considered as comments and ignored.
|
||||||
--id Use only video ID in file name
|
--id Use only video ID in file name
|
||||||
-o, --output TEMPLATE Output filename template, see the "OUTPUT
|
-o, --output TEMPLATE Output filename template, see the
|
||||||
TEMPLATE" for all the info
|
"OUTPUT TEMPLATE" for all the info
|
||||||
--autonumber-start NUMBER Specify the start value for %(autonumber)s
|
--output-na-placeholder PLACEHOLDER Placeholder value for unavailable meta
|
||||||
(default is 1)
|
fields in output filename template
|
||||||
|
(default is "NA")
|
||||||
|
--autonumber-start NUMBER Specify the start value for
|
||||||
|
%(autonumber)s (default is 1)
|
||||||
--restrict-filenames Restrict filenames to only ASCII
|
--restrict-filenames Restrict filenames to only ASCII
|
||||||
characters, and avoid "&" and spaces in
|
characters, and avoid "&" and spaces in
|
||||||
filenames
|
filenames
|
||||||
-w, --no-overwrites Do not overwrite files
|
-w, --no-overwrites Do not overwrite files
|
||||||
-c, --continue Force resume of partially downloaded files.
|
-c, --continue Force resume of partially downloaded
|
||||||
By default, youtube-dl will resume
|
files. By default, youtube-dl will
|
||||||
downloads if possible.
|
resume downloads if possible.
|
||||||
--no-continue Do not resume partially downloaded files
|
--no-continue Do not resume partially downloaded
|
||||||
(restart from beginning)
|
files (restart from beginning)
|
||||||
--no-part Do not use .part files - write directly
|
--no-part Do not use .part files - write directly
|
||||||
into output file
|
into output file
|
||||||
--no-mtime Do not use the Last-modified header to set
|
--no-mtime Do not use the Last-modified header to
|
||||||
the file modification time
|
set the file modification time
|
||||||
--write-description Write video description to a .description
|
--write-description Write video description to a
|
||||||
|
.description file
|
||||||
|
--write-info-json Write video metadata to a .info.json
|
||||||
file
|
file
|
||||||
--write-info-json Write video metadata to a .info.json file
|
|
||||||
--write-annotations Write video annotations to a
|
--write-annotations Write video annotations to a
|
||||||
.annotations.xml file
|
.annotations.xml file
|
||||||
--load-info-json FILE JSON file containing the video information
|
--load-info-json FILE JSON file containing the video
|
||||||
(created with the "--write-info-json"
|
information (created with the "--write-
|
||||||
option)
|
info-json" option)
|
||||||
--cookies FILE File to read cookies from and dump cookie
|
--cookies FILE File to read cookies from and dump
|
||||||
jar in
|
cookie jar in
|
||||||
--cache-dir DIR Location in the filesystem where youtube-dl
|
--cache-dir DIR Location in the filesystem where
|
||||||
can store some downloaded information
|
youtube-dl can store some downloaded
|
||||||
permanently. By default
|
information permanently. By default
|
||||||
$XDG_CACHE_HOME/youtube-dl or
|
$XDG_CACHE_HOME/youtube-dl or
|
||||||
~/.cache/youtube-dl . At the moment, only
|
~/.cache/youtube-dl . At the moment,
|
||||||
YouTube player files (for videos with
|
only YouTube player files (for videos
|
||||||
obfuscated signatures) are cached, but that
|
with obfuscated signatures) are cached,
|
||||||
may change.
|
but that may change.
|
||||||
--no-cache-dir Disable filesystem caching
|
--no-cache-dir Disable filesystem caching
|
||||||
--rm-cache-dir Delete all filesystem cache files
|
--rm-cache-dir Delete all filesystem cache files
|
||||||
|
|
||||||
## Thumbnail images:
|
## Thumbnail Options:
|
||||||
--write-thumbnail Write thumbnail image to disk
|
--write-thumbnail Write thumbnail image to disk
|
||||||
--write-all-thumbnails Write all thumbnail image formats to disk
|
--write-all-thumbnails Write all thumbnail image formats to
|
||||||
--list-thumbnails Simulate and list all available thumbnail
|
disk
|
||||||
formats
|
--list-thumbnails Simulate and list all available
|
||||||
|
thumbnail formats
|
||||||
|
|
||||||
## Verbosity / Simulation Options:
|
## Verbosity / Simulation Options:
|
||||||
-q, --quiet Activate quiet mode
|
-q, --quiet Activate quiet mode
|
||||||
--no-warnings Ignore warnings
|
--no-warnings Ignore warnings
|
||||||
-s, --simulate Do not download the video and do not write
|
-s, --simulate Do not download the video and do not
|
||||||
anything to disk
|
write anything to disk
|
||||||
--skip-download Do not download the video
|
--skip-download Do not download the video
|
||||||
-g, --get-url Simulate, quiet but print URL
|
-g, --get-url Simulate, quiet but print URL
|
||||||
-e, --get-title Simulate, quiet but print title
|
-e, --get-title Simulate, quiet but print title
|
||||||
--get-id Simulate, quiet but print id
|
--get-id Simulate, quiet but print id
|
||||||
--get-thumbnail Simulate, quiet but print thumbnail URL
|
--get-thumbnail Simulate, quiet but print thumbnail URL
|
||||||
--get-description Simulate, quiet but print video description
|
--get-description Simulate, quiet but print video
|
||||||
|
description
|
||||||
--get-duration Simulate, quiet but print video length
|
--get-duration Simulate, quiet but print video length
|
||||||
--get-filename Simulate, quiet but print output filename
|
--get-filename Simulate, quiet but print output
|
||||||
|
filename
|
||||||
--get-format Simulate, quiet but print output format
|
--get-format Simulate, quiet but print output format
|
||||||
-j, --dump-json Simulate, quiet but print JSON information.
|
-j, --dump-json Simulate, quiet but print JSON
|
||||||
See the "OUTPUT TEMPLATE" for a description
|
information. See the "OUTPUT TEMPLATE"
|
||||||
of available keys.
|
for a description of available keys.
|
||||||
-J, --dump-single-json Simulate, quiet but print JSON information
|
-J, --dump-single-json Simulate, quiet but print JSON
|
||||||
for each command-line argument. If the URL
|
information for each command-line
|
||||||
refers to a playlist, dump the whole
|
argument. If the URL refers to a
|
||||||
playlist information in a single line.
|
playlist, dump the whole playlist
|
||||||
--print-json Be quiet and print the video information as
|
information in a single line.
|
||||||
JSON (video is still being downloaded).
|
--print-json Be quiet and print the video
|
||||||
|
information as JSON (video is still
|
||||||
|
being downloaded).
|
||||||
--newline Output progress bar as new lines
|
--newline Output progress bar as new lines
|
||||||
--no-progress Do not print progress bar
|
--no-progress Do not print progress bar
|
||||||
--console-title Display progress in console titlebar
|
--console-title Display progress in console titlebar
|
||||||
-v, --verbose Print various debugging information
|
-v, --verbose Print various debugging information
|
||||||
--dump-pages Print downloaded pages encoded using base64
|
--dump-pages Print downloaded pages encoded using
|
||||||
to debug problems (very verbose)
|
base64 to debug problems (very verbose)
|
||||||
--write-pages Write downloaded intermediary pages to
|
--write-pages Write downloaded intermediary pages to
|
||||||
files in the current directory to debug
|
files in the current directory to debug
|
||||||
problems
|
problems
|
||||||
--print-traffic Display sent and read HTTP traffic
|
--print-traffic Display sent and read HTTP traffic
|
||||||
-C, --call-home Contact the youtube-dl server for debugging
|
-C, --call-home Contact the youtube-dl server for
|
||||||
--no-call-home Do NOT contact the youtube-dl server for
|
|
||||||
debugging
|
debugging
|
||||||
|
--no-call-home Do NOT contact the youtube-dl server
|
||||||
|
for debugging
|
||||||
|
|
||||||
## Workarounds:
|
## Workarounds:
|
||||||
--encoding ENCODING Force the specified encoding (experimental)
|
--encoding ENCODING Force the specified encoding
|
||||||
|
(experimental)
|
||||||
--no-check-certificate Suppress HTTPS certificate validation
|
--no-check-certificate Suppress HTTPS certificate validation
|
||||||
--prefer-insecure Use an unencrypted connection to retrieve
|
--prefer-insecure Use an unencrypted connection to
|
||||||
information about the video. (Currently
|
retrieve information about the video.
|
||||||
supported only for YouTube)
|
(Currently supported only for YouTube)
|
||||||
--user-agent UA Specify a custom user agent
|
--user-agent UA Specify a custom user agent
|
||||||
--referer URL Specify a custom referer, use if the video
|
--referer URL Specify a custom referer, use if the
|
||||||
access is restricted to one domain
|
video access is restricted to one
|
||||||
--add-header FIELD:VALUE Specify a custom HTTP header and its value,
|
domain
|
||||||
separated by a colon ':'. You can use this
|
--add-header FIELD:VALUE Specify a custom HTTP header and its
|
||||||
option multiple times
|
value, separated by a colon ':'. You
|
||||||
|
can use this option multiple times
|
||||||
--bidi-workaround Work around terminals that lack
|
--bidi-workaround Work around terminals that lack
|
||||||
bidirectional text support. Requires bidiv
|
bidirectional text support. Requires
|
||||||
or fribidi executable in PATH
|
bidiv or fribidi executable in PATH
|
||||||
--sleep-interval SECONDS Number of seconds to sleep before each
|
--sleep-interval SECONDS Number of seconds to sleep before each
|
||||||
download when used alone or a lower bound
|
download when used alone or a lower
|
||||||
of a range for randomized sleep before each
|
bound of a range for randomized sleep
|
||||||
download (minimum possible number of
|
before each download (minimum possible
|
||||||
seconds to sleep) when used along with
|
number of seconds to sleep) when used
|
||||||
--max-sleep-interval.
|
along with --max-sleep-interval.
|
||||||
--max-sleep-interval SECONDS Upper bound of a range for randomized sleep
|
--max-sleep-interval SECONDS Upper bound of a range for randomized
|
||||||
before each download (maximum possible
|
sleep before each download (maximum
|
||||||
number of seconds to sleep). Must only be
|
possible number of seconds to sleep).
|
||||||
used along with --min-sleep-interval.
|
Must only be used along with --min-
|
||||||
|
sleep-interval.
|
||||||
|
|
||||||
## Video Format Options:
|
## Video Format Options:
|
||||||
-f, --format FORMAT Video format code, see the "FORMAT
|
-f, --format FORMAT Video format code, see the "FORMAT
|
||||||
SELECTION" for all the info
|
SELECTION" for all the info
|
||||||
--all-formats Download all available video formats
|
--all-formats Download all available video formats
|
||||||
--prefer-free-formats Prefer free video formats unless a specific
|
--prefer-free-formats Prefer free video formats unless a
|
||||||
one is requested
|
specific one is requested
|
||||||
-F, --list-formats List all available formats of requested
|
-F, --list-formats List all available formats of requested
|
||||||
videos
|
videos
|
||||||
--youtube-skip-dash-manifest Do not download the DASH manifests and
|
--youtube-skip-dash-manifest Do not download the DASH manifests and
|
||||||
@@ -349,93 +378,102 @@ Alternatively, refer to the [developer instructions](#developer-instructions) fo
|
|||||||
--merge-output-format FORMAT If a merge is required (e.g.
|
--merge-output-format FORMAT If a merge is required (e.g.
|
||||||
bestvideo+bestaudio), output to given
|
bestvideo+bestaudio), output to given
|
||||||
container format. One of mkv, mp4, ogg,
|
container format. One of mkv, mp4, ogg,
|
||||||
webm, flv. Ignored if no merge is required
|
webm, flv. Ignored if no merge is
|
||||||
|
required
|
||||||
|
|
||||||
## Subtitle Options:
|
## Subtitle Options:
|
||||||
--write-sub Write subtitle file
|
--write-sub Write subtitle file
|
||||||
--write-auto-sub Write automatically generated subtitle file
|
--write-auto-sub Write automatically generated subtitle
|
||||||
(YouTube only)
|
file (YouTube only)
|
||||||
--all-subs Download all the available subtitles of the
|
--all-subs Download all the available subtitles of
|
||||||
|
the video
|
||||||
|
--list-subs List all available subtitles for the
|
||||||
video
|
video
|
||||||
--list-subs List all available subtitles for the video
|
|
||||||
--sub-format FORMAT Subtitle format, accepts formats
|
--sub-format FORMAT Subtitle format, accepts formats
|
||||||
preference, for example: "srt" or
|
preference, for example: "srt" or
|
||||||
"ass/srt/best"
|
"ass/srt/best"
|
||||||
--sub-lang LANGS Languages of the subtitles to download
|
--sub-lang LANGS Languages of the subtitles to download
|
||||||
(optional) separated by commas, use --list-
|
(optional) separated by commas, use
|
||||||
subs for available language tags
|
--list-subs for available language tags
|
||||||
|
|
||||||
## Authentication Options:
|
## Authentication Options:
|
||||||
-u, --username USERNAME Login with this account ID
|
-u, --username USERNAME Login with this account ID
|
||||||
-p, --password PASSWORD Account password. If this option is left
|
-p, --password PASSWORD Account password. If this option is
|
||||||
out, youtube-dl will ask interactively.
|
left out, youtube-dl will ask
|
||||||
|
interactively.
|
||||||
-2, --twofactor TWOFACTOR Two-factor authentication code
|
-2, --twofactor TWOFACTOR Two-factor authentication code
|
||||||
-n, --netrc Use .netrc authentication data
|
-n, --netrc Use .netrc authentication data
|
||||||
--video-password PASSWORD Video password (vimeo, smotri, youku)
|
--video-password PASSWORD Video password (vimeo, youku)
|
||||||
|
|
||||||
## Adobe Pass Options:
|
## Adobe Pass Options:
|
||||||
--ap-mso MSO Adobe Pass multiple-system operator (TV
|
--ap-mso MSO Adobe Pass multiple-system operator (TV
|
||||||
provider) identifier, use --ap-list-mso for
|
provider) identifier, use --ap-list-mso
|
||||||
a list of available MSOs
|
for a list of available MSOs
|
||||||
--ap-username USERNAME Multiple-system operator account login
|
--ap-username USERNAME Multiple-system operator account login
|
||||||
--ap-password PASSWORD Multiple-system operator account password.
|
--ap-password PASSWORD Multiple-system operator account
|
||||||
If this option is left out, youtube-dl will
|
password. If this option is left out,
|
||||||
ask interactively.
|
youtube-dl will ask interactively.
|
||||||
--ap-list-mso List all supported multiple-system
|
--ap-list-mso List all supported multiple-system
|
||||||
operators
|
operators
|
||||||
|
|
||||||
## Post-processing Options:
|
## Post-processing Options:
|
||||||
-x, --extract-audio Convert video files to audio-only files
|
-x, --extract-audio Convert video files to audio-only files
|
||||||
(requires ffmpeg or avconv and ffprobe or
|
(requires ffmpeg/avconv and
|
||||||
avprobe)
|
ffprobe/avprobe)
|
||||||
--audio-format FORMAT Specify audio format: "best", "aac",
|
--audio-format FORMAT Specify audio format: "best", "aac",
|
||||||
"flac", "mp3", "m4a", "opus", "vorbis", or
|
"flac", "mp3", "m4a", "opus", "vorbis",
|
||||||
"wav"; "best" by default; No effect without
|
or "wav"; "best" by default; No effect
|
||||||
-x
|
without -x
|
||||||
--audio-quality QUALITY Specify ffmpeg/avconv audio quality, insert
|
--audio-quality QUALITY Specify ffmpeg/avconv audio quality,
|
||||||
a value between 0 (better) and 9 (worse)
|
insert a value between 0 (better) and 9
|
||||||
for VBR or a specific bitrate like 128K
|
(worse) for VBR or a specific bitrate
|
||||||
(default 5)
|
like 128K (default 5)
|
||||||
--recode-video FORMAT Encode the video to another format if
|
--recode-video FORMAT Encode the video to another format if
|
||||||
necessary (currently supported:
|
necessary (currently supported:
|
||||||
mp4|flv|ogg|webm|mkv|avi)
|
mp4|flv|ogg|webm|mkv|avi)
|
||||||
--postprocessor-args ARGS Give these arguments to the postprocessor
|
--postprocessor-args ARGS Give these arguments to the
|
||||||
-k, --keep-video Keep the video file on disk after the post-
|
postprocessor
|
||||||
processing; the video is erased by default
|
-k, --keep-video Keep the video file on disk after the
|
||||||
--no-post-overwrites Do not overwrite post-processed files; the
|
post-processing; the video is erased by
|
||||||
post-processed files are overwritten by
|
|
||||||
default
|
default
|
||||||
--embed-subs Embed subtitles in the video (only for mp4,
|
--no-post-overwrites Do not overwrite post-processed files;
|
||||||
webm and mkv videos)
|
the post-processed files are
|
||||||
--embed-thumbnail Embed thumbnail in the audio as cover art
|
overwritten by default
|
||||||
|
--embed-subs Embed subtitles in the video (only for
|
||||||
|
mp4, webm and mkv videos)
|
||||||
|
--embed-thumbnail Embed thumbnail in the audio as cover
|
||||||
|
art
|
||||||
--add-metadata Write metadata to the video file
|
--add-metadata Write metadata to the video file
|
||||||
--metadata-from-title FORMAT Parse additional metadata like song title /
|
--metadata-from-title FORMAT Parse additional metadata like song
|
||||||
artist from the video title. The format
|
title / artist from the video title.
|
||||||
syntax is the same as --output. Regular
|
The format syntax is the same as
|
||||||
expression with named capture groups may
|
--output. Regular expression with named
|
||||||
also be used. The parsed parameters replace
|
capture groups may also be used. The
|
||||||
existing values. Example: --metadata-from-
|
parsed parameters replace existing
|
||||||
title "%(artist)s - %(title)s" matches a
|
values. Example: --metadata-from-title
|
||||||
title like "Coldplay - Paradise". Example
|
"%(artist)s - %(title)s" matches a
|
||||||
(regex): --metadata-from-title
|
title like "Coldplay - Paradise".
|
||||||
|
Example (regex): --metadata-from-title
|
||||||
"(?P<artist>.+?) - (?P<title>.+)"
|
"(?P<artist>.+?) - (?P<title>.+)"
|
||||||
--xattrs Write metadata to the video file's xattrs
|
--xattrs Write metadata to the video file's
|
||||||
(using dublin core and xdg standards)
|
xattrs (using dublin core and xdg
|
||||||
--fixup POLICY Automatically correct known faults of the
|
standards)
|
||||||
file. One of never (do nothing), warn (only
|
--fixup POLICY Automatically correct known faults of
|
||||||
emit a warning), detect_or_warn (the
|
the file. One of never (do nothing),
|
||||||
default; fix file if we can, warn
|
warn (only emit a warning),
|
||||||
otherwise)
|
detect_or_warn (the default; fix file
|
||||||
--prefer-avconv Prefer avconv over ffmpeg for running the
|
if we can, warn otherwise)
|
||||||
postprocessors
|
--prefer-avconv Prefer avconv over ffmpeg for running
|
||||||
--prefer-ffmpeg Prefer ffmpeg over avconv for running the
|
the postprocessors
|
||||||
postprocessors (default)
|
--prefer-ffmpeg Prefer ffmpeg over avconv for running
|
||||||
|
the postprocessors (default)
|
||||||
--ffmpeg-location PATH Location of the ffmpeg/avconv binary;
|
--ffmpeg-location PATH Location of the ffmpeg/avconv binary;
|
||||||
either the path to the binary or its
|
either the path to the binary or its
|
||||||
containing directory.
|
containing directory.
|
||||||
--exec CMD Execute a command on the file after
|
--exec CMD Execute a command on the file after
|
||||||
downloading, similar to find's -exec
|
downloading and post-processing,
|
||||||
syntax. Example: --exec 'adb push {}
|
similar to find's -exec syntax.
|
||||||
|
Example: --exec 'adb push {}
|
||||||
/sdcard/Music/ && rm {}'
|
/sdcard/Music/ && rm {}'
|
||||||
--convert-subs FORMAT Convert the subtitles to other format
|
--convert-subs FORMAT Convert the subtitles to other format
|
||||||
(currently supported: srt|ass|vtt|lrc)
|
(currently supported: srt|ass|vtt|lrc)
|
||||||
@@ -545,7 +583,7 @@ The basic usage is not to set any template arguments when downloading a single f
|
|||||||
- `extractor` (string): Name of the extractor
|
- `extractor` (string): Name of the extractor
|
||||||
- `extractor_key` (string): Key name of the extractor
|
- `extractor_key` (string): Key name of the extractor
|
||||||
- `epoch` (numeric): Unix epoch when creating the file
|
- `epoch` (numeric): Unix epoch when creating the file
|
||||||
- `autonumber` (numeric): Five-digit number that will be increased with each download, starting at zero
|
- `autonumber` (numeric): Number that will be increased with each download, starting at `--autonumber-start`
|
||||||
- `playlist` (string): Name or id of the playlist that contains the video
|
- `playlist` (string): Name or id of the playlist that contains the video
|
||||||
- `playlist_index` (numeric): Index of the video in the playlist padded with leading zeros according to the total length of the playlist
|
- `playlist_index` (numeric): Index of the video in the playlist padded with leading zeros according to the total length of the playlist
|
||||||
- `playlist_id` (string): Playlist identifier
|
- `playlist_id` (string): Playlist identifier
|
||||||
@@ -582,7 +620,7 @@ Available for the media that is a track or a part of a music album:
|
|||||||
- `disc_number` (numeric): Number of the disc or other physical medium the track belongs to
|
- `disc_number` (numeric): Number of the disc or other physical medium the track belongs to
|
||||||
- `release_year` (numeric): Year (YYYY) when the album was released
|
- `release_year` (numeric): Year (YYYY) when the album was released
|
||||||
|
|
||||||
Each aforementioned sequence when referenced in an output template will be replaced by the actual value corresponding to the sequence name. Note that some of the sequences are not guaranteed to be present since they depend on the metadata obtained by a particular extractor. Such sequences will be replaced with `NA`.
|
Each aforementioned sequence when referenced in an output template will be replaced by the actual value corresponding to the sequence name. Note that some of the sequences are not guaranteed to be present since they depend on the metadata obtained by a particular extractor. Such sequences will be replaced with placeholder value provided with `--output-na-placeholder` (`NA` by default).
|
||||||
|
|
||||||
For example for `-o %(title)s-%(id)s.%(ext)s` and an mp4 video with title `youtube-dl test video` and id `BaW_jenozKcj`, this will result in a `youtube-dl test video-BaW_jenozKcj.mp4` file created in the current directory.
|
For example for `-o %(title)s-%(id)s.%(ext)s` and an mp4 video with title `youtube-dl test video` and id `BaW_jenozKcj`, this will result in a `youtube-dl test video-BaW_jenozKcj.mp4` file created in the current directory.
|
||||||
|
|
||||||
@@ -677,6 +715,7 @@ Also filtering work for comparisons `=` (equals), `^=` (starts with), `$=` (ends
|
|||||||
- `container`: Name of the container format
|
- `container`: Name of the container format
|
||||||
- `protocol`: The protocol that will be used for the actual download, lower-case (`http`, `https`, `rtsp`, `rtmp`, `rtmpe`, `mms`, `f4m`, `ism`, `http_dash_segments`, `m3u8`, or `m3u8_native`)
|
- `protocol`: The protocol that will be used for the actual download, lower-case (`http`, `https`, `rtsp`, `rtmp`, `rtmpe`, `mms`, `f4m`, `ism`, `http_dash_segments`, `m3u8`, or `m3u8_native`)
|
||||||
- `format_id`: A short description of the format
|
- `format_id`: A short description of the format
|
||||||
|
- `language`: Language code
|
||||||
|
|
||||||
Any string comparison may be prefixed with negation `!` in order to produce an opposite comparison, e.g. `!*=` (does not contain).
|
Any string comparison may be prefixed with negation `!` in order to produce an opposite comparison, e.g. `!*=` (does not contain).
|
||||||
|
|
||||||
@@ -835,7 +874,9 @@ In February 2015, the new YouTube player contained a character sequence in a str
|
|||||||
|
|
||||||
### HTTP Error 429: Too Many Requests or 402: Payment Required
|
### HTTP Error 429: Too Many Requests or 402: Payment Required
|
||||||
|
|
||||||
These two error codes indicate that the service is blocking your IP address because of overuse. Contact the service and ask them to unblock your IP address, or - if you have acquired a whitelisted IP address already - use the [`--proxy` or `--source-address` options](#network-options) to select another IP address.
|
These two error codes indicate that the service is blocking your IP address because of overuse. Usually this is a soft block meaning that you can gain access again after solving CAPTCHA. Just open a browser and solve a CAPTCHA the service suggests you and after that [pass cookies](#how-do-i-pass-cookies-to-youtube-dl) to youtube-dl. Note that if your machine has multiple external IPs then you should also pass exactly the same IP you've used for solving CAPTCHA with [`--source-address`](#network-options). Also you may need to pass a `User-Agent` HTTP header of your browser with [`--user-agent`](#workarounds).
|
||||||
|
|
||||||
|
If this is not the case (no CAPTCHA suggested to solve by the service) then you can contact the service and ask them to unblock your IP address, or - if you have acquired a whitelisted IP address already - use the [`--proxy` or `--source-address` options](#network-options) to select another IP address.
|
||||||
|
|
||||||
### SyntaxError: Non-ASCII character
|
### SyntaxError: Non-ASCII character
|
||||||
|
|
||||||
@@ -852,7 +893,7 @@ Since June 2012 ([#342](https://github.com/ytdl-org/youtube-dl/issues/342)) yout
|
|||||||
|
|
||||||
### The exe throws an error due to missing `MSVCR100.dll`
|
### The exe throws an error due to missing `MSVCR100.dll`
|
||||||
|
|
||||||
To run the exe you need to install first the [Microsoft Visual C++ 2010 Redistributable Package (x86)](https://www.microsoft.com/en-US/download/details.aspx?id=5555).
|
To run the exe you need to install first the [Microsoft Visual C++ 2010 Service Pack 1 Redistributable Package (x86)](https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x86.exe).
|
||||||
|
|
||||||
### On Windows, how should I set up ffmpeg and youtube-dl? Where should I put the exe files?
|
### On Windows, how should I set up ffmpeg and youtube-dl? Where should I put the exe files?
|
||||||
|
|
||||||
@@ -877,7 +918,7 @@ Either prepend `https://www.youtube.com/watch?v=` or separate the ID from the op
|
|||||||
|
|
||||||
Use the `--cookies` option, for example `--cookies /path/to/cookies/file.txt`.
|
Use the `--cookies` option, for example `--cookies /path/to/cookies/file.txt`.
|
||||||
|
|
||||||
In order to extract cookies from browser use any conforming browser extension for exporting cookies. For example, [cookies.txt](https://chrome.google.com/webstore/detail/cookiestxt/njabckikapfpffapmjgojcnbfjonfjfg) (for Chrome) or [cookies.txt](https://addons.mozilla.org/en-US/firefox/addon/cookies-txt/) (for Firefox).
|
In order to extract cookies from browser use any conforming browser extension for exporting cookies. For example, [Get cookies.txt](https://chrome.google.com/webstore/detail/get-cookiestxt/bgaddhkoddajcdgocldbbfleckgcbcid/) (for Chrome) or [cookies.txt](https://addons.mozilla.org/en-US/firefox/addon/cookies-txt/) (for Firefox).
|
||||||
|
|
||||||
Note that the cookies file must be in Mozilla/Netscape format and the first line of the cookies file must be either `# HTTP Cookie File` or `# Netscape HTTP Cookie File`. Make sure you have correct [newline format](https://en.wikipedia.org/wiki/Newline) in the cookies file and convert newlines if necessary to correspond with your OS, namely `CRLF` (`\r\n`) for Windows and `LF` (`\n`) for Unix and Unix-like systems (Linux, macOS, etc.). `HTTP Error 400: Bad Request` when using `--cookies` is a good sign of invalid newline format.
|
Note that the cookies file must be in Mozilla/Netscape format and the first line of the cookies file must be either `# HTTP Cookie File` or `# Netscape HTTP Cookie File`. Make sure you have correct [newline format](https://en.wikipedia.org/wiki/Newline) in the cookies file and convert newlines if necessary to correspond with your OS, namely `CRLF` (`\r\n`) for Windows and `LF` (`\n`) for Unix and Unix-like systems (Linux, macOS, etc.). `HTTP Error 400: Bad Request` when using `--cookies` is a good sign of invalid newline format.
|
||||||
|
|
||||||
@@ -1028,9 +1069,11 @@ After you have ensured this site is distributing its content legally, you can fo
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
5. Add an import in [`youtube_dl/extractor/extractors.py`](https://github.com/ytdl-org/youtube-dl/blob/master/youtube_dl/extractor/extractors.py).
|
5. Add an import in [`youtube_dl/extractor/extractors.py`](https://github.com/ytdl-org/youtube-dl/blob/master/youtube_dl/extractor/extractors.py).
|
||||||
6. 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, then rename ``_TEST`` to ``_TESTS`` and make it into a list of dictionaries. 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.
|
6. 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 (actually, test case) then rename ``_TEST`` to ``_TESTS`` and make it into a list of dictionaries. The tests will then be named `TestDownload.test_YourExtractor`, `TestDownload.test_YourExtractor_1`, `TestDownload.test_YourExtractor_2`, etc. Note:
|
||||||
7. Have a look at [`youtube_dl/extractor/common.py`](https://github.com/ytdl-org/youtube-dl/blob/master/youtube_dl/extractor/common.py) for possible helper methods and a [detailed description of what your extractor should and may return](https://github.com/ytdl-org/youtube-dl/blob/7f41a598b3fba1bcab2817de64a08941200aa3c8/youtube_dl/extractor/common.py#L94-L303). Add tests and code for as many as you want.
|
* the test names use the extractor class name **without the trailing `IE`**
|
||||||
8. Make sure your code follows [youtube-dl coding conventions](#youtube-dl-coding-conventions) and check the code with [flake8](http://flake8.pycqa.org/en/latest/index.html#quickstart):
|
* tests with `only_matching` key in test's dict are not counted.
|
||||||
|
8. Have a look at [`youtube_dl/extractor/common.py`](https://github.com/ytdl-org/youtube-dl/blob/master/youtube_dl/extractor/common.py) for possible helper methods and a [detailed description of what your extractor should and may return](https://github.com/ytdl-org/youtube-dl/blob/7f41a598b3fba1bcab2817de64a08941200aa3c8/youtube_dl/extractor/common.py#L94-L303). Add tests and code for as many as you want.
|
||||||
|
9. Make sure your code follows [youtube-dl coding conventions](#youtube-dl-coding-conventions) and check the code with [flake8](https://flake8.pycqa.org/en/latest/index.html#quickstart):
|
||||||
|
|
||||||
$ flake8 youtube_dl/extractor/yourextractor.py
|
$ flake8 youtube_dl/extractor/yourextractor.py
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
wget http://central.maven.org/maven2/org/python/jython-installer/2.7.1/jython-installer-2.7.1.jar
|
|
||||||
java -jar jython-installer-2.7.1.jar -s -d "$HOME/jython"
|
|
||||||
$HOME/jython/bin/jython -m pip install nose
|
|
||||||
@@ -61,7 +61,7 @@ def build_lazy_ie(ie, name):
|
|||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
# find the correct sorting and add the required base classes so that sublcasses
|
# find the correct sorting and add the required base classes so that subclasses
|
||||||
# can be correctly created
|
# can be correctly created
|
||||||
classes = _ALL_CLASSES[:-1]
|
classes = _ALL_CLASSES[:-1]
|
||||||
ordered_cls = []
|
ordered_cls = []
|
||||||
|
|||||||
17
devscripts/run_tests.bat
Normal file
17
devscripts/run_tests.bat
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
@echo off
|
||||||
|
|
||||||
|
rem Keep this list in sync with the `offlinetest` target in Makefile
|
||||||
|
set DOWNLOAD_TESTS="age_restriction^|download^|iqiyi_sdk_interpreter^|socks^|subtitles^|write_annotations^|youtube_lists^|youtube_signature"
|
||||||
|
|
||||||
|
if "%YTDL_TEST_SET%" == "core" (
|
||||||
|
set test_set="-I test_("%DOWNLOAD_TESTS%")\.py"
|
||||||
|
set multiprocess_args=""
|
||||||
|
) else if "%YTDL_TEST_SET%" == "download" (
|
||||||
|
set test_set="-I test_(?!"%DOWNLOAD_TESTS%").+\.py"
|
||||||
|
set multiprocess_args="--processes=4 --process-timeout=540"
|
||||||
|
) else (
|
||||||
|
echo YTDL_TEST_SET is not set or invalid
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
nosetests test --verbose %test_set:"=% %multiprocess_args:"=%
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
# Supported sites
|
# Supported sites
|
||||||
- **1tv**: Первый канал
|
- **1tv**: Первый канал
|
||||||
- **1up.com**
|
|
||||||
- **20min**
|
- **20min**
|
||||||
- **220.ro**
|
- **220.ro**
|
||||||
- **23video**
|
- **23video**
|
||||||
|
- **247sports**
|
||||||
- **24video**
|
- **24video**
|
||||||
- **3qsdn**: 3Q SDN
|
- **3qsdn**: 3Q SDN
|
||||||
- **3sat**
|
- **3sat**
|
||||||
@@ -35,32 +35,39 @@
|
|||||||
- **adobetv:video**
|
- **adobetv:video**
|
||||||
- **AdultSwim**
|
- **AdultSwim**
|
||||||
- **aenetworks**: A+E Networks: A&E, Lifetime, History.com, FYI Network and History Vault
|
- **aenetworks**: A+E Networks: A&E, Lifetime, History.com, FYI Network and History Vault
|
||||||
|
- **aenetworks:collection**
|
||||||
|
- **aenetworks:show**
|
||||||
- **afreecatv**: afreecatv.com
|
- **afreecatv**: afreecatv.com
|
||||||
- **AirMozilla**
|
- **AirMozilla**
|
||||||
- **AliExpressLive**
|
- **AliExpressLive**
|
||||||
- **AlJazeera**
|
- **AlJazeera**
|
||||||
- **Allocine**
|
- **Allocine**
|
||||||
- **AlphaPorno**
|
- **AlphaPorno**
|
||||||
|
- **Amara**
|
||||||
- **AMCNetworks**
|
- **AMCNetworks**
|
||||||
- **AmericasTestKitchen**
|
- **AmericasTestKitchen**
|
||||||
|
- **AmericasTestKitchenSeason**
|
||||||
- **anderetijden**: npo.nl, ntr.nl, omroepwnl.nl, zapp.nl and npo3.nl
|
- **anderetijden**: npo.nl, ntr.nl, omroepwnl.nl, zapp.nl and npo3.nl
|
||||||
- **AnimeOnDemand**
|
- **AnimeOnDemand**
|
||||||
- **Anvato**
|
- **Anvato**
|
||||||
- **aol.com**
|
- **aol.com**: Yahoo screen and movies
|
||||||
- **APA**
|
- **APA**
|
||||||
- **Aparat**
|
- **Aparat**
|
||||||
- **AppleConnect**
|
- **AppleConnect**
|
||||||
- **AppleDaily**: 臺灣蘋果日報
|
- **AppleDaily**: 臺灣蘋果日報
|
||||||
|
- **ApplePodcasts**
|
||||||
- **appletrailers**
|
- **appletrailers**
|
||||||
- **appletrailers:section**
|
- **appletrailers:section**
|
||||||
- **archive.org**: archive.org videos
|
- **archive.org**: archive.org videos
|
||||||
|
- **ArcPublishing**
|
||||||
- **ARD**
|
- **ARD**
|
||||||
- **ARD:mediathek**
|
- **ARD:mediathek**
|
||||||
- **ARDBetaMediathek**
|
- **ARDBetaMediathek**
|
||||||
- **Arkena**
|
- **Arkena**
|
||||||
- **arte.tv:+7**
|
- **arte.sky.it**
|
||||||
- **arte.tv:embed**
|
- **ArteTV**
|
||||||
- **arte.tv:playlist**
|
- **ArteTVEmbed**
|
||||||
|
- **ArteTVPlaylist**
|
||||||
- **AsianCrush**
|
- **AsianCrush**
|
||||||
- **AsianCrushPlaylist**
|
- **AsianCrushPlaylist**
|
||||||
- **AtresPlayer**
|
- **AtresPlayer**
|
||||||
@@ -76,6 +83,7 @@
|
|||||||
- **awaan:video**
|
- **awaan:video**
|
||||||
- **AZMedien**: AZ Medien videos
|
- **AZMedien**: AZ Medien videos
|
||||||
- **BaiduVideo**: 百度视频
|
- **BaiduVideo**: 百度视频
|
||||||
|
- **bandaichannel**
|
||||||
- **Bandcamp**
|
- **Bandcamp**
|
||||||
- **Bandcamp:album**
|
- **Bandcamp:album**
|
||||||
- **Bandcamp:weekly**
|
- **Bandcamp:weekly**
|
||||||
@@ -83,7 +91,8 @@
|
|||||||
- **bbc**: BBC
|
- **bbc**: BBC
|
||||||
- **bbc.co.uk**: BBC iPlayer
|
- **bbc.co.uk**: BBC iPlayer
|
||||||
- **bbc.co.uk:article**: BBC articles
|
- **bbc.co.uk:article**: BBC articles
|
||||||
- **bbc.co.uk:iplayer:playlist**
|
- **bbc.co.uk:iplayer:episodes**
|
||||||
|
- **bbc.co.uk:iplayer:group**
|
||||||
- **bbc.co.uk:playlist**
|
- **bbc.co.uk:playlist**
|
||||||
- **BBVTV**
|
- **BBVTV**
|
||||||
- **Beatport**
|
- **Beatport**
|
||||||
@@ -93,21 +102,28 @@
|
|||||||
- **BellMedia**
|
- **BellMedia**
|
||||||
- **Bet**
|
- **Bet**
|
||||||
- **bfi:player**
|
- **bfi:player**
|
||||||
|
- **bfmtv**
|
||||||
|
- **bfmtv:article**
|
||||||
|
- **bfmtv:live**
|
||||||
|
- **BibelTV**
|
||||||
- **Bigflix**
|
- **Bigflix**
|
||||||
- **Bild**: Bild.de
|
- **Bild**: Bild.de
|
||||||
- **BiliBili**
|
- **BiliBili**
|
||||||
- **BilibiliAudio**
|
- **BilibiliAudio**
|
||||||
- **BilibiliAudioAlbum**
|
- **BilibiliAudioAlbum**
|
||||||
|
- **BiliBiliPlayer**
|
||||||
- **BioBioChileTV**
|
- **BioBioChileTV**
|
||||||
|
- **Biography**
|
||||||
- **BIQLE**
|
- **BIQLE**
|
||||||
- **BitChute**
|
- **BitChute**
|
||||||
- **BitChuteChannel**
|
- **BitChuteChannel**
|
||||||
- **BleacherReport**
|
- **BleacherReport**
|
||||||
- **BleacherReportCMS**
|
- **BleacherReportCMS**
|
||||||
- **blinkx**
|
|
||||||
- **Bloomberg**
|
- **Bloomberg**
|
||||||
- **BokeCC**
|
- **BokeCC**
|
||||||
|
- **BongaCams**
|
||||||
- **BostonGlobe**
|
- **BostonGlobe**
|
||||||
|
- **Box**
|
||||||
- **Bpb**: Bundeszentrale für politische Bildung
|
- **Bpb**: Bundeszentrale für politische Bildung
|
||||||
- **BR**: Bayerischer Rundfunk
|
- **BR**: Bayerischer Rundfunk
|
||||||
- **BravoTV**
|
- **BravoTV**
|
||||||
@@ -140,10 +156,12 @@
|
|||||||
- **CBS**
|
- **CBS**
|
||||||
- **CBSInteractive**
|
- **CBSInteractive**
|
||||||
- **CBSLocal**
|
- **CBSLocal**
|
||||||
|
- **CBSLocalArticle**
|
||||||
- **cbsnews**: CBS News
|
- **cbsnews**: CBS News
|
||||||
- **cbsnews:embed**
|
- **cbsnews:embed**
|
||||||
- **cbsnews:livevideo**: CBS News Live Videos
|
- **cbsnews:livevideo**: CBS News Live Videos
|
||||||
- **CBSSports**
|
- **cbssports**
|
||||||
|
- **cbssports:embed**
|
||||||
- **CCMA**
|
- **CCMA**
|
||||||
- **CCTV**: 央视网
|
- **CCTV**: 央视网
|
||||||
- **CDA**
|
- **CDA**
|
||||||
@@ -155,6 +173,7 @@
|
|||||||
- **Chilloutzone**
|
- **Chilloutzone**
|
||||||
- **chirbit**
|
- **chirbit**
|
||||||
- **chirbit:profile**
|
- **chirbit:profile**
|
||||||
|
- **cielotv.it**
|
||||||
- **Cinchcast**
|
- **Cinchcast**
|
||||||
- **Cinemax**
|
- **Cinemax**
|
||||||
- **CiscoLiveSearch**
|
- **CiscoLiveSearch**
|
||||||
@@ -176,8 +195,6 @@
|
|||||||
- **CNNArticle**
|
- **CNNArticle**
|
||||||
- **CNNBlogs**
|
- **CNNBlogs**
|
||||||
- **ComedyCentral**
|
- **ComedyCentral**
|
||||||
- **ComedyCentralFullEpisodes**
|
|
||||||
- **ComedyCentralShortname**
|
|
||||||
- **ComedyCentralTV**
|
- **ComedyCentralTV**
|
||||||
- **CondeNast**: Condé Nast media group: Allure, Architectural Digest, Ars Technica, Bon Appétit, Brides, Condé Nast, Condé Nast Traveler, Details, Epicurious, GQ, Glamour, Golf Digest, SELF, Teen Vogue, The New Yorker, Vanity Fair, Vogue, W Magazine, WIRED
|
- **CondeNast**: Condé Nast media group: Allure, Architectural Digest, Ars Technica, Bon Appétit, Brides, Condé Nast, Condé Nast Traveler, Details, Epicurious, GQ, Glamour, Golf Digest, SELF, Teen Vogue, The New Yorker, Vanity Fair, Vogue, W Magazine, WIRED
|
||||||
- **CONtv**
|
- **CONtv**
|
||||||
@@ -188,9 +205,9 @@
|
|||||||
- **CrooksAndLiars**
|
- **CrooksAndLiars**
|
||||||
- **crunchyroll**
|
- **crunchyroll**
|
||||||
- **crunchyroll:playlist**
|
- **crunchyroll:playlist**
|
||||||
- **CSNNE**
|
|
||||||
- **CSpan**: C-SPAN
|
- **CSpan**: C-SPAN
|
||||||
- **CtsNews**: 華視新聞
|
- **CtsNews**: 華視新聞
|
||||||
|
- **CTV**
|
||||||
- **CTVNews**
|
- **CTVNews**
|
||||||
- **cu.ntv.co.jp**: Nippon Television Network
|
- **cu.ntv.co.jp**: Nippon Television Network
|
||||||
- **Culturebox**
|
- **Culturebox**
|
||||||
@@ -198,6 +215,7 @@
|
|||||||
- **curiositystream**
|
- **curiositystream**
|
||||||
- **curiositystream:collection**
|
- **curiositystream:collection**
|
||||||
- **CWTV**
|
- **CWTV**
|
||||||
|
- **DagelijkseKost**: dagelijksekost.een.be
|
||||||
- **DailyMail**
|
- **DailyMail**
|
||||||
- **dailymotion**
|
- **dailymotion**
|
||||||
- **dailymotion:playlist**
|
- **dailymotion:playlist**
|
||||||
@@ -219,6 +237,7 @@
|
|||||||
- **DiscoveryGo**
|
- **DiscoveryGo**
|
||||||
- **DiscoveryGoPlaylist**
|
- **DiscoveryGoPlaylist**
|
||||||
- **DiscoveryNetworksDe**
|
- **DiscoveryNetworksDe**
|
||||||
|
- **DiscoveryPlus**
|
||||||
- **DiscoveryVR**
|
- **DiscoveryVR**
|
||||||
- **Disney**
|
- **Disney**
|
||||||
- **dlive:stream**
|
- **dlive:stream**
|
||||||
@@ -261,7 +280,6 @@
|
|||||||
- **ESPNArticle**
|
- **ESPNArticle**
|
||||||
- **EsriVideo**
|
- **EsriVideo**
|
||||||
- **Europa**
|
- **Europa**
|
||||||
- **EveryonesMixtape**
|
|
||||||
- **EWETV**
|
- **EWETV**
|
||||||
- **ExpoTV**
|
- **ExpoTV**
|
||||||
- **Expressen**
|
- **Expressen**
|
||||||
@@ -303,11 +321,11 @@
|
|||||||
- **FrontendMasters**
|
- **FrontendMasters**
|
||||||
- **FrontendMastersCourse**
|
- **FrontendMastersCourse**
|
||||||
- **FrontendMastersLesson**
|
- **FrontendMastersLesson**
|
||||||
|
- **FujiTVFODPlus7**
|
||||||
- **Funimation**
|
- **Funimation**
|
||||||
- **Funk**
|
- **Funk**
|
||||||
- **Fusion**
|
- **Fusion**
|
||||||
- **Fux**
|
- **Fux**
|
||||||
- **FXNetworks**
|
|
||||||
- **Gaia**
|
- **Gaia**
|
||||||
- **GameInformer**
|
- **GameInformer**
|
||||||
- **GameSpot**
|
- **GameSpot**
|
||||||
@@ -315,6 +333,7 @@
|
|||||||
- **Gaskrank**
|
- **Gaskrank**
|
||||||
- **Gazeta**
|
- **Gazeta**
|
||||||
- **GDCVault**
|
- **GDCVault**
|
||||||
|
- **GediDigital**
|
||||||
- **generic**: Generic downloader that works on some sites
|
- **generic**: Generic downloader that works on some sites
|
||||||
- **Gfycat**
|
- **Gfycat**
|
||||||
- **GiantBomb**
|
- **GiantBomb**
|
||||||
@@ -326,6 +345,8 @@
|
|||||||
- **Go**
|
- **Go**
|
||||||
- **GodTube**
|
- **GodTube**
|
||||||
- **Golem**
|
- **Golem**
|
||||||
|
- **google:podcasts**
|
||||||
|
- **google:podcasts:feed**
|
||||||
- **GoogleDrive**
|
- **GoogleDrive**
|
||||||
- **Goshgay**
|
- **Goshgay**
|
||||||
- **GPUTechConf**
|
- **GPUTechConf**
|
||||||
@@ -338,8 +359,10 @@
|
|||||||
- **HentaiStigma**
|
- **HentaiStigma**
|
||||||
- **hetklokhuis**
|
- **hetklokhuis**
|
||||||
- **hgtv.com:show**
|
- **hgtv.com:show**
|
||||||
|
- **HGTVDe**
|
||||||
- **HiDive**
|
- **HiDive**
|
||||||
- **HistoricFilms**
|
- **HistoricFilms**
|
||||||
|
- **history:player**
|
||||||
- **history:topic**: History.com Topic
|
- **history:topic**: History.com Topic
|
||||||
- **hitbox**
|
- **hitbox**
|
||||||
- **hitbox:live**
|
- **hitbox:live**
|
||||||
@@ -359,6 +382,10 @@
|
|||||||
- **HungamaSong**
|
- **HungamaSong**
|
||||||
- **Hypem**
|
- **Hypem**
|
||||||
- **ign.com**
|
- **ign.com**
|
||||||
|
- **IGNArticle**
|
||||||
|
- **IGNVideo**
|
||||||
|
- **IHeartRadio**
|
||||||
|
- **iheartradio:podcast**
|
||||||
- **imdb**: Internet Movie Database trailers
|
- **imdb**: Internet Movie Database trailers
|
||||||
- **imdb:list**: Internet Movie Database lists
|
- **imdb:list**: Internet Movie Database lists
|
||||||
- **Imgur**
|
- **Imgur**
|
||||||
@@ -392,14 +419,14 @@
|
|||||||
- **JWPlatform**
|
- **JWPlatform**
|
||||||
- **Kakao**
|
- **Kakao**
|
||||||
- **Kaltura**
|
- **Kaltura**
|
||||||
- **KanalPlay**: Kanal 5/9/11 Play
|
|
||||||
- **Kankan**
|
- **Kankan**
|
||||||
- **Karaoketv**
|
- **Karaoketv**
|
||||||
- **KarriereVideos**
|
- **KarriereVideos**
|
||||||
- **Katsomo**
|
- **Katsomo**
|
||||||
- **KeezMovies**
|
- **KeezMovies**
|
||||||
- **Ketnet**
|
- **Ketnet**
|
||||||
- **KhanAcademy**
|
- **khanacademy**
|
||||||
|
- **khanacademy:unit**
|
||||||
- **KickStarter**
|
- **KickStarter**
|
||||||
- **KinjaEmbed**
|
- **KinjaEmbed**
|
||||||
- **KinoPoisk**
|
- **KinoPoisk**
|
||||||
@@ -416,6 +443,8 @@
|
|||||||
- **la7.it**
|
- **la7.it**
|
||||||
- **laola1tv**
|
- **laola1tv**
|
||||||
- **laola1tv:embed**
|
- **laola1tv:embed**
|
||||||
|
- **lbry**
|
||||||
|
- **lbry:channel**
|
||||||
- **LCI**
|
- **LCI**
|
||||||
- **Lcp**
|
- **Lcp**
|
||||||
- **LcpPlay**
|
- **LcpPlay**
|
||||||
@@ -435,14 +464,14 @@
|
|||||||
- **limelight**
|
- **limelight**
|
||||||
- **limelight:channel**
|
- **limelight:channel**
|
||||||
- **limelight:channel_list**
|
- **limelight:channel_list**
|
||||||
|
- **LineLive**
|
||||||
|
- **LineLiveChannel**
|
||||||
- **LineTV**
|
- **LineTV**
|
||||||
- **linkedin:learning**
|
- **linkedin:learning**
|
||||||
- **linkedin:learning:course**
|
- **linkedin:learning:course**
|
||||||
- **LinuxAcademy**
|
- **LinuxAcademy**
|
||||||
- **LiTV**
|
- **LiTV**
|
||||||
- **LiveJournal**
|
- **LiveJournal**
|
||||||
- **LiveLeak**
|
|
||||||
- **LiveLeakEmbed**
|
|
||||||
- **livestream**
|
- **livestream**
|
||||||
- **livestream:original**
|
- **livestream:original**
|
||||||
- **LnkGo**
|
- **LnkGo**
|
||||||
@@ -460,11 +489,13 @@
|
|||||||
- **mangomolo:live**
|
- **mangomolo:live**
|
||||||
- **mangomolo:video**
|
- **mangomolo:video**
|
||||||
- **ManyVids**
|
- **ManyVids**
|
||||||
|
- **MaoriTV**
|
||||||
- **Markiza**
|
- **Markiza**
|
||||||
- **MarkizaPage**
|
- **MarkizaPage**
|
||||||
- **massengeschmack.tv**
|
- **massengeschmack.tv**
|
||||||
- **MatchTV**
|
- **MatchTV**
|
||||||
- **MDR**: MDR.DE and KiKA
|
- **MDR**: MDR.DE and KiKA
|
||||||
|
- **MedalTV**
|
||||||
- **media.ccc.de**
|
- **media.ccc.de**
|
||||||
- **media.ccc.de:lists**
|
- **media.ccc.de:lists**
|
||||||
- **Medialaan**
|
- **Medialaan**
|
||||||
@@ -479,9 +510,13 @@
|
|||||||
- **META**
|
- **META**
|
||||||
- **metacafe**
|
- **metacafe**
|
||||||
- **Metacritic**
|
- **Metacritic**
|
||||||
|
- **mewatch**
|
||||||
- **Mgoon**
|
- **Mgoon**
|
||||||
- **MGTV**: 芒果TV
|
- **MGTV**: 芒果TV
|
||||||
- **MiaoPai**
|
- **MiaoPai**
|
||||||
|
- **minds**
|
||||||
|
- **minds:channel**
|
||||||
|
- **minds:group**
|
||||||
- **MinistryGrid**
|
- **MinistryGrid**
|
||||||
- **Minoto**
|
- **Minoto**
|
||||||
- **miomio.tv**
|
- **miomio.tv**
|
||||||
@@ -489,13 +524,13 @@
|
|||||||
- **mixcloud**
|
- **mixcloud**
|
||||||
- **mixcloud:playlist**
|
- **mixcloud:playlist**
|
||||||
- **mixcloud:user**
|
- **mixcloud:user**
|
||||||
- **Mixer:live**
|
|
||||||
- **Mixer:vod**
|
|
||||||
- **MLB**
|
- **MLB**
|
||||||
|
- **MLBVideo**
|
||||||
- **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
|
||||||
- **Mofosex**
|
- **Mofosex**
|
||||||
|
- **MofosexEmbed**
|
||||||
- **Mojvideo**
|
- **Mojvideo**
|
||||||
- **Morningstar**: morningstar.com
|
- **Morningstar**: morningstar.com
|
||||||
- **Motherless**
|
- **Motherless**
|
||||||
@@ -512,6 +547,7 @@
|
|||||||
- **mtv:video**
|
- **mtv:video**
|
||||||
- **mtvjapan**
|
- **mtvjapan**
|
||||||
- **mtvservices:embedded**
|
- **mtvservices:embedded**
|
||||||
|
- **MTVUutisetArticle**
|
||||||
- **MuenchenTV**: münchen.tv
|
- **MuenchenTV**: münchen.tv
|
||||||
- **mva**: Microsoft Virtual Academy videos
|
- **mva**: Microsoft Virtual Academy videos
|
||||||
- **mva:course**: Microsoft Virtual Academy courses
|
- **mva:course**: Microsoft Virtual Academy courses
|
||||||
@@ -530,6 +566,11 @@
|
|||||||
- **NationalGeographicTV**
|
- **NationalGeographicTV**
|
||||||
- **Naver**
|
- **Naver**
|
||||||
- **NBA**
|
- **NBA**
|
||||||
|
- **nba:watch**
|
||||||
|
- **nba:watch:collection**
|
||||||
|
- **NBAChannel**
|
||||||
|
- **NBAEmbed**
|
||||||
|
- **NBAWatchEmbed**
|
||||||
- **NBC**
|
- **NBC**
|
||||||
- **NBCNews**
|
- **NBCNews**
|
||||||
- **nbcolympics**
|
- **nbcolympics**
|
||||||
@@ -559,8 +600,10 @@
|
|||||||
- **NextTV**: 壹電視
|
- **NextTV**: 壹電視
|
||||||
- **Nexx**
|
- **Nexx**
|
||||||
- **NexxEmbed**
|
- **NexxEmbed**
|
||||||
- **nfl.com**
|
- **nfl.com** (Currently broken)
|
||||||
|
- **nfl.com:article** (Currently broken)
|
||||||
- **NhkVod**
|
- **NhkVod**
|
||||||
|
- **NhkVodProgram**
|
||||||
- **nhl.com**
|
- **nhl.com**
|
||||||
- **nick.com**
|
- **nick.com**
|
||||||
- **nick.de**
|
- **nick.de**
|
||||||
@@ -574,7 +617,6 @@
|
|||||||
- **njoy:embed**
|
- **njoy:embed**
|
||||||
- **NJPWWorld**: 新日本プロレスワールド
|
- **NJPWWorld**: 新日本プロレスワールド
|
||||||
- **NobelPrize**
|
- **NobelPrize**
|
||||||
- **Noco**
|
|
||||||
- **NonkTube**
|
- **NonkTube**
|
||||||
- **Noovo**
|
- **Noovo**
|
||||||
- **Normalboots**
|
- **Normalboots**
|
||||||
@@ -592,6 +634,7 @@
|
|||||||
- **Npr**
|
- **Npr**
|
||||||
- **NRK**
|
- **NRK**
|
||||||
- **NRKPlaylist**
|
- **NRKPlaylist**
|
||||||
|
- **NRKRadioPodkast**
|
||||||
- **NRKSkole**: NRK Skole
|
- **NRKSkole**: NRK Skole
|
||||||
- **NRKTV**: NRK TV and NRK Radio
|
- **NRKTV**: NRK TV and NRK Radio
|
||||||
- **NRKTVDirekte**: NRK TV Direkte and NRK Radio Direkte
|
- **NRKTVDirekte**: NRK TV Direkte and NRK Radio Direkte
|
||||||
@@ -604,6 +647,7 @@
|
|||||||
- **Nuvid**
|
- **Nuvid**
|
||||||
- **NYTimes**
|
- **NYTimes**
|
||||||
- **NYTimesArticle**
|
- **NYTimesArticle**
|
||||||
|
- **NYTimesCooking**
|
||||||
- **NZZ**
|
- **NZZ**
|
||||||
- **ocw.mit.edu**
|
- **ocw.mit.edu**
|
||||||
- **OdaTV**
|
- **OdaTV**
|
||||||
@@ -618,21 +662,33 @@
|
|||||||
- **Ooyala**
|
- **Ooyala**
|
||||||
- **OoyalaExternal**
|
- **OoyalaExternal**
|
||||||
- **OraTV**
|
- **OraTV**
|
||||||
|
- **orf:burgenland**: Radio Burgenland
|
||||||
- **orf:fm4**: radio FM4
|
- **orf:fm4**: radio FM4
|
||||||
- **orf:fm4:story**: fm4.orf.at stories
|
- **orf:fm4:story**: fm4.orf.at stories
|
||||||
- **orf:iptv**: iptv.ORF.at
|
- **orf:iptv**: iptv.ORF.at
|
||||||
|
- **orf:kaernten**: Radio Kärnten
|
||||||
|
- **orf:noe**: Radio Niederösterreich
|
||||||
|
- **orf:oberoesterreich**: Radio Oberösterreich
|
||||||
- **orf:oe1**: Radio Österreich 1
|
- **orf:oe1**: Radio Österreich 1
|
||||||
|
- **orf:oe3**: Radio Österreich 3
|
||||||
|
- **orf:salzburg**: Radio Salzburg
|
||||||
|
- **orf:steiermark**: Radio Steiermark
|
||||||
|
- **orf:tirol**: Radio Tirol
|
||||||
- **orf:tvthek**: ORF TVthek
|
- **orf:tvthek**: ORF TVthek
|
||||||
|
- **orf:vorarlberg**: Radio Vorarlberg
|
||||||
|
- **orf:wien**: Radio Wien
|
||||||
- **OsnatelTV**
|
- **OsnatelTV**
|
||||||
- **OutsideTV**
|
- **OutsideTV**
|
||||||
- **PacktPub**
|
- **PacktPub**
|
||||||
- **PacktPubCourse**
|
- **PacktPubCourse**
|
||||||
|
- **PalcoMP3:artist**
|
||||||
|
- **PalcoMP3:song**
|
||||||
|
- **PalcoMP3:video**
|
||||||
- **pandora.tv**: 판도라TV
|
- **pandora.tv**: 판도라TV
|
||||||
- **ParamountNetwork**
|
- **ParamountNetwork**
|
||||||
- **parliamentlive.tv**: UK parliament videos
|
- **parliamentlive.tv**: UK parliament videos
|
||||||
- **Patreon**
|
- **Patreon**
|
||||||
- **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)
|
||||||
- **pcmag**
|
|
||||||
- **PearVideo**
|
- **PearVideo**
|
||||||
- **PeerTube**
|
- **PeerTube**
|
||||||
- **People**
|
- **People**
|
||||||
@@ -646,18 +702,21 @@
|
|||||||
- **PicartoVod**
|
- **PicartoVod**
|
||||||
- **Piksel**
|
- **Piksel**
|
||||||
- **Pinkbike**
|
- **Pinkbike**
|
||||||
|
- **Pinterest**
|
||||||
|
- **PinterestCollection**
|
||||||
- **Pladform**
|
- **Pladform**
|
||||||
- **Platzi**
|
- **Platzi**
|
||||||
- **PlatziCourse**
|
- **PlatziCourse**
|
||||||
- **play.fm**
|
- **play.fm**
|
||||||
|
- **player.sky.it**
|
||||||
- **PlayPlusTV**
|
- **PlayPlusTV**
|
||||||
|
- **PlayStuff**
|
||||||
- **PlaysTV**
|
- **PlaysTV**
|
||||||
- **Playtvak**: Playtvak.cz, iDNES.cz and Lidovky.cz
|
- **Playtvak**: Playtvak.cz, iDNES.cz and Lidovky.cz
|
||||||
- **Playvid**
|
- **Playvid**
|
||||||
- **Playwire**
|
- **Playwire**
|
||||||
- **pluralsight**
|
- **pluralsight**
|
||||||
- **pluralsight:course**
|
- **pluralsight:course**
|
||||||
- **plus.google**: Google Plus
|
|
||||||
- **podomatic**
|
- **podomatic**
|
||||||
- **Pokemon**
|
- **Pokemon**
|
||||||
- **PolskieRadio**
|
- **PolskieRadio**
|
||||||
@@ -687,6 +746,7 @@
|
|||||||
- **qqmusic:singer**: QQ音乐 - 歌手
|
- **qqmusic:singer**: QQ音乐 - 歌手
|
||||||
- **qqmusic:toplist**: QQ音乐 - 排行榜
|
- **qqmusic:toplist**: QQ音乐 - 排行榜
|
||||||
- **QuantumTV**
|
- **QuantumTV**
|
||||||
|
- **Qub**
|
||||||
- **Quickline**
|
- **Quickline**
|
||||||
- **QuicklineLive**
|
- **QuicklineLive**
|
||||||
- **R7**
|
- **R7**
|
||||||
@@ -705,6 +765,8 @@
|
|||||||
- **RayWenderlichCourse**
|
- **RayWenderlichCourse**
|
||||||
- **RBMARadio**
|
- **RBMARadio**
|
||||||
- **RDS**: RDS.ca
|
- **RDS**: RDS.ca
|
||||||
|
- **RedBull**
|
||||||
|
- **RedBullEmbed**
|
||||||
- **RedBullTV**
|
- **RedBullTV**
|
||||||
- **RedBullTVRrnContent**
|
- **RedBullTVRrnContent**
|
||||||
- **Reddit**
|
- **Reddit**
|
||||||
@@ -739,6 +801,7 @@
|
|||||||
- **RTVNH**
|
- **RTVNH**
|
||||||
- **RTVS**
|
- **RTVS**
|
||||||
- **RUHD**
|
- **RUHD**
|
||||||
|
- **RumbleEmbed**
|
||||||
- **rutube**: Rutube videos
|
- **rutube**: Rutube videos
|
||||||
- **rutube:channel**: Rutube channels
|
- **rutube:channel**: Rutube channels
|
||||||
- **rutube:embed**: Rutube embedded videos
|
- **rutube:embed**: Rutube embedded videos
|
||||||
@@ -753,6 +816,7 @@
|
|||||||
- **safari:course**: safaribooksonline.com online courses
|
- **safari:course**: safaribooksonline.com online courses
|
||||||
- **SAKTV**
|
- **SAKTV**
|
||||||
- **SaltTV**
|
- **SaltTV**
|
||||||
|
- **SampleFocus**
|
||||||
- **Sapo**: SAPO Vídeos
|
- **Sapo**: SAPO Vídeos
|
||||||
- **savefrom.net**
|
- **savefrom.net**
|
||||||
- **SBS**: sbs.com.au
|
- **SBS**: sbs.com.au
|
||||||
@@ -775,19 +839,21 @@
|
|||||||
- **ShahidShow**
|
- **ShahidShow**
|
||||||
- **Shared**: shared.sx
|
- **Shared**: shared.sx
|
||||||
- **ShowRoomLive**
|
- **ShowRoomLive**
|
||||||
|
- **simplecast**
|
||||||
|
- **simplecast:episode**
|
||||||
|
- **simplecast:podcast**
|
||||||
- **Sina**
|
- **Sina**
|
||||||
|
- **sky.it**
|
||||||
|
- **sky:news**
|
||||||
|
- **sky:sports**
|
||||||
|
- **sky:sports:news**
|
||||||
|
- **skyacademy.it**
|
||||||
- **SkylineWebcams**
|
- **SkylineWebcams**
|
||||||
- **SkyNews**
|
|
||||||
- **skynewsarabia:article**
|
- **skynewsarabia:article**
|
||||||
- **skynewsarabia:video**
|
- **skynewsarabia:video**
|
||||||
- **SkySports**
|
|
||||||
- **Slideshare**
|
- **Slideshare**
|
||||||
- **SlidesLive**
|
- **SlidesLive**
|
||||||
- **Slutload**
|
- **Slutload**
|
||||||
- **smotri**: Smotri.com
|
|
||||||
- **smotri:broadcast**: Smotri.com broadcasts
|
|
||||||
- **smotri:community**: Smotri.com community videos
|
|
||||||
- **smotri:user**: Smotri.com user videos
|
|
||||||
- **Snotr**
|
- **Snotr**
|
||||||
- **Sohu**
|
- **Sohu**
|
||||||
- **SonyLIV**
|
- **SonyLIV**
|
||||||
@@ -809,12 +875,16 @@
|
|||||||
- **SpankBangPlaylist**
|
- **SpankBangPlaylist**
|
||||||
- **Spankwire**
|
- **Spankwire**
|
||||||
- **Spiegel**
|
- **Spiegel**
|
||||||
- **Spiegel:Article**: Articles on spiegel.de
|
|
||||||
- **Spiegeltv**
|
|
||||||
- **sport.francetvinfo.fr**
|
- **sport.francetvinfo.fr**
|
||||||
- **Sport5**
|
- **Sport5**
|
||||||
- **SportBox**
|
- **SportBox**
|
||||||
- **SportDeutschland**
|
- **SportDeutschland**
|
||||||
|
- **spotify**
|
||||||
|
- **spotify:show**
|
||||||
|
- **Spreaker**
|
||||||
|
- **SpreakerPage**
|
||||||
|
- **SpreakerShow**
|
||||||
|
- **SpreakerShowPage**
|
||||||
- **SpringboardPlatform**
|
- **SpringboardPlatform**
|
||||||
- **Sprout**
|
- **Sprout**
|
||||||
- **sr:mediathek**: Saarländischer Rundfunk
|
- **sr:mediathek**: Saarländischer Rundfunk
|
||||||
@@ -823,6 +893,10 @@
|
|||||||
- **stanfordoc**: Stanford Open ClassRoom
|
- **stanfordoc**: Stanford Open ClassRoom
|
||||||
- **Steam**
|
- **Steam**
|
||||||
- **Stitcher**
|
- **Stitcher**
|
||||||
|
- **StitcherShow**
|
||||||
|
- **StoryFire**
|
||||||
|
- **StoryFireSeries**
|
||||||
|
- **StoryFireUser**
|
||||||
- **Streamable**
|
- **Streamable**
|
||||||
- **streamcloud.eu**
|
- **streamcloud.eu**
|
||||||
- **StreamCZ**
|
- **StreamCZ**
|
||||||
@@ -843,7 +917,6 @@
|
|||||||
- **Tagesschau**
|
- **Tagesschau**
|
||||||
- **tagesschau:player**
|
- **tagesschau:player**
|
||||||
- **Tass**
|
- **Tass**
|
||||||
- **TastyTrade**
|
|
||||||
- **TBS**
|
- **TBS**
|
||||||
- **TDSLifeway**
|
- **TDSLifeway**
|
||||||
- **Teachable**
|
- **Teachable**
|
||||||
@@ -866,6 +939,7 @@
|
|||||||
- **TeleQuebecEmission**
|
- **TeleQuebecEmission**
|
||||||
- **TeleQuebecLive**
|
- **TeleQuebecLive**
|
||||||
- **TeleQuebecSquat**
|
- **TeleQuebecSquat**
|
||||||
|
- **TeleQuebecVideo**
|
||||||
- **TeleTask**
|
- **TeleTask**
|
||||||
- **Telewebion**
|
- **Telewebion**
|
||||||
- **TennisTV**
|
- **TennisTV**
|
||||||
@@ -883,7 +957,7 @@
|
|||||||
- **ThisAV**
|
- **ThisAV**
|
||||||
- **ThisOldHouse**
|
- **ThisOldHouse**
|
||||||
- **TikTok**
|
- **TikTok**
|
||||||
- **TikTokUser**
|
- **TikTokUser** (Currently broken)
|
||||||
- **tinypic**: tinypic.com videos
|
- **tinypic**: tinypic.com videos
|
||||||
- **TMZ**
|
- **TMZ**
|
||||||
- **TMZArticle**
|
- **TMZArticle**
|
||||||
@@ -891,12 +965,13 @@
|
|||||||
- **TNAFlixNetworkEmbed**
|
- **TNAFlixNetworkEmbed**
|
||||||
- **toggle**
|
- **toggle**
|
||||||
- **ToonGoggles**
|
- **ToonGoggles**
|
||||||
- **Tosh**: Tosh.0
|
|
||||||
- **tou.tv**
|
- **tou.tv**
|
||||||
- **Toypics**: Toypics video
|
- **Toypics**: Toypics video
|
||||||
- **ToypicsUser**: Toypics user profile
|
- **ToypicsUser**: Toypics user profile
|
||||||
- **TrailerAddict** (Currently broken)
|
- **TrailerAddict** (Currently broken)
|
||||||
- **Trilulilu**
|
- **Trilulilu**
|
||||||
|
- **Trovo**
|
||||||
|
- **TrovoVod**
|
||||||
- **TruNews**
|
- **TruNews**
|
||||||
- **TruTV**
|
- **TruTV**
|
||||||
- **Tube8**
|
- **Tube8**
|
||||||
@@ -916,11 +991,15 @@
|
|||||||
- **TV2DKBornholmPlay**
|
- **TV2DKBornholmPlay**
|
||||||
- **TV4**: tv4.se and tv4play.se
|
- **TV4**: tv4.se and tv4play.se
|
||||||
- **TV5MondePlus**: TV5MONDE+
|
- **TV5MondePlus**: TV5MONDE+
|
||||||
|
- **tv5unis**
|
||||||
|
- **tv5unis:video**
|
||||||
|
- **tv8.it**
|
||||||
- **TVA**
|
- **TVA**
|
||||||
- **TVANouvelles**
|
- **TVANouvelles**
|
||||||
- **TVANouvellesArticle**
|
- **TVANouvellesArticle**
|
||||||
- **TVC**
|
- **TVC**
|
||||||
- **TVCArticle**
|
- **TVCArticle**
|
||||||
|
- **TVer**
|
||||||
- **tvigle**: Интернет-телевидение Tvigle.ru
|
- **tvigle**: Интернет-телевидение Tvigle.ru
|
||||||
- **tvland.com**
|
- **tvland.com**
|
||||||
- **TVN24**
|
- **TVN24**
|
||||||
@@ -938,16 +1017,13 @@
|
|||||||
- **TVPlayHome**
|
- **TVPlayHome**
|
||||||
- **Tweakers**
|
- **Tweakers**
|
||||||
- **TwitCasting**
|
- **TwitCasting**
|
||||||
- **twitch:chapter**
|
|
||||||
- **twitch:clips**
|
- **twitch:clips**
|
||||||
- **twitch:profile**
|
|
||||||
- **twitch:stream**
|
- **twitch:stream**
|
||||||
- **twitch:video**
|
|
||||||
- **twitch:videos:all**
|
|
||||||
- **twitch:videos:highlights**
|
|
||||||
- **twitch:videos:past-broadcasts**
|
|
||||||
- **twitch:videos:uploads**
|
|
||||||
- **twitch:vod**
|
- **twitch:vod**
|
||||||
|
- **TwitchCollection**
|
||||||
|
- **TwitchVideos**
|
||||||
|
- **TwitchVideosClips**
|
||||||
|
- **TwitchVideosCollections**
|
||||||
- **twitter**
|
- **twitter**
|
||||||
- **twitter:amplify**
|
- **twitter:amplify**
|
||||||
- **twitter:broadcast**
|
- **twitter:broadcast**
|
||||||
@@ -989,7 +1065,10 @@
|
|||||||
- **Vidbit**
|
- **Vidbit**
|
||||||
- **Viddler**
|
- **Viddler**
|
||||||
- **Videa**
|
- **Videa**
|
||||||
|
- **video.arnes.si**: Arnes Video
|
||||||
- **video.google:search**: Google Video search
|
- **video.google:search**: Google Video search
|
||||||
|
- **video.sky.it**
|
||||||
|
- **video.sky.it:live**
|
||||||
- **VideoDetective**
|
- **VideoDetective**
|
||||||
- **videofy.me**
|
- **videofy.me**
|
||||||
- **videomore**
|
- **videomore**
|
||||||
@@ -1001,7 +1080,6 @@
|
|||||||
- **vidme**
|
- **vidme**
|
||||||
- **vidme:user**
|
- **vidme:user**
|
||||||
- **vidme:user:likes**
|
- **vidme:user:likes**
|
||||||
- **Vidzi**
|
|
||||||
- **vier**: vier.be and vijf.be
|
- **vier**: vier.be and vijf.be
|
||||||
- **vier:videos**
|
- **vier:videos**
|
||||||
- **viewlift**
|
- **viewlift**
|
||||||
@@ -1031,7 +1109,7 @@
|
|||||||
- **vk:wallpost**
|
- **vk:wallpost**
|
||||||
- **vlive**
|
- **vlive**
|
||||||
- **vlive:channel**
|
- **vlive:channel**
|
||||||
- **vlive:playlist**
|
- **vlive:post**
|
||||||
- **Vodlocker**
|
- **Vodlocker**
|
||||||
- **VODPl**
|
- **VODPl**
|
||||||
- **VODPlatform**
|
- **VODPlatform**
|
||||||
@@ -1046,10 +1124,12 @@
|
|||||||
- **vrv**
|
- **vrv**
|
||||||
- **vrv:series**
|
- **vrv:series**
|
||||||
- **VShare**
|
- **VShare**
|
||||||
|
- **VTM**
|
||||||
- **VTXTV**
|
- **VTXTV**
|
||||||
- **vube**: Vube.com
|
- **vube**: Vube.com
|
||||||
- **VuClip**
|
- **VuClip**
|
||||||
- **VVVVID**
|
- **VVVVID**
|
||||||
|
- **VVVVIDShow**
|
||||||
- **VyboryMos**
|
- **VyboryMos**
|
||||||
- **Vzaar**
|
- **Vzaar**
|
||||||
- **Wakanim**
|
- **Wakanim**
|
||||||
@@ -1072,6 +1152,7 @@
|
|||||||
- **WeiboMobile**
|
- **WeiboMobile**
|
||||||
- **WeiqiTV**: WQTV
|
- **WeiqiTV**: WQTV
|
||||||
- **Wistia**
|
- **Wistia**
|
||||||
|
- **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**
|
||||||
- **WSJ**: Wall Street Journal
|
- **WSJ**: Wall Street Journal
|
||||||
@@ -1079,7 +1160,7 @@
|
|||||||
- **WWE**
|
- **WWE**
|
||||||
- **XBef**
|
- **XBef**
|
||||||
- **XboxClips**
|
- **XboxClips**
|
||||||
- **XFileShare**: XFileShare based sites: ClipWatching, GoUnlimited, GoVid, HolaVid, Streamty, TheVideoBee, Uqload, VidBom, vidlo, VidLocker, VidShare, VUp, XVideoSharing
|
- **XFileShare**: XFileShare based sites: Aparat, ClipWatching, GoUnlimited, GoVid, HolaVid, Streamty, TheVideoBee, Uqload, VidBom, vidlo, VidLocker, VidShare, VUp, WolfStream, XVideoSharing
|
||||||
- **XHamster**
|
- **XHamster**
|
||||||
- **XHamsterEmbed**
|
- **XHamsterEmbed**
|
||||||
- **XHamsterUser**
|
- **XHamsterUser**
|
||||||
@@ -1103,6 +1184,8 @@
|
|||||||
- **yahoo:japannews**: Yahoo! Japan News
|
- **yahoo:japannews**: Yahoo! Japan News
|
||||||
- **YandexDisk**
|
- **YandexDisk**
|
||||||
- **yandexmusic:album**: Яндекс.Музыка - Альбом
|
- **yandexmusic:album**: Яндекс.Музыка - Альбом
|
||||||
|
- **yandexmusic:artist:albums**: Яндекс.Музыка - Артист - Альбомы
|
||||||
|
- **yandexmusic:artist:tracks**: Яндекс.Музыка - Артист - Треки
|
||||||
- **yandexmusic:playlist**: Яндекс.Музыка - Плейлист
|
- **yandexmusic:playlist**: Яндекс.Музыка - Плейлист
|
||||||
- **yandexmusic:track**: Яндекс.Музыка - Трек
|
- **yandexmusic:track**: Яндекс.Музыка - Трек
|
||||||
- **YandexVideo**
|
- **YandexVideo**
|
||||||
@@ -1120,25 +1203,24 @@
|
|||||||
- **YourPorn**
|
- **YourPorn**
|
||||||
- **YourUpload**
|
- **YourUpload**
|
||||||
- **youtube**: YouTube.com
|
- **youtube**: YouTube.com
|
||||||
- **youtube:channel**: YouTube.com channels
|
|
||||||
- **youtube:favorites**: YouTube.com favourite videos, ":ytfav" for short (requires authentication)
|
- **youtube:favorites**: YouTube.com favourite videos, ":ytfav" for short (requires authentication)
|
||||||
- **youtube:history**: Youtube watch history, ":ythistory" for short (requires authentication)
|
- **youtube:history**: Youtube watch history, ":ythistory" for short (requires authentication)
|
||||||
- **youtube:live**: YouTube.com live streams
|
|
||||||
- **youtube:playlist**: YouTube.com playlists
|
- **youtube:playlist**: YouTube.com playlists
|
||||||
- **youtube:playlists**: YouTube.com user/channel playlists
|
|
||||||
- **youtube:recommended**: YouTube.com recommended videos, ":ytrec" for short (requires authentication)
|
- **youtube:recommended**: YouTube.com recommended videos, ":ytrec" for short (requires authentication)
|
||||||
- **youtube:search**: YouTube.com searches
|
- **youtube:search**: YouTube.com searches
|
||||||
- **youtube:search:date**: YouTube.com searches, newest videos first
|
- **youtube:search:date**: YouTube.com searches, newest videos first
|
||||||
- **youtube:search_url**: YouTube.com search URLs
|
|
||||||
- **youtube:show**: YouTube.com (multi-season) shows
|
|
||||||
- **youtube:subscriptions**: YouTube.com subscriptions feed, "ytsubs" keyword (requires authentication)
|
- **youtube:subscriptions**: YouTube.com subscriptions feed, "ytsubs" keyword (requires authentication)
|
||||||
- **youtube:user**: YouTube.com user videos (URL or "ytuser" keyword)
|
- **youtube:tab**: YouTube.com tab
|
||||||
- **youtube:watchlater**: Youtube watch later list, ":ytwatchlater" for short (requires authentication)
|
- **youtube:watchlater**: Youtube watch later list, ":ytwatchlater" for short (requires authentication)
|
||||||
|
- **YoutubeYtBe**
|
||||||
|
- **YoutubeYtUser**
|
||||||
- **Zapiks**
|
- **Zapiks**
|
||||||
- **Zaq1**
|
|
||||||
- **Zattoo**
|
- **Zattoo**
|
||||||
- **ZattooLive**
|
- **ZattooLive**
|
||||||
- **ZDF**
|
- **ZDF**
|
||||||
- **ZDFChannel**
|
- **ZDFChannel**
|
||||||
|
- **Zhihu**
|
||||||
- **zingmp3**: mp3.zing.vn
|
- **zingmp3**: mp3.zing.vn
|
||||||
|
- **zingmp3:album**
|
||||||
|
- **zoom**
|
||||||
- **Zype**
|
- **Zype**
|
||||||
|
|||||||
@@ -128,6 +128,12 @@ def expect_value(self, got, expected, field):
|
|||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
contains_str in got,
|
contains_str in got,
|
||||||
'field %s (value: %r) should contain %r' % (field, got, contains_str))
|
'field %s (value: %r) should contain %r' % (field, got, contains_str))
|
||||||
|
elif isinstance(expected, compat_str) and re.match(r'^lambda \w+:', expected):
|
||||||
|
fn = eval(expected)
|
||||||
|
suite = expected.split(':', 1)[1].strip()
|
||||||
|
self.assertTrue(
|
||||||
|
fn(got),
|
||||||
|
'Expected field %s to meet condition %s, but value %r failed ' % (field, suite, got))
|
||||||
elif isinstance(expected, type):
|
elif isinstance(expected, type):
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
isinstance(got, expected),
|
isinstance(got, expected),
|
||||||
@@ -137,7 +143,7 @@ def expect_value(self, got, expected, field):
|
|||||||
elif isinstance(expected, list) and isinstance(got, list):
|
elif isinstance(expected, list) and isinstance(got, list):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
len(expected), len(got),
|
len(expected), len(got),
|
||||||
'Expect a list of length %d, but got a list of length %d for field %s' % (
|
'Expected a list of length %d, but got a list of length %d for field %s' % (
|
||||||
len(expected), len(got), field))
|
len(expected), len(got), field))
|
||||||
for index, (item_got, item_expected) in enumerate(zip(got, expected)):
|
for index, (item_got, item_expected) in enumerate(zip(got, expected)):
|
||||||
type_got = type(item_got)
|
type_got = type(item_got)
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
"noprogress": false,
|
"noprogress": false,
|
||||||
"outtmpl": "%(id)s.%(ext)s",
|
"outtmpl": "%(id)s.%(ext)s",
|
||||||
"password": null,
|
"password": null,
|
||||||
"playlistend": -1,
|
|
||||||
"playliststart": 1,
|
"playliststart": 1,
|
||||||
"prefer_free_formats": false,
|
"prefer_free_formats": false,
|
||||||
"quiet": false,
|
"quiet": false,
|
||||||
@@ -37,7 +36,7 @@
|
|||||||
"writeinfojson": true,
|
"writeinfojson": true,
|
||||||
"writesubtitles": false,
|
"writesubtitles": false,
|
||||||
"allsubtitles": false,
|
"allsubtitles": false,
|
||||||
"listssubtitles": false,
|
"listsubtitles": false,
|
||||||
"socket_timeout": 20,
|
"socket_timeout": 20,
|
||||||
"fixup": "never"
|
"fixup": "never"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,6 +98,55 @@ class TestInfoExtractor(unittest.TestCase):
|
|||||||
self.assertRaises(RegexNotFoundError, ie._html_search_meta, 'z', html, None, fatal=True)
|
self.assertRaises(RegexNotFoundError, ie._html_search_meta, 'z', html, None, fatal=True)
|
||||||
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):
|
||||||
|
# https://github.com/ytdl-org/youtube-dl/issues/23306
|
||||||
|
expect_dict(
|
||||||
|
self,
|
||||||
|
self.ie._search_json_ld(r'''<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "http://schema.org/",
|
||||||
|
"@type": "VideoObject",
|
||||||
|
"name": "1 On 1 With Kleio",
|
||||||
|
"url": "https://www.eporner.com/hd-porn/xN49A1cT3eB/1-On-1-With-Kleio/",
|
||||||
|
"duration": "PT0H12M23S",
|
||||||
|
"thumbnailUrl": ["https://static-eu-cdn.eporner.com/thumbs/static4/7/78/780/780814/9_360.jpg", "https://imggen.eporner.com/780814/1920/1080/9.jpg"],
|
||||||
|
"contentUrl": "https://gvideo.eporner.com/xN49A1cT3eB/xN49A1cT3eB.mp4",
|
||||||
|
"embedUrl": "https://www.eporner.com/embed/xN49A1cT3eB/1-On-1-With-Kleio/",
|
||||||
|
"image": "https://static-eu-cdn.eporner.com/thumbs/static4/7/78/780/780814/9_360.jpg",
|
||||||
|
"width": "1920",
|
||||||
|
"height": "1080",
|
||||||
|
"encodingFormat": "mp4",
|
||||||
|
"bitrate": "6617kbps",
|
||||||
|
"isFamilyFriendly": "False",
|
||||||
|
"description": "Kleio Valentien",
|
||||||
|
"uploadDate": "2015-12-05T21:24:35+01:00",
|
||||||
|
"interactionStatistic": {
|
||||||
|
"@type": "InteractionCounter",
|
||||||
|
"interactionType": { "@type": "http://schema.org/WatchAction" },
|
||||||
|
"userInteractionCount": 1120958
|
||||||
|
}, "aggregateRating": {
|
||||||
|
"@type": "AggregateRating",
|
||||||
|
"ratingValue": "88",
|
||||||
|
"ratingCount": "630",
|
||||||
|
"bestRating": "100",
|
||||||
|
"worstRating": "0"
|
||||||
|
}, "actor": [{
|
||||||
|
"@type": "Person",
|
||||||
|
"name": "Kleio Valentien",
|
||||||
|
"url": "https://www.eporner.com/pornstar/kleio-valentien/"
|
||||||
|
}]}
|
||||||
|
</script>''', None),
|
||||||
|
{
|
||||||
|
'title': '1 On 1 With Kleio',
|
||||||
|
'description': 'Kleio Valentien',
|
||||||
|
'url': 'https://gvideo.eporner.com/xN49A1cT3eB/xN49A1cT3eB.mp4',
|
||||||
|
'timestamp': 1449347075,
|
||||||
|
'duration': 743.0,
|
||||||
|
'view_count': 1120958,
|
||||||
|
'width': 1920,
|
||||||
|
'height': 1080,
|
||||||
|
})
|
||||||
|
|
||||||
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')
|
||||||
self.assertEqual(self.ie._download_json(uri, None), {'foo': 'blah'})
|
self.assertEqual(self.ie._download_json(uri, None), {'foo': 'blah'})
|
||||||
@@ -108,6 +157,18 @@ class TestInfoExtractor(unittest.TestCase):
|
|||||||
self.assertEqual(self.ie._download_json(uri, None, fatal=False), None)
|
self.assertEqual(self.ie._download_json(uri, None, fatal=False), None)
|
||||||
|
|
||||||
def test_parse_html5_media_entries(self):
|
def test_parse_html5_media_entries(self):
|
||||||
|
# inline video tag
|
||||||
|
expect_dict(
|
||||||
|
self,
|
||||||
|
self.ie._parse_html5_media_entries(
|
||||||
|
'https://127.0.0.1/video.html',
|
||||||
|
r'<html><video src="/vid.mp4" /></html>', None)[0],
|
||||||
|
{
|
||||||
|
'formats': [{
|
||||||
|
'url': 'https://127.0.0.1/vid.mp4',
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
|
||||||
# from https://www.r18.com/
|
# from https://www.r18.com/
|
||||||
# with kpbs in label
|
# with kpbs in label
|
||||||
expect_dict(
|
expect_dict(
|
||||||
|
|||||||
@@ -464,6 +464,7 @@ class TestFormatSelection(unittest.TestCase):
|
|||||||
assert_syntax_error('+bestaudio')
|
assert_syntax_error('+bestaudio')
|
||||||
assert_syntax_error('bestvideo+')
|
assert_syntax_error('bestvideo+')
|
||||||
assert_syntax_error('/')
|
assert_syntax_error('/')
|
||||||
|
assert_syntax_error('bestvideo+bestvideo+bestaudio')
|
||||||
|
|
||||||
def test_format_filtering(self):
|
def test_format_filtering(self):
|
||||||
formats = [
|
formats = [
|
||||||
@@ -632,13 +633,20 @@ class TestYoutubeDL(unittest.TestCase):
|
|||||||
'title2': '%PATH%',
|
'title2': '%PATH%',
|
||||||
}
|
}
|
||||||
|
|
||||||
def fname(templ):
|
def fname(templ, na_placeholder='NA'):
|
||||||
ydl = YoutubeDL({'outtmpl': templ})
|
params = {'outtmpl': templ}
|
||||||
|
if na_placeholder != 'NA':
|
||||||
|
params['outtmpl_na_placeholder'] = na_placeholder
|
||||||
|
ydl = YoutubeDL(params)
|
||||||
return ydl.prepare_filename(info)
|
return ydl.prepare_filename(info)
|
||||||
self.assertEqual(fname('%(id)s.%(ext)s'), '1234.mp4')
|
self.assertEqual(fname('%(id)s.%(ext)s'), '1234.mp4')
|
||||||
self.assertEqual(fname('%(id)s-%(width)s.%(ext)s'), '1234-NA.mp4')
|
self.assertEqual(fname('%(id)s-%(width)s.%(ext)s'), '1234-NA.mp4')
|
||||||
# Replace missing fields with 'NA'
|
NA_TEST_OUTTMPL = '%(uploader_date)s-%(width)d-%(id)s.%(ext)s'
|
||||||
self.assertEqual(fname('%(uploader_date)s-%(id)s.%(ext)s'), 'NA-1234.mp4')
|
# Replace missing fields with 'NA' by default
|
||||||
|
self.assertEqual(fname(NA_TEST_OUTTMPL), 'NA-NA-1234.mp4')
|
||||||
|
# Or by provided placeholder
|
||||||
|
self.assertEqual(fname(NA_TEST_OUTTMPL, na_placeholder='none'), 'none-none-1234.mp4')
|
||||||
|
self.assertEqual(fname(NA_TEST_OUTTMPL, na_placeholder=''), '--1234.mp4')
|
||||||
self.assertEqual(fname('%(height)d.%(ext)s'), '1080.mp4')
|
self.assertEqual(fname('%(height)d.%(ext)s'), '1080.mp4')
|
||||||
self.assertEqual(fname('%(height)6d.%(ext)s'), ' 1080.mp4')
|
self.assertEqual(fname('%(height)6d.%(ext)s'), ' 1080.mp4')
|
||||||
self.assertEqual(fname('%(height)-6d.%(ext)s'), '1080 .mp4')
|
self.assertEqual(fname('%(height)-6d.%(ext)s'), '1080 .mp4')
|
||||||
@@ -919,6 +927,95 @@ class TestYoutubeDL(unittest.TestCase):
|
|||||||
self.assertEqual(downloaded['extractor'], 'testex')
|
self.assertEqual(downloaded['extractor'], 'testex')
|
||||||
self.assertEqual(downloaded['extractor_key'], 'TestEx')
|
self.assertEqual(downloaded['extractor_key'], 'TestEx')
|
||||||
|
|
||||||
|
# Test case for https://github.com/ytdl-org/youtube-dl/issues/27064
|
||||||
|
def test_ignoreerrors_for_playlist_with_url_transparent_iterable_entries(self):
|
||||||
|
|
||||||
|
class _YDL(YDL):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(_YDL, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def trouble(self, s, tb=None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
ydl = _YDL({
|
||||||
|
'format': 'extra',
|
||||||
|
'ignoreerrors': True,
|
||||||
|
})
|
||||||
|
|
||||||
|
class VideoIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'video:(?P<id>\d+)'
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
formats = [{
|
||||||
|
'format_id': 'default',
|
||||||
|
'url': 'url:',
|
||||||
|
}]
|
||||||
|
if video_id == '0':
|
||||||
|
raise ExtractorError('foo')
|
||||||
|
if video_id == '2':
|
||||||
|
formats.append({
|
||||||
|
'format_id': 'extra',
|
||||||
|
'url': TEST_URL,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': 'Video %s' % video_id,
|
||||||
|
'formats': formats,
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlaylistIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'playlist:'
|
||||||
|
|
||||||
|
def _entries(self):
|
||||||
|
for n in range(3):
|
||||||
|
video_id = compat_str(n)
|
||||||
|
yield {
|
||||||
|
'_type': 'url_transparent',
|
||||||
|
'ie_key': VideoIE.ie_key(),
|
||||||
|
'id': video_id,
|
||||||
|
'url': 'video:%s' % video_id,
|
||||||
|
'title': 'Video Transparent %s' % video_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
return self.playlist_result(self._entries())
|
||||||
|
|
||||||
|
ydl.add_info_extractor(VideoIE(ydl))
|
||||||
|
ydl.add_info_extractor(PlaylistIE(ydl))
|
||||||
|
info = ydl.extract_info('playlist:')
|
||||||
|
entries = info['entries']
|
||||||
|
self.assertEqual(len(entries), 3)
|
||||||
|
self.assertTrue(entries[0] is None)
|
||||||
|
self.assertTrue(entries[1] is None)
|
||||||
|
self.assertEqual(len(ydl.downloaded_info_dicts), 1)
|
||||||
|
downloaded = ydl.downloaded_info_dicts[0]
|
||||||
|
self.assertEqual(entries[2], downloaded)
|
||||||
|
self.assertEqual(downloaded['url'], TEST_URL)
|
||||||
|
self.assertEqual(downloaded['title'], 'Video Transparent 2')
|
||||||
|
self.assertEqual(downloaded['id'], '2')
|
||||||
|
self.assertEqual(downloaded['extractor'], 'Video')
|
||||||
|
self.assertEqual(downloaded['extractor_key'], 'Video')
|
||||||
|
|
||||||
|
def test_default_times(self):
|
||||||
|
"""Test addition of missing upload/release/_date from /release_/timestamp"""
|
||||||
|
info = {
|
||||||
|
'id': '1234',
|
||||||
|
'url': TEST_URL,
|
||||||
|
'title': 'Title',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'timestamp': 1631352900,
|
||||||
|
'release_timestamp': 1632995931,
|
||||||
|
}
|
||||||
|
|
||||||
|
params = {'simulate': True, }
|
||||||
|
ydl = FakeYDL(params)
|
||||||
|
out_info = ydl.process_ie_result(info)
|
||||||
|
self.assertTrue(isinstance(out_info['upload_date'], compat_str))
|
||||||
|
self.assertEqual(out_info['upload_date'], '20210911')
|
||||||
|
self.assertTrue(isinstance(out_info['release_date'], compat_str))
|
||||||
|
self.assertEqual(out_info['release_date'], '20210930')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ class TestYoutubeDLCookieJar(unittest.TestCase):
|
|||||||
assert_cookie_has_value('HTTPONLY_COOKIE')
|
assert_cookie_has_value('HTTPONLY_COOKIE')
|
||||||
assert_cookie_has_value('JS_ACCESSIBLE_COOKIE')
|
assert_cookie_has_value('JS_ACCESSIBLE_COOKIE')
|
||||||
|
|
||||||
|
def test_malformed_cookies(self):
|
||||||
|
cookiejar = YoutubeDLCookieJar('./test/testdata/cookies/malformed_cookies.txt')
|
||||||
|
cookiejar.load(ignore_discard=True, ignore_expires=True)
|
||||||
|
# Cookies should be empty since all malformed cookie file entries
|
||||||
|
# will be ignored
|
||||||
|
self.assertFalse(cookiejar._cookies)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import sys
|
|||||||
import unittest
|
import unittest
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
from youtube_dl.aes import aes_decrypt, aes_encrypt, aes_cbc_decrypt, aes_cbc_encrypt, aes_decrypt_text
|
from youtube_dl.aes import aes_decrypt, aes_encrypt, aes_cbc_decrypt, aes_cbc_encrypt, aes_decrypt_text, aes_ecb_encrypt
|
||||||
from youtube_dl.utils import bytes_to_intlist, intlist_to_bytes
|
from youtube_dl.utils import bytes_to_intlist, intlist_to_bytes
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
@@ -58,6 +58,13 @@ 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)
|
||||||
|
encrypted = intlist_to_bytes(aes_ecb_encrypt(data, self.key))
|
||||||
|
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:')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -31,16 +31,17 @@ class TestAllURLsMatching(unittest.TestCase):
|
|||||||
|
|
||||||
def test_youtube_playlist_matching(self):
|
def test_youtube_playlist_matching(self):
|
||||||
assertPlaylist = lambda url: self.assertMatch(url, ['youtube:playlist'])
|
assertPlaylist = lambda url: self.assertMatch(url, ['youtube:playlist'])
|
||||||
|
assertTab = lambda url: self.assertMatch(url, ['youtube:tab'])
|
||||||
assertPlaylist('ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')
|
assertPlaylist('ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')
|
||||||
assertPlaylist('UUBABnxM4Ar9ten8Mdjj1j0Q') # 585
|
assertPlaylist('UUBABnxM4Ar9ten8Mdjj1j0Q') # 585
|
||||||
assertPlaylist('PL63F0C78739B09958')
|
assertPlaylist('PL63F0C78739B09958')
|
||||||
assertPlaylist('https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q')
|
assertTab('https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q')
|
||||||
assertPlaylist('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')
|
assertTab('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')
|
||||||
assertPlaylist('https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC')
|
assertTab('https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC')
|
||||||
assertPlaylist('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'))
|
||||||
# Top tracks
|
# Top tracks
|
||||||
assertPlaylist('https://www.youtube.com/playlist?list=MCUS.20142101')
|
assertTab('https://www.youtube.com/playlist?list=MCUS.20142101')
|
||||||
|
|
||||||
def test_youtube_matching(self):
|
def test_youtube_matching(self):
|
||||||
self.assertTrue(YoutubeIE.suitable('PLtS2H6bU1M'))
|
self.assertTrue(YoutubeIE.suitable('PLtS2H6bU1M'))
|
||||||
@@ -51,36 +52,24 @@ class TestAllURLsMatching(unittest.TestCase):
|
|||||||
self.assertMatch('http://www.cleanvideosearch.com/media/action/yt/watch?videoId=8v_4O44sfjM', ['youtube'])
|
self.assertMatch('http://www.cleanvideosearch.com/media/action/yt/watch?videoId=8v_4O44sfjM', ['youtube'])
|
||||||
|
|
||||||
def test_youtube_channel_matching(self):
|
def test_youtube_channel_matching(self):
|
||||||
assertChannel = lambda url: self.assertMatch(url, ['youtube:channel'])
|
assertChannel = lambda url: self.assertMatch(url, ['youtube:tab'])
|
||||||
assertChannel('https://www.youtube.com/channel/HCtnHdj3df7iM')
|
assertChannel('https://www.youtube.com/channel/HCtnHdj3df7iM')
|
||||||
assertChannel('https://www.youtube.com/channel/HCtnHdj3df7iM?feature=gb_ch_rec')
|
assertChannel('https://www.youtube.com/channel/HCtnHdj3df7iM?feature=gb_ch_rec')
|
||||||
assertChannel('https://www.youtube.com/channel/HCtnHdj3df7iM/videos')
|
assertChannel('https://www.youtube.com/channel/HCtnHdj3df7iM/videos')
|
||||||
|
|
||||||
def test_youtube_user_matching(self):
|
def test_youtube_user_matching(self):
|
||||||
self.assertMatch('http://www.youtube.com/NASAgovVideo/videos', ['youtube:user'])
|
self.assertMatch('http://www.youtube.com/NASAgovVideo/videos', ['youtube:tab'])
|
||||||
|
|
||||||
def test_youtube_feeds(self):
|
def test_youtube_feeds(self):
|
||||||
self.assertMatch('https://www.youtube.com/feed/watch_later', ['youtube:watchlater'])
|
self.assertMatch('https://www.youtube.com/feed/library', ['youtube:tab'])
|
||||||
self.assertMatch('https://www.youtube.com/feed/subscriptions', ['youtube:subscriptions'])
|
self.assertMatch('https://www.youtube.com/feed/history', ['youtube:tab'])
|
||||||
self.assertMatch('https://www.youtube.com/feed/recommended', ['youtube:recommended'])
|
self.assertMatch('https://www.youtube.com/feed/watch_later', ['youtube:tab'])
|
||||||
self.assertMatch('https://www.youtube.com/my_favorites', ['youtube:favorites'])
|
self.assertMatch('https://www.youtube.com/feed/subscriptions', ['youtube:tab'])
|
||||||
|
|
||||||
def test_youtube_show_matching(self):
|
|
||||||
self.assertMatch('http://www.youtube.com/show/airdisasters', ['youtube:show'])
|
|
||||||
|
|
||||||
def test_youtube_search_matching(self):
|
def test_youtube_search_matching(self):
|
||||||
self.assertMatch('http://www.youtube.com/results?search_query=making+mustard', ['youtube:search_url'])
|
self.assertMatch('http://www.youtube.com/results?search_query=making+mustard', ['youtube:search_url'])
|
||||||
self.assertMatch('https://www.youtube.com/results?baz=bar&search_query=youtube-dl+test+video&filters=video&lclk=video', ['youtube:search_url'])
|
self.assertMatch('https://www.youtube.com/results?baz=bar&search_query=youtube-dl+test+video&filters=video&lclk=video', ['youtube:search_url'])
|
||||||
|
|
||||||
def test_youtube_extract(self):
|
|
||||||
assertExtractId = lambda url, id: self.assertEqual(YoutubeIE.extract_id(url), id)
|
|
||||||
assertExtractId('http://www.youtube.com/watch?&v=BaW_jenozKc', 'BaW_jenozKc')
|
|
||||||
assertExtractId('https://www.youtube.com/watch?&v=BaW_jenozKc', 'BaW_jenozKc')
|
|
||||||
assertExtractId('https://www.youtube.com/watch?feature=player_embedded&v=BaW_jenozKc', 'BaW_jenozKc')
|
|
||||||
assertExtractId('https://www.youtube.com/watch_popup?v=BaW_jenozKc', 'BaW_jenozKc')
|
|
||||||
assertExtractId('http://www.youtube.com/watch?v=BaW_jenozKcsharePLED17F32AD9753930', 'BaW_jenozKc')
|
|
||||||
assertExtractId('BaW_jenozKc', 'BaW_jenozKc')
|
|
||||||
|
|
||||||
def test_facebook_matching(self):
|
def test_facebook_matching(self):
|
||||||
self.assertTrue(FacebookIE.suitable('https://www.facebook.com/Shiniknoh#!/photo.php?v=10153317450565268'))
|
self.assertTrue(FacebookIE.suitable('https://www.facebook.com/Shiniknoh#!/photo.php?v=10153317450565268'))
|
||||||
self.assertTrue(FacebookIE.suitable('https://www.facebook.com/cindyweather?fref=ts#!/photo.php?v=10152183998945793'))
|
self.assertTrue(FacebookIE.suitable('https://www.facebook.com/cindyweather?fref=ts#!/photo.php?v=10152183998945793'))
|
||||||
|
|||||||
@@ -3,17 +3,18 @@
|
|||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import shutil
|
|
||||||
|
|
||||||
# Allow direct execution
|
# Allow direct execution
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
|
||||||
from test.helper import FakeYDL
|
from test.helper import FakeYDL
|
||||||
from youtube_dl.cache import Cache
|
from youtube_dl.cache import Cache
|
||||||
|
from youtube_dl.utils import version_tuple
|
||||||
|
from youtube_dl.version import __version__
|
||||||
|
|
||||||
|
|
||||||
def _is_empty(d):
|
def _is_empty(d):
|
||||||
@@ -54,6 +55,17 @@ class TestCache(unittest.TestCase):
|
|||||||
self.assertFalse(os.path.exists(self.test_dir))
|
self.assertFalse(os.path.exists(self.test_dir))
|
||||||
self.assertEqual(c.load('test_cache', 'k.'), None)
|
self.assertEqual(c.load('test_cache', 'k.'), None)
|
||||||
|
|
||||||
|
def test_cache_validation(self):
|
||||||
|
ydl = FakeYDL({
|
||||||
|
'cachedir': self.test_dir,
|
||||||
|
})
|
||||||
|
c = Cache(ydl)
|
||||||
|
obj = {'x': 1, 'y': ['ä', '\\a', True]}
|
||||||
|
c.store('test_cache', 'k.', obj)
|
||||||
|
self.assertEqual(c.load('test_cache', 'k.', min_ver='1970.01.01'), obj)
|
||||||
|
new_version = '.'.join(('%d' % ((v + 1) if i == 0 else v, )) for i, v in enumerate(version_tuple(__version__)))
|
||||||
|
self.assertIs(c.load('test_cache', 'k.', min_ver=new_version), None)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|||||||
|
|
||||||
|
|
||||||
from youtube_dl.compat import (
|
from youtube_dl.compat import (
|
||||||
|
compat_casefold,
|
||||||
compat_getenv,
|
compat_getenv,
|
||||||
compat_setenv,
|
compat_setenv,
|
||||||
compat_etree_Element,
|
compat_etree_Element,
|
||||||
@@ -118,9 +119,21 @@ class TestCompat(unittest.TestCase):
|
|||||||
<smil xmlns="http://www.w3.org/2001/SMIL20/Language"></smil>'''
|
<smil xmlns="http://www.w3.org/2001/SMIL20/Language"></smil>'''
|
||||||
compat_etree_fromstring(xml)
|
compat_etree_fromstring(xml)
|
||||||
|
|
||||||
def test_struct_unpack(self):
|
def test_compat_struct_unpack(self):
|
||||||
self.assertEqual(compat_struct_unpack('!B', b'\x00'), (0,))
|
self.assertEqual(compat_struct_unpack('!B', b'\x00'), (0,))
|
||||||
|
|
||||||
|
def test_compat_casefold(self):
|
||||||
|
if hasattr(compat_str, 'casefold'):
|
||||||
|
# don't bother to test str.casefold() (again)
|
||||||
|
return
|
||||||
|
# thanks https://bugs.python.org/file24232/casefolding.patch
|
||||||
|
self.assertEqual(compat_casefold('hello'), 'hello')
|
||||||
|
self.assertEqual(compat_casefold('hELlo'), 'hello')
|
||||||
|
self.assertEqual(compat_casefold('ß'), 'ss')
|
||||||
|
self.assertEqual(compat_casefold('fi'), 'fi')
|
||||||
|
self.assertEqual(compat_casefold('\u03a3'), '\u03c3')
|
||||||
|
self.assertEqual(compat_casefold('A\u0345\u03a3'), 'a\u03b9\u03c3')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ from youtube_dl.compat import (
|
|||||||
from youtube_dl.utils import (
|
from youtube_dl.utils import (
|
||||||
DownloadError,
|
DownloadError,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
error_to_compat_str,
|
||||||
format_bytes,
|
format_bytes,
|
||||||
UnavailableVideoError,
|
UnavailableVideoError,
|
||||||
)
|
)
|
||||||
@@ -100,27 +101,28 @@ def generator(test_case, tname):
|
|||||||
|
|
||||||
def print_skipping(reason):
|
def print_skipping(reason):
|
||||||
print('Skipping %s: %s' % (test_case['name'], reason))
|
print('Skipping %s: %s' % (test_case['name'], reason))
|
||||||
|
self.skipTest(reason)
|
||||||
|
|
||||||
if not ie.working():
|
if not ie.working():
|
||||||
print_skipping('IE marked as not _WORKING')
|
print_skipping('IE marked as not _WORKING')
|
||||||
return
|
|
||||||
|
|
||||||
for tc in test_cases:
|
for tc in test_cases:
|
||||||
info_dict = tc.get('info_dict', {})
|
info_dict = tc.get('info_dict', {})
|
||||||
if not (info_dict.get('id') and info_dict.get('ext')):
|
if not (info_dict.get('id') and info_dict.get('ext')):
|
||||||
raise Exception('Test definition incorrect. The output file cannot be known. Are both \'id\' and \'ext\' keys present?')
|
raise Exception('Test definition (%s) requires both \'id\' and \'ext\' keys present to define the output file' % (tname, ))
|
||||||
|
|
||||||
if 'skip' in test_case:
|
if 'skip' in test_case:
|
||||||
print_skipping(test_case['skip'])
|
print_skipping(test_case['skip'])
|
||||||
return
|
|
||||||
for other_ie in other_ies:
|
for other_ie in other_ies:
|
||||||
if not other_ie.working():
|
if not other_ie.working():
|
||||||
print_skipping('test depends on %sIE, marked as not WORKING' % other_ie.ie_key())
|
print_skipping('test depends on %sIE, marked as not WORKING' % other_ie.ie_key())
|
||||||
return
|
|
||||||
|
|
||||||
params = get_params(test_case.get('params', {}))
|
params = get_params(test_case.get('params', {}))
|
||||||
params['outtmpl'] = tname + '_' + params['outtmpl']
|
params['outtmpl'] = tname + '_' + params['outtmpl']
|
||||||
if is_playlist and 'playlist' not in test_case:
|
if is_playlist and 'playlist' not in test_case:
|
||||||
params.setdefault('extract_flat', 'in_playlist')
|
params.setdefault('extract_flat', 'in_playlist')
|
||||||
|
params.setdefault('playlistend', test_case.get('playlist_mincount'))
|
||||||
params.setdefault('skip_download', True)
|
params.setdefault('skip_download', True)
|
||||||
|
|
||||||
ydl = YoutubeDL(params, auto_init=False)
|
ydl = YoutubeDL(params, auto_init=False)
|
||||||
@@ -160,7 +162,9 @@ def generator(test_case, tname):
|
|||||||
except (DownloadError, ExtractorError) as err:
|
except (DownloadError, ExtractorError) as err:
|
||||||
# Check if the exception is not a network related one
|
# Check if the exception is not a network related one
|
||||||
if not err.exc_info[0] in (compat_urllib_error.URLError, socket.timeout, UnavailableVideoError, compat_http_client.BadStatusLine) or (err.exc_info[0] == compat_HTTPError and err.exc_info[1].code == 503):
|
if not err.exc_info[0] in (compat_urllib_error.URLError, socket.timeout, UnavailableVideoError, compat_http_client.BadStatusLine) or (err.exc_info[0] == compat_HTTPError and err.exc_info[1].code == 503):
|
||||||
raise
|
msg = getattr(err, 'msg', error_to_compat_str(err))
|
||||||
|
err.msg = '%s (%s)' % (msg, tname, )
|
||||||
|
raise err
|
||||||
|
|
||||||
if try_num == RETRIES:
|
if try_num == RETRIES:
|
||||||
report_warning('%s failed due to network errors, skipping...' % tname)
|
report_warning('%s failed due to network errors, skipping...' % tname)
|
||||||
|
|||||||
@@ -39,6 +39,16 @@ class TestExecution(unittest.TestCase):
|
|||||||
_, stderr = p.communicate()
|
_, stderr = p.communicate()
|
||||||
self.assertFalse(stderr)
|
self.assertFalse(stderr)
|
||||||
|
|
||||||
|
def test_lazy_extractors(self):
|
||||||
|
try:
|
||||||
|
subprocess.check_call([sys.executable, 'devscripts/make_lazy_extractors.py', 'youtube_dl/extractor/lazy_extractors.py'], cwd=rootDir, stdout=_DEV_NULL)
|
||||||
|
subprocess.check_call([sys.executable, 'test/test_all_urls.py'], cwd=rootDir, stdout=_DEV_NULL)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
os.remove('youtube_dl/extractor/lazy_extractors.py')
|
||||||
|
except (IOError, OSError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ import sys
|
|||||||
import unittest
|
import unittest
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
from youtube_dl.jsinterp import JSInterpreter
|
import math
|
||||||
|
import re
|
||||||
|
|
||||||
|
from youtube_dl.compat import compat_re_Pattern
|
||||||
|
|
||||||
|
from youtube_dl.jsinterp import JS_Undefined, JSInterpreter
|
||||||
|
|
||||||
|
|
||||||
class TestJSInterpreter(unittest.TestCase):
|
class TestJSInterpreter(unittest.TestCase):
|
||||||
@@ -19,6 +24,9 @@ class TestJSInterpreter(unittest.TestCase):
|
|||||||
jsi = JSInterpreter('function x3(){return 42;}')
|
jsi = JSInterpreter('function x3(){return 42;}')
|
||||||
self.assertEqual(jsi.call_function('x3'), 42)
|
self.assertEqual(jsi.call_function('x3'), 42)
|
||||||
|
|
||||||
|
jsi = JSInterpreter('function x3(){42}')
|
||||||
|
self.assertEqual(jsi.call_function('x3'), None)
|
||||||
|
|
||||||
jsi = JSInterpreter('var x5 = function(){return 42;}')
|
jsi = JSInterpreter('var x5 = function(){return 42;}')
|
||||||
self.assertEqual(jsi.call_function('x5'), 42)
|
self.assertEqual(jsi.call_function('x5'), 42)
|
||||||
|
|
||||||
@@ -45,14 +53,32 @@ class TestJSInterpreter(unittest.TestCase):
|
|||||||
jsi = JSInterpreter('function f(){return 1 << 5;}')
|
jsi = JSInterpreter('function f(){return 1 << 5;}')
|
||||||
self.assertEqual(jsi.call_function('f'), 32)
|
self.assertEqual(jsi.call_function('f'), 32)
|
||||||
|
|
||||||
|
jsi = JSInterpreter('function f(){return 2 ** 5}')
|
||||||
|
self.assertEqual(jsi.call_function('f'), 32)
|
||||||
|
|
||||||
jsi = JSInterpreter('function f(){return 19 & 21;}')
|
jsi = JSInterpreter('function f(){return 19 & 21;}')
|
||||||
self.assertEqual(jsi.call_function('f'), 17)
|
self.assertEqual(jsi.call_function('f'), 17)
|
||||||
|
|
||||||
jsi = JSInterpreter('function f(){return 11 >> 2;}')
|
jsi = JSInterpreter('function f(){return 11 >> 2;}')
|
||||||
self.assertEqual(jsi.call_function('f'), 2)
|
self.assertEqual(jsi.call_function('f'), 2)
|
||||||
|
|
||||||
|
jsi = JSInterpreter('function f(){return []? 2+3: 4;}')
|
||||||
|
self.assertEqual(jsi.call_function('f'), 5)
|
||||||
|
|
||||||
|
jsi = JSInterpreter('function f(){return 1 == 2}')
|
||||||
|
self.assertEqual(jsi.call_function('f'), False)
|
||||||
|
|
||||||
|
jsi = JSInterpreter('function f(){return 0 && 1 || 2;}')
|
||||||
|
self.assertEqual(jsi.call_function('f'), 2)
|
||||||
|
|
||||||
|
jsi = JSInterpreter('function f(){return 0 ?? 42;}')
|
||||||
|
self.assertEqual(jsi.call_function('f'), 0)
|
||||||
|
|
||||||
|
jsi = JSInterpreter('function f(){return "life, the universe and everything" < 42;}')
|
||||||
|
self.assertFalse(jsi.call_function('f'))
|
||||||
|
|
||||||
def test_array_access(self):
|
def test_array_access(self):
|
||||||
jsi = JSInterpreter('function f(){var x = [1,2,3]; x[0] = 4; x[0] = 5; x[2] = 7; return x;}')
|
jsi = JSInterpreter('function f(){var x = [1,2,3]; x[0] = 4; x[0] = 5; x[2.0] = 7; return x;}')
|
||||||
self.assertEqual(jsi.call_function('f'), [5, 2, 7])
|
self.assertEqual(jsi.call_function('f'), [5, 2, 7])
|
||||||
|
|
||||||
def test_parens(self):
|
def test_parens(self):
|
||||||
@@ -62,6 +88,10 @@ class TestJSInterpreter(unittest.TestCase):
|
|||||||
jsi = JSInterpreter('function f(){return (1 + 2) * 3;}')
|
jsi = JSInterpreter('function f(){return (1 + 2) * 3;}')
|
||||||
self.assertEqual(jsi.call_function('f'), 9)
|
self.assertEqual(jsi.call_function('f'), 9)
|
||||||
|
|
||||||
|
def test_quotes(self):
|
||||||
|
jsi = JSInterpreter(r'function f(){return "a\"\\("}')
|
||||||
|
self.assertEqual(jsi.call_function('f'), r'a"\(')
|
||||||
|
|
||||||
def test_assignments(self):
|
def test_assignments(self):
|
||||||
jsi = JSInterpreter('function f(){var x = 20; x = 30 + 1; return x;}')
|
jsi = JSInterpreter('function f(){var x = 20; x = 30 + 1; return x;}')
|
||||||
self.assertEqual(jsi.call_function('f'), 31)
|
self.assertEqual(jsi.call_function('f'), 31)
|
||||||
@@ -104,13 +134,277 @@ class TestJSInterpreter(unittest.TestCase):
|
|||||||
}''')
|
}''')
|
||||||
self.assertEqual(jsi.call_function('x'), [20, 20, 30, 40, 50])
|
self.assertEqual(jsi.call_function('x'), [20, 20, 30, 40, 50])
|
||||||
|
|
||||||
|
def test_builtins(self):
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { return NaN }
|
||||||
|
''')
|
||||||
|
self.assertTrue(math.isnan(jsi.call_function('x')))
|
||||||
|
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { return new Date('Wednesday 31 December 1969 18:01:26 MDT') - 0; }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x'), 86000)
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x(dt) { return new Date(dt) - 0; }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x', 'Wednesday 31 December 1969 18:01:26 MDT'), 86000)
|
||||||
|
|
||||||
def test_call(self):
|
def test_call(self):
|
||||||
jsi = JSInterpreter('''
|
jsi = JSInterpreter('''
|
||||||
function x() { return 2; }
|
function x() { return 2; }
|
||||||
function y(a) { return x() + a; }
|
function y(a) { return x() + (a?a:0); }
|
||||||
function z() { return y(3); }
|
function z() { return y(3); }
|
||||||
''')
|
''')
|
||||||
self.assertEqual(jsi.call_function('z'), 5)
|
self.assertEqual(jsi.call_function('z'), 5)
|
||||||
|
self.assertEqual(jsi.call_function('y'), 2)
|
||||||
|
|
||||||
|
def test_for_loop(self):
|
||||||
|
# function x() { a=0; for (i=0; i-10; i++) {a++} a }
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { a=0; for (i=0; i-10; i++) {a++} return 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_catch(self):
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { try{throw 10} catch(e){return 5} }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x'), 5)
|
||||||
|
|
||||||
|
def test_finally(self):
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { try{throw 10} finally {return 42} }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x'), 42)
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { try{throw 10} catch(e){return 5} finally {return 42} }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x'), 42)
|
||||||
|
|
||||||
|
def test_nested_try(self):
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() {try {
|
||||||
|
try{throw 10} finally {throw 42}
|
||||||
|
} catch(e){return 5} }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x'), 5)
|
||||||
|
|
||||||
|
def test_for_loop_continue(self):
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { a=0; for (i=0; i-10; i++) { continue; a++ } return 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++ } return a }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x'), 0)
|
||||||
|
|
||||||
|
def test_for_loop_try(self):
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() {
|
||||||
|
for (i=0; i-10; i++) { try { if (i == 5) throw i} catch {return 10} finally {break} };
|
||||||
|
return 42 }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x'), 42)
|
||||||
|
|
||||||
|
def test_literal_list(self):
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { return [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)
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { a=5; return (a -= 1, a+=3, a); }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x'), 7)
|
||||||
|
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { return (l=[0,1,2,3], function(a, b){return a+b})((l[1], l[2]), l[3]) }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x'), 5)
|
||||||
|
|
||||||
|
def test_void(self):
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { return void 42; }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x'), None)
|
||||||
|
|
||||||
|
def test_return_function(self):
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { return [1, function(){return 1}][1] }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x')([]), 1)
|
||||||
|
|
||||||
|
def test_null(self):
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { return null; }
|
||||||
|
''')
|
||||||
|
self.assertIs(jsi.call_function('x'), None)
|
||||||
|
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { return [null > 0, null < 0, null == 0, null === 0]; }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x'), [False, False, False, False])
|
||||||
|
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { return [null >= 0, null <= 0]; }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x'), [True, True])
|
||||||
|
|
||||||
|
def test_undefined(self):
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { return undefined === undefined; }
|
||||||
|
''')
|
||||||
|
self.assertTrue(jsi.call_function('x'))
|
||||||
|
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { return undefined; }
|
||||||
|
''')
|
||||||
|
self.assertIs(jsi.call_function('x'), JS_Undefined)
|
||||||
|
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { let v; return v; }
|
||||||
|
''')
|
||||||
|
self.assertIs(jsi.call_function('x'), JS_Undefined)
|
||||||
|
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { return [undefined === undefined, undefined == undefined, undefined < undefined, undefined > undefined]; }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x'), [True, True, False, False])
|
||||||
|
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { return [undefined === 0, undefined == 0, undefined < 0, undefined > 0]; }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x'), [False, False, False, False])
|
||||||
|
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { return [undefined >= 0, undefined <= 0]; }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x'), [False, False])
|
||||||
|
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { return [undefined > null, undefined < null, undefined == null, undefined === null]; }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x'), [False, False, True, False])
|
||||||
|
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { return [undefined === null, undefined == null, undefined < null, undefined > null]; }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x'), [False, True, False, False])
|
||||||
|
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { let v; return [42+v, v+42, v**42, 42**v, 0**v]; }
|
||||||
|
''')
|
||||||
|
for y in jsi.call_function('x'):
|
||||||
|
self.assertTrue(math.isnan(y))
|
||||||
|
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { let v; return v**0; }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x'), 1)
|
||||||
|
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { let v; return [v>42, v<=42, v&&42, 42&&v]; }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x'), [False, False, JS_Undefined, JS_Undefined])
|
||||||
|
|
||||||
|
jsi = JSInterpreter('function x(){return undefined ?? 42; }')
|
||||||
|
self.assertEqual(jsi.call_function('x'), 42)
|
||||||
|
|
||||||
|
def test_object(self):
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { return {}; }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x'), {})
|
||||||
|
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { let a = {m1: 42, m2: 0 }; return [a["m1"], a.m2]; }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x'), [42, 0])
|
||||||
|
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { let a; return a?.qq; }
|
||||||
|
''')
|
||||||
|
self.assertIs(jsi.call_function('x'), JS_Undefined)
|
||||||
|
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { let a = {m1: 42, m2: 0 }; return a?.qq; }
|
||||||
|
''')
|
||||||
|
self.assertIs(jsi.call_function('x'), JS_Undefined)
|
||||||
|
|
||||||
|
def test_regex(self):
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { let a=/,,[/,913,/](,)}/; }
|
||||||
|
''')
|
||||||
|
self.assertIs(jsi.call_function('x'), None)
|
||||||
|
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { let a=/,,[/,913,/](,)}/; return a; }
|
||||||
|
''')
|
||||||
|
self.assertIsInstance(jsi.call_function('x'), compat_re_Pattern)
|
||||||
|
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { let a=/,,[/,913,/](,)}/i; return a; }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x').flags & ~re.U, re.I)
|
||||||
|
|
||||||
|
def test_char_code_at(self):
|
||||||
|
jsi = JSInterpreter('function x(i){return "test".charCodeAt(i)}')
|
||||||
|
self.assertEqual(jsi.call_function('x', 0), 116)
|
||||||
|
self.assertEqual(jsi.call_function('x', 1), 101)
|
||||||
|
self.assertEqual(jsi.call_function('x', 2), 115)
|
||||||
|
self.assertEqual(jsi.call_function('x', 3), 116)
|
||||||
|
self.assertEqual(jsi.call_function('x', 4), None)
|
||||||
|
self.assertEqual(jsi.call_function('x', 'not_a_number'), 116)
|
||||||
|
|
||||||
|
def test_bitwise_operators_overflow(self):
|
||||||
|
jsi = JSInterpreter('function x(){return -524999584 << 5}')
|
||||||
|
self.assertEqual(jsi.call_function('x'), 379882496)
|
||||||
|
|
||||||
|
jsi = JSInterpreter('function x(){return 1236566549 << 5}')
|
||||||
|
self.assertEqual(jsi.call_function('x'), 915423904)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ class BaseTestSubtitles(unittest.TestCase):
|
|||||||
self.DL = FakeYDL()
|
self.DL = FakeYDL()
|
||||||
self.ie = self.IE()
|
self.ie = self.IE()
|
||||||
self.DL.add_info_extractor(self.ie)
|
self.DL.add_info_extractor(self.ie)
|
||||||
|
if not self.IE.working():
|
||||||
|
print('Skipping: %s marked as not _WORKING' % self.IE.ie_key())
|
||||||
|
self.skipTest('IE marked as not _WORKING')
|
||||||
|
|
||||||
def getInfoDict(self):
|
def getInfoDict(self):
|
||||||
info_dict = self.DL.extract_info(self.url, download=False)
|
info_dict = self.DL.extract_info(self.url, download=False)
|
||||||
@@ -56,6 +59,21 @@ class BaseTestSubtitles(unittest.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
class TestYoutubeSubtitles(BaseTestSubtitles):
|
class TestYoutubeSubtitles(BaseTestSubtitles):
|
||||||
|
# Available subtitles for QRS8MkLhQmM:
|
||||||
|
# Language formats
|
||||||
|
# ru vtt, ttml, srv3, srv2, srv1, json3
|
||||||
|
# fr vtt, ttml, srv3, srv2, srv1, json3
|
||||||
|
# en vtt, ttml, srv3, srv2, srv1, json3
|
||||||
|
# nl vtt, ttml, srv3, srv2, srv1, json3
|
||||||
|
# de vtt, ttml, srv3, srv2, srv1, json3
|
||||||
|
# ko vtt, ttml, srv3, srv2, srv1, json3
|
||||||
|
# it vtt, ttml, srv3, srv2, srv1, json3
|
||||||
|
# zh-Hant vtt, ttml, srv3, srv2, srv1, json3
|
||||||
|
# hi vtt, ttml, srv3, srv2, srv1, json3
|
||||||
|
# pt-BR vtt, ttml, srv3, srv2, srv1, json3
|
||||||
|
# es-MX vtt, ttml, srv3, srv2, srv1, json3
|
||||||
|
# ja vtt, ttml, srv3, srv2, srv1, json3
|
||||||
|
# pl vtt, ttml, srv3, srv2, srv1, json3
|
||||||
url = 'QRS8MkLhQmM'
|
url = 'QRS8MkLhQmM'
|
||||||
IE = YoutubeIE
|
IE = YoutubeIE
|
||||||
|
|
||||||
@@ -64,41 +82,60 @@ class TestYoutubeSubtitles(BaseTestSubtitles):
|
|||||||
self.DL.params['allsubtitles'] = True
|
self.DL.params['allsubtitles'] = True
|
||||||
subtitles = self.getSubtitles()
|
subtitles = self.getSubtitles()
|
||||||
self.assertEqual(len(subtitles.keys()), 13)
|
self.assertEqual(len(subtitles.keys()), 13)
|
||||||
self.assertEqual(md5(subtitles['en']), '3cb210999d3e021bd6c7f0ea751eab06')
|
self.assertEqual(md5(subtitles['en']), 'ae1bd34126571a77aabd4d276b28044d')
|
||||||
self.assertEqual(md5(subtitles['it']), '6d752b98c31f1cf8d597050c7a2cb4b5')
|
self.assertEqual(md5(subtitles['it']), '0e0b667ba68411d88fd1c5f4f4eab2f9')
|
||||||
for lang in ['fr', 'de']:
|
for lang in ['fr', 'de']:
|
||||||
self.assertTrue(subtitles.get(lang) is not None, 'Subtitles for \'%s\' not extracted' % lang)
|
self.assertTrue(subtitles.get(lang) is not None, 'Subtitles for \'%s\' not extracted' % lang)
|
||||||
|
|
||||||
def test_youtube_subtitles_ttml_format(self):
|
def _test_subtitles_format(self, fmt, md5_hash, lang='en'):
|
||||||
self.DL.params['writesubtitles'] = True
|
self.DL.params['writesubtitles'] = True
|
||||||
self.DL.params['subtitlesformat'] = 'ttml'
|
self.DL.params['subtitlesformat'] = fmt
|
||||||
subtitles = self.getSubtitles()
|
subtitles = self.getSubtitles()
|
||||||
self.assertEqual(md5(subtitles['en']), 'e306f8c42842f723447d9f63ad65df54')
|
self.assertEqual(md5(subtitles[lang]), md5_hash)
|
||||||
|
|
||||||
|
def test_youtube_subtitles_ttml_format(self):
|
||||||
|
self._test_subtitles_format('ttml', 'c97ddf1217390906fa9fbd34901f3da2')
|
||||||
|
|
||||||
def test_youtube_subtitles_vtt_format(self):
|
def test_youtube_subtitles_vtt_format(self):
|
||||||
self.DL.params['writesubtitles'] = True
|
self._test_subtitles_format('vtt', 'ae1bd34126571a77aabd4d276b28044d')
|
||||||
self.DL.params['subtitlesformat'] = 'vtt'
|
|
||||||
|
def test_youtube_subtitles_json3_format(self):
|
||||||
|
self._test_subtitles_format('json3', '688dd1ce0981683867e7fe6fde2a224b')
|
||||||
|
|
||||||
|
def _test_automatic_captions(self, url, lang):
|
||||||
|
self.url = url
|
||||||
|
self.DL.params['writeautomaticsub'] = True
|
||||||
|
self.DL.params['subtitleslangs'] = [lang]
|
||||||
subtitles = self.getSubtitles()
|
subtitles = self.getSubtitles()
|
||||||
self.assertEqual(md5(subtitles['en']), '3cb210999d3e021bd6c7f0ea751eab06')
|
self.assertTrue(subtitles[lang] is not None)
|
||||||
|
|
||||||
def test_youtube_automatic_captions(self):
|
def test_youtube_automatic_captions(self):
|
||||||
self.url = '8YoUxe5ncPo'
|
# Available automatic captions for 8YoUxe5ncPo:
|
||||||
self.DL.params['writeautomaticsub'] = True
|
# Language formats (all in vtt, ttml, srv3, srv2, srv1, json3)
|
||||||
self.DL.params['subtitleslangs'] = ['it']
|
# gu, zh-Hans, zh-Hant, gd, ga, gl, lb, la, lo, tt, tr,
|
||||||
subtitles = self.getSubtitles()
|
# lv, lt, tk, th, tg, te, fil, haw, yi, ceb, yo, de, da,
|
||||||
self.assertTrue(subtitles['it'] is not None)
|
# el, eo, en, eu, et, es, ru, rw, ro, bn, be, bg, uk, jv,
|
||||||
|
# bs, ja, or, xh, co, ca, cy, cs, ps, pt, pa, vi, pl, hy,
|
||||||
|
# hr, ht, hu, hmn, hi, ha, mg, uz, ml, mn, mi, mk, ur,
|
||||||
|
# mt, ms, mr, ug, ta, my, af, sw, is, am,
|
||||||
|
# *it*, iw, sv, ar,
|
||||||
|
# su, zu, az, id, ig, nl, no, ne, ny, fr, ku, fy, fa, fi,
|
||||||
|
# ka, kk, sr, sq, ko, kn, km, st, sk, si, so, sn, sm, sl,
|
||||||
|
# ky, sd
|
||||||
|
# ...
|
||||||
|
self._test_automatic_captions('8YoUxe5ncPo', 'it')
|
||||||
|
|
||||||
|
@unittest.skip('ASR subs all in all supported langs now')
|
||||||
def test_youtube_translated_subtitles(self):
|
def test_youtube_translated_subtitles(self):
|
||||||
# This video has a subtitles track, which can be translated
|
# This video has a subtitles track, which can be translated (#4555)
|
||||||
self.url = 'Ky9eprVWzlI'
|
self._test_automatic_captions('Ky9eprVWzlI', 'it')
|
||||||
self.DL.params['writeautomaticsub'] = True
|
|
||||||
self.DL.params['subtitleslangs'] = ['it']
|
|
||||||
subtitles = self.getSubtitles()
|
|
||||||
self.assertTrue(subtitles['it'] is not None)
|
|
||||||
|
|
||||||
def test_youtube_nosubtitles(self):
|
def test_youtube_nosubtitles(self):
|
||||||
self.DL.expect_warning('video doesn\'t have subtitles')
|
self.DL.expect_warning('video doesn\'t have subtitles')
|
||||||
self.url = 'n5BB19UTcdA'
|
# Available automatic captions for 8YoUxe5ncPo:
|
||||||
|
# ...
|
||||||
|
# 8YoUxe5ncPo has no subtitles
|
||||||
|
self.url = '8YoUxe5ncPo'
|
||||||
self.DL.params['writesubtitles'] = True
|
self.DL.params['writesubtitles'] = True
|
||||||
self.DL.params['allsubtitles'] = True
|
self.DL.params['allsubtitles'] = True
|
||||||
subtitles = self.getSubtitles()
|
subtitles = self.getSubtitles()
|
||||||
@@ -128,6 +165,7 @@ class TestDailymotionSubtitles(BaseTestSubtitles):
|
|||||||
self.assertFalse(subtitles)
|
self.assertFalse(subtitles)
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skip('IE broken')
|
||||||
class TestTedSubtitles(BaseTestSubtitles):
|
class TestTedSubtitles(BaseTestSubtitles):
|
||||||
url = 'http://www.ted.com/talks/dan_dennett_on_our_consciousness.html'
|
url = 'http://www.ted.com/talks/dan_dennett_on_our_consciousness.html'
|
||||||
IE = TEDIE
|
IE = TEDIE
|
||||||
@@ -152,18 +190,19 @@ class TestVimeoSubtitles(BaseTestSubtitles):
|
|||||||
self.DL.params['allsubtitles'] = True
|
self.DL.params['allsubtitles'] = True
|
||||||
subtitles = self.getSubtitles()
|
subtitles = self.getSubtitles()
|
||||||
self.assertEqual(set(subtitles.keys()), set(['de', 'en', 'es', 'fr']))
|
self.assertEqual(set(subtitles.keys()), set(['de', 'en', 'es', 'fr']))
|
||||||
self.assertEqual(md5(subtitles['en']), '8062383cf4dec168fc40a088aa6d5888')
|
self.assertEqual(md5(subtitles['en']), '386cbc9320b94e25cb364b97935e5dd1')
|
||||||
self.assertEqual(md5(subtitles['fr']), 'b6191146a6c5d3a452244d853fde6dc8')
|
self.assertEqual(md5(subtitles['fr']), 'c9b69eef35bc6641c0d4da8a04f9dfac')
|
||||||
|
|
||||||
def test_nosubtitles(self):
|
def test_nosubtitles(self):
|
||||||
self.DL.expect_warning('video doesn\'t have subtitles')
|
self.DL.expect_warning('video doesn\'t have subtitles')
|
||||||
self.url = 'http://vimeo.com/56015672'
|
self.url = 'http://vimeo.com/68093876'
|
||||||
self.DL.params['writesubtitles'] = True
|
self.DL.params['writesubtitles'] = True
|
||||||
self.DL.params['allsubtitles'] = True
|
self.DL.params['allsubtitles'] = True
|
||||||
subtitles = self.getSubtitles()
|
subtitles = self.getSubtitles()
|
||||||
self.assertFalse(subtitles)
|
self.assertFalse(subtitles)
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skip('IE broken')
|
||||||
class TestWallaSubtitles(BaseTestSubtitles):
|
class TestWallaSubtitles(BaseTestSubtitles):
|
||||||
url = 'http://vod.walla.co.il/movie/2705958/the-yes-men'
|
url = 'http://vod.walla.co.il/movie/2705958/the-yes-men'
|
||||||
IE = WallaIE
|
IE = WallaIE
|
||||||
@@ -185,6 +224,7 @@ class TestWallaSubtitles(BaseTestSubtitles):
|
|||||||
self.assertFalse(subtitles)
|
self.assertFalse(subtitles)
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skip('IE broken')
|
||||||
class TestCeskaTelevizeSubtitles(BaseTestSubtitles):
|
class TestCeskaTelevizeSubtitles(BaseTestSubtitles):
|
||||||
url = 'http://www.ceskatelevize.cz/ivysilani/10600540290-u6-uzasny-svet-techniky'
|
url = 'http://www.ceskatelevize.cz/ivysilani/10600540290-u6-uzasny-svet-techniky'
|
||||||
IE = CeskaTelevizeIE
|
IE = CeskaTelevizeIE
|
||||||
@@ -206,6 +246,7 @@ class TestCeskaTelevizeSubtitles(BaseTestSubtitles):
|
|||||||
self.assertFalse(subtitles)
|
self.assertFalse(subtitles)
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skip('IE broken')
|
||||||
class TestLyndaSubtitles(BaseTestSubtitles):
|
class TestLyndaSubtitles(BaseTestSubtitles):
|
||||||
url = 'http://www.lynda.com/Bootstrap-tutorials/Using-exercise-files/110885/114408-4.html'
|
url = 'http://www.lynda.com/Bootstrap-tutorials/Using-exercise-files/110885/114408-4.html'
|
||||||
IE = LyndaIE
|
IE = LyndaIE
|
||||||
@@ -218,6 +259,7 @@ class TestLyndaSubtitles(BaseTestSubtitles):
|
|||||||
self.assertEqual(md5(subtitles['en']), '09bbe67222259bed60deaa26997d73a7')
|
self.assertEqual(md5(subtitles['en']), '09bbe67222259bed60deaa26997d73a7')
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skip('IE broken')
|
||||||
class TestNPOSubtitles(BaseTestSubtitles):
|
class TestNPOSubtitles(BaseTestSubtitles):
|
||||||
url = 'http://www.npo.nl/nos-journaal/28-08-2014/POW_00722860'
|
url = 'http://www.npo.nl/nos-journaal/28-08-2014/POW_00722860'
|
||||||
IE = NPOIE
|
IE = NPOIE
|
||||||
@@ -230,6 +272,7 @@ class TestNPOSubtitles(BaseTestSubtitles):
|
|||||||
self.assertEqual(md5(subtitles['nl']), 'fc6435027572b63fb4ab143abd5ad3f4')
|
self.assertEqual(md5(subtitles['nl']), 'fc6435027572b63fb4ab143abd5ad3f4')
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skip('IE broken')
|
||||||
class TestMTVSubtitles(BaseTestSubtitles):
|
class TestMTVSubtitles(BaseTestSubtitles):
|
||||||
url = 'http://www.cc.com/video-clips/p63lk0/adam-devine-s-house-party-chasing-white-swans'
|
url = 'http://www.cc.com/video-clips/p63lk0/adam-devine-s-house-party-chasing-white-swans'
|
||||||
IE = ComedyCentralIE
|
IE = ComedyCentralIE
|
||||||
@@ -253,22 +296,31 @@ class TestNRKSubtitles(BaseTestSubtitles):
|
|||||||
self.DL.params['writesubtitles'] = True
|
self.DL.params['writesubtitles'] = True
|
||||||
self.DL.params['allsubtitles'] = True
|
self.DL.params['allsubtitles'] = True
|
||||||
subtitles = self.getSubtitles()
|
subtitles = self.getSubtitles()
|
||||||
self.assertEqual(set(subtitles.keys()), set(['no']))
|
self.assertEqual(set(subtitles.keys()), set(['nb-ttv']))
|
||||||
self.assertEqual(md5(subtitles['no']), '544fa917d3197fcbee64634559221cc2')
|
self.assertEqual(md5(subtitles['nb-ttv']), '67e06ff02d0deaf975e68f6cb8f6a149')
|
||||||
|
|
||||||
|
|
||||||
class TestRaiPlaySubtitles(BaseTestSubtitles):
|
class TestRaiPlaySubtitles(BaseTestSubtitles):
|
||||||
url = 'http://www.raiplay.it/video/2014/04/Report-del-07042014-cb27157f-9dd0-4aee-b788-b1f67643a391.html'
|
|
||||||
IE = RaiPlayIE
|
IE = RaiPlayIE
|
||||||
|
|
||||||
def test_allsubtitles(self):
|
def test_subtitles_key(self):
|
||||||
|
self.url = 'http://www.raiplay.it/video/2014/04/Report-del-07042014-cb27157f-9dd0-4aee-b788-b1f67643a391.html'
|
||||||
self.DL.params['writesubtitles'] = True
|
self.DL.params['writesubtitles'] = True
|
||||||
self.DL.params['allsubtitles'] = True
|
self.DL.params['allsubtitles'] = True
|
||||||
subtitles = self.getSubtitles()
|
subtitles = self.getSubtitles()
|
||||||
self.assertEqual(set(subtitles.keys()), set(['it']))
|
self.assertEqual(set(subtitles.keys()), set(['it']))
|
||||||
self.assertEqual(md5(subtitles['it']), 'b1d90a98755126b61e667567a1f6680a')
|
self.assertEqual(md5(subtitles['it']), 'b1d90a98755126b61e667567a1f6680a')
|
||||||
|
|
||||||
|
def test_subtitles_array_key(self):
|
||||||
|
self.url = 'https://www.raiplay.it/video/2020/12/Report---04-01-2021-2e90f1de-8eee-4de4-ac0e-78d21db5b600.html'
|
||||||
|
self.DL.params['writesubtitles'] = True
|
||||||
|
self.DL.params['allsubtitles'] = True
|
||||||
|
subtitles = self.getSubtitles()
|
||||||
|
self.assertEqual(set(subtitles.keys()), set(['it']))
|
||||||
|
self.assertEqual(md5(subtitles['it']), '4b3264186fbb103508abe5311cfcb9cd')
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skip('IE broken - DRM only')
|
||||||
class TestVikiSubtitles(BaseTestSubtitles):
|
class TestVikiSubtitles(BaseTestSubtitles):
|
||||||
url = 'http://www.viki.com/videos/1060846v-punch-episode-18'
|
url = 'http://www.viki.com/videos/1060846v-punch-episode-18'
|
||||||
IE = VikiIE
|
IE = VikiIE
|
||||||
@@ -295,6 +347,7 @@ class TestThePlatformSubtitles(BaseTestSubtitles):
|
|||||||
self.assertEqual(md5(subtitles['en']), '97e7670cbae3c4d26ae8bcc7fdd78d4b')
|
self.assertEqual(md5(subtitles['en']), '97e7670cbae3c4d26ae8bcc7fdd78d4b')
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skip('IE broken')
|
||||||
class TestThePlatformFeedSubtitles(BaseTestSubtitles):
|
class TestThePlatformFeedSubtitles(BaseTestSubtitles):
|
||||||
url = 'http://feed.theplatform.com/f/7wvmTC/msnbc_video-p-test?form=json&pretty=true&range=-40&byGuid=n_hardball_5biden_140207'
|
url = 'http://feed.theplatform.com/f/7wvmTC/msnbc_video-p-test?form=json&pretty=true&range=-40&byGuid=n_hardball_5biden_140207'
|
||||||
IE = ThePlatformFeedIE
|
IE = ThePlatformFeedIE
|
||||||
@@ -330,7 +383,7 @@ class TestDemocracynowSubtitles(BaseTestSubtitles):
|
|||||||
self.DL.params['allsubtitles'] = True
|
self.DL.params['allsubtitles'] = True
|
||||||
subtitles = self.getSubtitles()
|
subtitles = self.getSubtitles()
|
||||||
self.assertEqual(set(subtitles.keys()), set(['en']))
|
self.assertEqual(set(subtitles.keys()), set(['en']))
|
||||||
self.assertEqual(md5(subtitles['en']), 'acaca989e24a9e45a6719c9b3d60815c')
|
self.assertEqual(md5(subtitles['en']), 'a3cc4c0b5eadd74d9974f1c1f5101045')
|
||||||
|
|
||||||
def test_subtitles_in_page(self):
|
def test_subtitles_in_page(self):
|
||||||
self.url = 'http://www.democracynow.org/2015/7/3/this_flag_comes_down_today_bree'
|
self.url = 'http://www.democracynow.org/2015/7/3/this_flag_comes_down_today_bree'
|
||||||
@@ -338,7 +391,7 @@ class TestDemocracynowSubtitles(BaseTestSubtitles):
|
|||||||
self.DL.params['allsubtitles'] = True
|
self.DL.params['allsubtitles'] = True
|
||||||
subtitles = self.getSubtitles()
|
subtitles = self.getSubtitles()
|
||||||
self.assertEqual(set(subtitles.keys()), set(['en']))
|
self.assertEqual(set(subtitles.keys()), set(['en']))
|
||||||
self.assertEqual(md5(subtitles['en']), 'acaca989e24a9e45a6719c9b3d60815c')
|
self.assertEqual(md5(subtitles['en']), 'a3cc4c0b5eadd74d9974f1c1f5101045')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|||||||
|
|
||||||
# Various small unit tests
|
# Various small unit tests
|
||||||
import io
|
import io
|
||||||
|
import itertools
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import xml.etree.ElementTree
|
import xml.etree.ElementTree
|
||||||
|
|
||||||
from youtube_dl.utils import (
|
from youtube_dl.utils import (
|
||||||
@@ -21,6 +23,7 @@ from youtube_dl.utils import (
|
|||||||
encode_base_n,
|
encode_base_n,
|
||||||
caesar,
|
caesar,
|
||||||
clean_html,
|
clean_html,
|
||||||
|
clean_podcast_url,
|
||||||
date_from_str,
|
date_from_str,
|
||||||
DateRange,
|
DateRange,
|
||||||
detect_exe_version,
|
detect_exe_version,
|
||||||
@@ -39,11 +42,14 @@ from youtube_dl.utils import (
|
|||||||
get_element_by_attribute,
|
get_element_by_attribute,
|
||||||
get_elements_by_class,
|
get_elements_by_class,
|
||||||
get_elements_by_attribute,
|
get_elements_by_attribute,
|
||||||
|
get_first,
|
||||||
InAdvancePagedList,
|
InAdvancePagedList,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
intlist_to_bytes,
|
intlist_to_bytes,
|
||||||
is_html,
|
is_html,
|
||||||
|
join_nonempty,
|
||||||
js_to_json,
|
js_to_json,
|
||||||
|
LazyList,
|
||||||
limit_length,
|
limit_length,
|
||||||
merge_dicts,
|
merge_dicts,
|
||||||
mimetype2ext,
|
mimetype2ext,
|
||||||
@@ -78,6 +84,8 @@ from youtube_dl.utils import (
|
|||||||
strip_or_none,
|
strip_or_none,
|
||||||
subtitles_filename,
|
subtitles_filename,
|
||||||
timeconvert,
|
timeconvert,
|
||||||
|
traverse_obj,
|
||||||
|
try_call,
|
||||||
unescapeHTML,
|
unescapeHTML,
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
unified_timestamp,
|
unified_timestamp,
|
||||||
@@ -91,6 +99,7 @@ from youtube_dl.utils import (
|
|||||||
urlencode_postdata,
|
urlencode_postdata,
|
||||||
urshift,
|
urshift,
|
||||||
update_url_query,
|
update_url_query,
|
||||||
|
variadic,
|
||||||
version_tuple,
|
version_tuple,
|
||||||
xpath_with_ns,
|
xpath_with_ns,
|
||||||
xpath_element,
|
xpath_element,
|
||||||
@@ -111,12 +120,18 @@ from youtube_dl.compat import (
|
|||||||
compat_getenv,
|
compat_getenv,
|
||||||
compat_os_name,
|
compat_os_name,
|
||||||
compat_setenv,
|
compat_setenv,
|
||||||
|
compat_str,
|
||||||
compat_urlparse,
|
compat_urlparse,
|
||||||
compat_parse_qs,
|
compat_parse_qs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestUtil(unittest.TestCase):
|
class TestUtil(unittest.TestCase):
|
||||||
|
|
||||||
|
# yt-dlp shim
|
||||||
|
def assertCountEqual(self, expected, got, msg='count should be the same'):
|
||||||
|
return self.assertEqual(len(tuple(expected)), len(tuple(got)), msg=msg)
|
||||||
|
|
||||||
def test_timeconvert(self):
|
def test_timeconvert(self):
|
||||||
self.assertTrue(timeconvert('') is None)
|
self.assertTrue(timeconvert('') is None)
|
||||||
self.assertTrue(timeconvert('bougrg') is None)
|
self.assertTrue(timeconvert('bougrg') is None)
|
||||||
@@ -369,6 +384,9 @@ class TestUtil(unittest.TestCase):
|
|||||||
self.assertEqual(unified_timestamp('Sep 11, 2013 | 5:49 AM'), 1378878540)
|
self.assertEqual(unified_timestamp('Sep 11, 2013 | 5:49 AM'), 1378878540)
|
||||||
self.assertEqual(unified_timestamp('December 15, 2017 at 7:49 am'), 1513324140)
|
self.assertEqual(unified_timestamp('December 15, 2017 at 7:49 am'), 1513324140)
|
||||||
self.assertEqual(unified_timestamp('2018-03-14T08:32:43.1493874+00:00'), 1521016363)
|
self.assertEqual(unified_timestamp('2018-03-14T08:32:43.1493874+00:00'), 1521016363)
|
||||||
|
self.assertEqual(unified_timestamp('December 31 1969 20:00:01 EDT'), 1)
|
||||||
|
self.assertEqual(unified_timestamp('Wednesday 31 December 1969 18:01:26 MDT'), 86)
|
||||||
|
self.assertEqual(unified_timestamp('12/31/1969 20:01:18 EDT', False), 78)
|
||||||
|
|
||||||
def test_determine_ext(self):
|
def test_determine_ext(self):
|
||||||
self.assertEqual(determine_ext('http://example.com/foo/bar.mp4/?download'), 'mp4')
|
self.assertEqual(determine_ext('http://example.com/foo/bar.mp4/?download'), 'mp4')
|
||||||
@@ -554,6 +572,11 @@ class TestUtil(unittest.TestCase):
|
|||||||
self.assertEqual(url_or_none('http$://foo.de'), None)
|
self.assertEqual(url_or_none('http$://foo.de'), None)
|
||||||
self.assertEqual(url_or_none('http://foo.de'), 'http://foo.de')
|
self.assertEqual(url_or_none('http://foo.de'), 'http://foo.de')
|
||||||
self.assertEqual(url_or_none('//foo.de'), '//foo.de')
|
self.assertEqual(url_or_none('//foo.de'), '//foo.de')
|
||||||
|
self.assertEqual(url_or_none('s3://foo.de'), None)
|
||||||
|
self.assertEqual(url_or_none('rtmpte://foo.de'), 'rtmpte://foo.de')
|
||||||
|
self.assertEqual(url_or_none('mms://foo.de'), 'mms://foo.de')
|
||||||
|
self.assertEqual(url_or_none('rtspu://foo.de'), 'rtspu://foo.de')
|
||||||
|
self.assertEqual(url_or_none('ftps://foo.de'), 'ftps://foo.de')
|
||||||
|
|
||||||
def test_parse_age_limit(self):
|
def test_parse_age_limit(self):
|
||||||
self.assertEqual(parse_age_limit(None), None)
|
self.assertEqual(parse_age_limit(None), None)
|
||||||
@@ -803,6 +826,8 @@ class TestUtil(unittest.TestCase):
|
|||||||
self.assertEqual(mimetype2ext('text/vtt'), 'vtt')
|
self.assertEqual(mimetype2ext('text/vtt'), 'vtt')
|
||||||
self.assertEqual(mimetype2ext('text/vtt;charset=utf-8'), 'vtt')
|
self.assertEqual(mimetype2ext('text/vtt;charset=utf-8'), 'vtt')
|
||||||
self.assertEqual(mimetype2ext('text/html; charset=utf-8'), 'html')
|
self.assertEqual(mimetype2ext('text/html; charset=utf-8'), 'html')
|
||||||
|
self.assertEqual(mimetype2ext('audio/x-wav'), 'wav')
|
||||||
|
self.assertEqual(mimetype2ext('audio/x-wav;codec=pcm'), 'wav')
|
||||||
|
|
||||||
def test_month_by_name(self):
|
def test_month_by_name(self):
|
||||||
self.assertEqual(month_by_name(None), None)
|
self.assertEqual(month_by_name(None), None)
|
||||||
@@ -935,6 +960,28 @@ class TestUtil(unittest.TestCase):
|
|||||||
self.assertEqual(d['x'], 1)
|
self.assertEqual(d['x'], 1)
|
||||||
self.assertEqual(d['y'], 'a')
|
self.assertEqual(d['y'], 'a')
|
||||||
|
|
||||||
|
# Just drop ! prefix for now though this results in a wrong value
|
||||||
|
on = js_to_json('''{
|
||||||
|
a: !0,
|
||||||
|
b: !1,
|
||||||
|
c: !!0,
|
||||||
|
d: !!42.42,
|
||||||
|
e: !!![],
|
||||||
|
f: !"abc",
|
||||||
|
g: !"",
|
||||||
|
!42: 42
|
||||||
|
}''')
|
||||||
|
self.assertEqual(json.loads(on), {
|
||||||
|
'a': 0,
|
||||||
|
'b': 1,
|
||||||
|
'c': 0,
|
||||||
|
'd': 42.42,
|
||||||
|
'e': [],
|
||||||
|
'f': "abc",
|
||||||
|
'g': "",
|
||||||
|
'42': 42
|
||||||
|
})
|
||||||
|
|
||||||
on = js_to_json('["abc", "def",]')
|
on = js_to_json('["abc", "def",]')
|
||||||
self.assertEqual(json.loads(on), ['abc', 'def'])
|
self.assertEqual(json.loads(on), ['abc', 'def'])
|
||||||
|
|
||||||
@@ -992,6 +1039,12 @@ class TestUtil(unittest.TestCase):
|
|||||||
on = js_to_json('{42:4.2e1}')
|
on = js_to_json('{42:4.2e1}')
|
||||||
self.assertEqual(json.loads(on), {'42': 42.0})
|
self.assertEqual(json.loads(on), {'42': 42.0})
|
||||||
|
|
||||||
|
on = js_to_json('{ "0x40": "0x40" }')
|
||||||
|
self.assertEqual(json.loads(on), {'0x40': '0x40'})
|
||||||
|
|
||||||
|
on = js_to_json('{ "040": "040" }')
|
||||||
|
self.assertEqual(json.loads(on), {'040': '040'})
|
||||||
|
|
||||||
def test_js_to_json_malformed(self):
|
def test_js_to_json_malformed(self):
|
||||||
self.assertEqual(js_to_json('42a1'), '42"a1"')
|
self.assertEqual(js_to_json('42a1'), '42"a1"')
|
||||||
self.assertEqual(js_to_json('42a-1'), '42"a"-1')
|
self.assertEqual(js_to_json('42a-1'), '42"a"-1')
|
||||||
@@ -1435,6 +1488,319 @@ Line 1
|
|||||||
self.assertEqual(get_elements_by_attribute('class', 'foo', html), [])
|
self.assertEqual(get_elements_by_attribute('class', 'foo', html), [])
|
||||||
self.assertEqual(get_elements_by_attribute('class', 'no-such-foo', html), [])
|
self.assertEqual(get_elements_by_attribute('class', 'no-such-foo', html), [])
|
||||||
|
|
||||||
|
def test_clean_podcast_url(self):
|
||||||
|
self.assertEqual(clean_podcast_url('https://www.podtrac.com/pts/redirect.mp3/chtbl.com/track/5899E/traffic.megaphone.fm/HSW7835899191.mp3'), 'https://traffic.megaphone.fm/HSW7835899191.mp3')
|
||||||
|
self.assertEqual(clean_podcast_url('https://play.podtrac.com/npr-344098539/edge1.pod.npr.org/anon.npr-podcasts/podcast/npr/waitwait/2020/10/20201003_waitwait_wwdtmpodcast201003-015621a5-f035-4eca-a9a1-7c118d90bc3c.mp3'), 'https://edge1.pod.npr.org/anon.npr-podcasts/podcast/npr/waitwait/2020/10/20201003_waitwait_wwdtmpodcast201003-015621a5-f035-4eca-a9a1-7c118d90bc3c.mp3')
|
||||||
|
|
||||||
|
def test_LazyList(self):
|
||||||
|
it = list(range(10))
|
||||||
|
|
||||||
|
self.assertEqual(list(LazyList(it)), it)
|
||||||
|
self.assertEqual(LazyList(it).exhaust(), it)
|
||||||
|
self.assertEqual(LazyList(it)[5], it[5])
|
||||||
|
|
||||||
|
self.assertEqual(LazyList(it)[5:], it[5:])
|
||||||
|
self.assertEqual(LazyList(it)[:5], it[:5])
|
||||||
|
self.assertEqual(LazyList(it)[::2], it[::2])
|
||||||
|
self.assertEqual(LazyList(it)[1::2], it[1::2])
|
||||||
|
self.assertEqual(LazyList(it)[5::-1], it[5::-1])
|
||||||
|
self.assertEqual(LazyList(it)[6:2:-2], it[6:2:-2])
|
||||||
|
self.assertEqual(LazyList(it)[::-1], it[::-1])
|
||||||
|
|
||||||
|
self.assertTrue(LazyList(it))
|
||||||
|
self.assertFalse(LazyList(range(0)))
|
||||||
|
self.assertEqual(len(LazyList(it)), len(it))
|
||||||
|
self.assertEqual(repr(LazyList(it)), repr(it))
|
||||||
|
self.assertEqual(compat_str(LazyList(it)), compat_str(it))
|
||||||
|
|
||||||
|
self.assertEqual(list(LazyList(it, reverse=True)), it[::-1])
|
||||||
|
self.assertEqual(list(reversed(LazyList(it))[::-1]), it)
|
||||||
|
self.assertEqual(list(reversed(LazyList(it))[1:3:7]), it[::-1][1:3:7])
|
||||||
|
|
||||||
|
def test_LazyList_laziness(self):
|
||||||
|
|
||||||
|
def test(ll, idx, val, cache):
|
||||||
|
self.assertEqual(ll[idx], val)
|
||||||
|
self.assertEqual(ll._cache, list(cache))
|
||||||
|
|
||||||
|
ll = LazyList(range(10))
|
||||||
|
test(ll, 0, 0, range(1))
|
||||||
|
test(ll, 5, 5, range(6))
|
||||||
|
test(ll, -3, 7, range(10))
|
||||||
|
|
||||||
|
ll = LazyList(range(10), reverse=True)
|
||||||
|
test(ll, -1, 0, range(1))
|
||||||
|
test(ll, 3, 6, range(10))
|
||||||
|
|
||||||
|
ll = LazyList(itertools.count())
|
||||||
|
test(ll, 10, 10, range(11))
|
||||||
|
ll = reversed(ll)
|
||||||
|
test(ll, -15, 14, range(15))
|
||||||
|
|
||||||
|
def test_try_call(self):
|
||||||
|
def total(*x, **kwargs):
|
||||||
|
return sum(x) + sum(kwargs.values())
|
||||||
|
|
||||||
|
self.assertEqual(try_call(None), None,
|
||||||
|
msg='not a fn should give None')
|
||||||
|
self.assertEqual(try_call(lambda: 1), 1,
|
||||||
|
msg='int fn with no expected_type should give int')
|
||||||
|
self.assertEqual(try_call(lambda: 1, expected_type=int), 1,
|
||||||
|
msg='int fn with expected_type int should give int')
|
||||||
|
self.assertEqual(try_call(lambda: 1, expected_type=dict), None,
|
||||||
|
msg='int fn with wrong expected_type should give None')
|
||||||
|
self.assertEqual(try_call(total, args=(0, 1, 0, ), expected_type=int), 1,
|
||||||
|
msg='fn should accept arglist')
|
||||||
|
self.assertEqual(try_call(total, kwargs={'a': 0, 'b': 1, 'c': 0}, expected_type=int), 1,
|
||||||
|
msg='fn should accept kwargs')
|
||||||
|
self.assertEqual(try_call(lambda: 1, expected_type=dict), None,
|
||||||
|
msg='int fn with no expected_type should give None')
|
||||||
|
self.assertEqual(try_call(lambda x: {}, total, args=(42, ), expected_type=int), 42,
|
||||||
|
msg='expect first int result with expected_type int')
|
||||||
|
|
||||||
|
def test_variadic(self):
|
||||||
|
self.assertEqual(variadic(None), (None, ))
|
||||||
|
self.assertEqual(variadic('spam'), ('spam', ))
|
||||||
|
self.assertEqual(variadic('spam', allowed_types=dict), 'spam')
|
||||||
|
|
||||||
|
def test_traverse_obj(self):
|
||||||
|
_TEST_DATA = {
|
||||||
|
100: 100,
|
||||||
|
1.2: 1.2,
|
||||||
|
'str': 'str',
|
||||||
|
'None': None,
|
||||||
|
'...': Ellipsis,
|
||||||
|
'urls': [
|
||||||
|
{'index': 0, 'url': 'https://www.example.com/0'},
|
||||||
|
{'index': 1, 'url': 'https://www.example.com/1'},
|
||||||
|
],
|
||||||
|
'data': (
|
||||||
|
{'index': 2},
|
||||||
|
{'index': 3},
|
||||||
|
),
|
||||||
|
'dict': {},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test base functionality
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, ('str',)), 'str',
|
||||||
|
msg='allow tuple path')
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, ['str']), 'str',
|
||||||
|
msg='allow list path')
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, (value for value in ("str",))), 'str',
|
||||||
|
msg='allow iterable path')
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, 'str'), 'str',
|
||||||
|
msg='single items should be treated as a path')
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, None), _TEST_DATA)
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, 100), 100)
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, 1.2), 1.2)
|
||||||
|
|
||||||
|
# Test Ellipsis behavior
|
||||||
|
self.assertCountEqual(traverse_obj(_TEST_DATA, Ellipsis),
|
||||||
|
(item for item in _TEST_DATA.values() if item is not None),
|
||||||
|
msg='`...` should give all values except `None`')
|
||||||
|
self.assertCountEqual(traverse_obj(_TEST_DATA, ('urls', 0, Ellipsis)), _TEST_DATA['urls'][0].values(),
|
||||||
|
msg='`...` selection for dicts should select all values')
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, (Ellipsis, Ellipsis, 'url')),
|
||||||
|
['https://www.example.com/0', 'https://www.example.com/1'],
|
||||||
|
msg='nested `...` queries should work')
|
||||||
|
self.assertCountEqual(traverse_obj(_TEST_DATA, (Ellipsis, Ellipsis, 'index')), range(4),
|
||||||
|
msg='`...` query result should be flattened')
|
||||||
|
|
||||||
|
# Test function as key
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, lambda x, y: x == 'urls' and isinstance(y, list)),
|
||||||
|
[_TEST_DATA['urls']],
|
||||||
|
msg='function as query key should perform a filter based on (key, value)')
|
||||||
|
self.assertCountEqual(traverse_obj(_TEST_DATA, lambda _, x: isinstance(x[0], compat_str)), {'str'},
|
||||||
|
msg='exceptions in the query function should be caught')
|
||||||
|
|
||||||
|
# Test alternative paths
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, 'fail', 'str'), 'str',
|
||||||
|
msg='multiple `paths` should be treated as alternative paths')
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, 'str', 100), 'str',
|
||||||
|
msg='alternatives should exit early')
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, 'fail', 'fail'), None,
|
||||||
|
msg='alternatives should return `default` if exhausted')
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, (Ellipsis, 'fail'), 100), 100,
|
||||||
|
msg='alternatives should track their own branching return')
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, ('dict', Ellipsis), ('data', Ellipsis)), list(_TEST_DATA['data']),
|
||||||
|
msg='alternatives on empty objects should search further')
|
||||||
|
|
||||||
|
# Test branch and path nesting
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, ('urls', (3, 0), 'url')), ['https://www.example.com/0'],
|
||||||
|
msg='tuple as key should be treated as branches')
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, ('urls', [3, 0], 'url')), ['https://www.example.com/0'],
|
||||||
|
msg='list as key should be treated as branches')
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, ('urls', ((1, 'fail'), (0, 'url')))), ['https://www.example.com/0'],
|
||||||
|
msg='double nesting in path should be treated as paths')
|
||||||
|
self.assertEqual(traverse_obj(['0', [1, 2]], [(0, 1), 0]), [1],
|
||||||
|
msg='do not fail early on branching')
|
||||||
|
self.assertCountEqual(traverse_obj(_TEST_DATA, ('urls', ((1, ('fail', 'url')), (0, 'url')))),
|
||||||
|
['https://www.example.com/0', 'https://www.example.com/1'],
|
||||||
|
msg='triple nesting in path should be treated as branches')
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, ('urls', ('fail', (Ellipsis, 'url')))),
|
||||||
|
['https://www.example.com/0', 'https://www.example.com/1'],
|
||||||
|
msg='ellipsis as branch path start gets flattened')
|
||||||
|
|
||||||
|
# Test dictionary as key
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, {0: 100, 1: 1.2}), {0: 100, 1: 1.2},
|
||||||
|
msg='dict key should result in a dict with the same keys')
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, {0: ('urls', 0, 'url')}),
|
||||||
|
{0: 'https://www.example.com/0'},
|
||||||
|
msg='dict key should allow paths')
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, {0: ('urls', (3, 0), 'url')}),
|
||||||
|
{0: ['https://www.example.com/0']},
|
||||||
|
msg='tuple in dict path should be treated as branches')
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, {0: ('urls', ((1, 'fail'), (0, 'url')))}),
|
||||||
|
{0: ['https://www.example.com/0']},
|
||||||
|
msg='double nesting in dict path should be treated as paths')
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, {0: ('urls', ((1, ('fail', 'url')), (0, 'url')))}),
|
||||||
|
{0: ['https://www.example.com/1', 'https://www.example.com/0']},
|
||||||
|
msg='triple nesting in dict path should be treated as branches')
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, {0: 'fail'}), {},
|
||||||
|
msg='remove `None` values when dict key')
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, {0: 'fail'}, default=Ellipsis), {0: Ellipsis},
|
||||||
|
msg='do not remove `None` values if `default`')
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, {0: 'dict'}), {0: {}},
|
||||||
|
msg='do not remove empty values when dict key')
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, {0: 'dict'}, default=Ellipsis), {0: {}},
|
||||||
|
msg='do not remove empty values when dict key and a default')
|
||||||
|
self.assertEqual(traverse_obj(_TEST_DATA, {0: ('dict', Ellipsis)}), {0: []},
|
||||||
|
msg='if branch in dict key not successful, return `[]`')
|
||||||
|
|
||||||
|
# Testing default parameter behavior
|
||||||
|
_DEFAULT_DATA = {'None': None, 'int': 0, 'list': []}
|
||||||
|
self.assertEqual(traverse_obj(_DEFAULT_DATA, 'fail'), None,
|
||||||
|
msg='default value should be `None`')
|
||||||
|
self.assertEqual(traverse_obj(_DEFAULT_DATA, 'fail', 'fail', default=Ellipsis), Ellipsis,
|
||||||
|
msg='chained fails should result in default')
|
||||||
|
self.assertEqual(traverse_obj(_DEFAULT_DATA, 'None', 'int'), 0,
|
||||||
|
msg='should not short cirquit on `None`')
|
||||||
|
self.assertEqual(traverse_obj(_DEFAULT_DATA, 'fail', default=1), 1,
|
||||||
|
msg='invalid dict key should result in `default`')
|
||||||
|
self.assertEqual(traverse_obj(_DEFAULT_DATA, 'None', default=1), 1,
|
||||||
|
msg='`None` is a deliberate sentinel and should become `default`')
|
||||||
|
self.assertEqual(traverse_obj(_DEFAULT_DATA, ('list', 10)), None,
|
||||||
|
msg='`IndexError` should result in `default`')
|
||||||
|
self.assertEqual(traverse_obj(_DEFAULT_DATA, (Ellipsis, 'fail'), default=1), 1,
|
||||||
|
msg='if branched but not successful return `default` if defined, not `[]`')
|
||||||
|
self.assertEqual(traverse_obj(_DEFAULT_DATA, (Ellipsis, 'fail'), default=None), None,
|
||||||
|
msg='if branched but not successful return `default` even if `default` is `None`')
|
||||||
|
self.assertEqual(traverse_obj(_DEFAULT_DATA, (Ellipsis, 'fail')), [],
|
||||||
|
msg='if branched but not successful return `[]`, not `default`')
|
||||||
|
self.assertEqual(traverse_obj(_DEFAULT_DATA, ('list', Ellipsis)), [],
|
||||||
|
msg='if branched but object is empty return `[]`, not `default`')
|
||||||
|
|
||||||
|
# Testing expected_type behavior
|
||||||
|
_EXPECTED_TYPE_DATA = {'str': 'str', 'int': 0}
|
||||||
|
self.assertEqual(traverse_obj(_EXPECTED_TYPE_DATA, 'str', expected_type=compat_str), 'str',
|
||||||
|
msg='accept matching `expected_type` type')
|
||||||
|
self.assertEqual(traverse_obj(_EXPECTED_TYPE_DATA, 'str', expected_type=int), None,
|
||||||
|
msg='reject non matching `expected_type` type')
|
||||||
|
self.assertEqual(traverse_obj(_EXPECTED_TYPE_DATA, 'int', expected_type=lambda x: compat_str(x)), '0',
|
||||||
|
msg='transform type using type function')
|
||||||
|
self.assertEqual(traverse_obj(_EXPECTED_TYPE_DATA, 'str',
|
||||||
|
expected_type=lambda _: 1 / 0), None,
|
||||||
|
msg='wrap expected_type function in try_call')
|
||||||
|
self.assertEqual(traverse_obj(_EXPECTED_TYPE_DATA, Ellipsis, expected_type=compat_str), ['str'],
|
||||||
|
msg='eliminate items that expected_type fails on')
|
||||||
|
|
||||||
|
# Test get_all behavior
|
||||||
|
_GET_ALL_DATA = {'key': [0, 1, 2]}
|
||||||
|
self.assertEqual(traverse_obj(_GET_ALL_DATA, ('key', Ellipsis), get_all=False), 0,
|
||||||
|
msg='if not `get_all`, return only first matching value')
|
||||||
|
self.assertEqual(traverse_obj(_GET_ALL_DATA, Ellipsis, get_all=False), [0, 1, 2],
|
||||||
|
msg='do not overflatten if not `get_all`')
|
||||||
|
|
||||||
|
# Test casesense behavior
|
||||||
|
_CASESENSE_DATA = {
|
||||||
|
'KeY': 'value0',
|
||||||
|
0: {
|
||||||
|
'KeY': 'value1',
|
||||||
|
0: {'KeY': 'value2'},
|
||||||
|
},
|
||||||
|
# FULLWIDTH LATIN CAPITAL LETTER K
|
||||||
|
'\uff2bey': 'value3',
|
||||||
|
}
|
||||||
|
self.assertEqual(traverse_obj(_CASESENSE_DATA, 'key'), None,
|
||||||
|
msg='dict keys should be case sensitive unless `casesense`')
|
||||||
|
self.assertEqual(traverse_obj(_CASESENSE_DATA, 'keY',
|
||||||
|
casesense=False), 'value0',
|
||||||
|
msg='allow non matching key case if `casesense`')
|
||||||
|
self.assertEqual(traverse_obj(_CASESENSE_DATA, '\uff4bey', # FULLWIDTH LATIN SMALL LETTER K
|
||||||
|
casesense=False), 'value3',
|
||||||
|
msg='allow non matching Unicode key case if `casesense`')
|
||||||
|
self.assertEqual(traverse_obj(_CASESENSE_DATA, (0, ('keY',)),
|
||||||
|
casesense=False), ['value1'],
|
||||||
|
msg='allow non matching key case in branch if `casesense`')
|
||||||
|
self.assertEqual(traverse_obj(_CASESENSE_DATA, (0, ((0, 'keY'),)),
|
||||||
|
casesense=False), ['value2'],
|
||||||
|
msg='allow non matching key case in branch path if `casesense`')
|
||||||
|
|
||||||
|
# Test traverse_string behavior
|
||||||
|
_TRAVERSE_STRING_DATA = {'str': 'str', 1.2: 1.2}
|
||||||
|
self.assertEqual(traverse_obj(_TRAVERSE_STRING_DATA, ('str', 0)), None,
|
||||||
|
msg='do not traverse into string if not `traverse_string`')
|
||||||
|
self.assertEqual(traverse_obj(_TRAVERSE_STRING_DATA, ('str', 0),
|
||||||
|
_traverse_string=True), 's',
|
||||||
|
msg='traverse into string if `traverse_string`')
|
||||||
|
self.assertEqual(traverse_obj(_TRAVERSE_STRING_DATA, (1.2, 1),
|
||||||
|
_traverse_string=True), '.',
|
||||||
|
msg='traverse into converted data if `traverse_string`')
|
||||||
|
self.assertEqual(traverse_obj(_TRAVERSE_STRING_DATA, ('str', Ellipsis),
|
||||||
|
_traverse_string=True), list('str'),
|
||||||
|
msg='`...` branching into string should result in list')
|
||||||
|
self.assertEqual(traverse_obj(_TRAVERSE_STRING_DATA, ('str', (0, 2)),
|
||||||
|
_traverse_string=True), ['s', 'r'],
|
||||||
|
msg='branching into string should result in list')
|
||||||
|
self.assertEqual(traverse_obj(_TRAVERSE_STRING_DATA, ('str', lambda _, x: x),
|
||||||
|
_traverse_string=True), list('str'),
|
||||||
|
msg='function branching into string should result in list')
|
||||||
|
|
||||||
|
# Test is_user_input behavior
|
||||||
|
_IS_USER_INPUT_DATA = {'range8': list(range(8))}
|
||||||
|
self.assertEqual(traverse_obj(_IS_USER_INPUT_DATA, ('range8', '3'),
|
||||||
|
_is_user_input=True), 3,
|
||||||
|
msg='allow for string indexing if `is_user_input`')
|
||||||
|
self.assertCountEqual(traverse_obj(_IS_USER_INPUT_DATA, ('range8', '3:'),
|
||||||
|
_is_user_input=True), tuple(range(8))[3:],
|
||||||
|
msg='allow for string slice if `is_user_input`')
|
||||||
|
self.assertCountEqual(traverse_obj(_IS_USER_INPUT_DATA, ('range8', ':4:2'),
|
||||||
|
_is_user_input=True), tuple(range(8))[:4:2],
|
||||||
|
msg='allow step in string slice if `is_user_input`')
|
||||||
|
self.assertCountEqual(traverse_obj(_IS_USER_INPUT_DATA, ('range8', ':'),
|
||||||
|
_is_user_input=True), range(8),
|
||||||
|
msg='`:` should be treated as `...` if `is_user_input`')
|
||||||
|
with self.assertRaises(TypeError, msg='too many params should result in error'):
|
||||||
|
traverse_obj(_IS_USER_INPUT_DATA, ('range8', ':::'), _is_user_input=True)
|
||||||
|
|
||||||
|
# Test re.Match as input obj
|
||||||
|
mobj = re.match(r'^0(12)(?P<group>3)(4)?$', '0123')
|
||||||
|
self.assertEqual(traverse_obj(mobj, Ellipsis), [x for x in mobj.groups() if x is not None],
|
||||||
|
msg='`...` on a `re.Match` should give its `groups()`')
|
||||||
|
self.assertEqual(traverse_obj(mobj, lambda k, _: k in (0, 2)), ['0123', '3'],
|
||||||
|
msg='function on a `re.Match` should give groupno, value starting at 0')
|
||||||
|
self.assertEqual(traverse_obj(mobj, 'group'), '3',
|
||||||
|
msg='str key on a `re.Match` should give group with that name')
|
||||||
|
self.assertEqual(traverse_obj(mobj, 2), '3',
|
||||||
|
msg='int key on a `re.Match` should give group with that name')
|
||||||
|
self.assertEqual(traverse_obj(mobj, 'gRoUp', casesense=False), '3',
|
||||||
|
msg='str key on a `re.Match` should respect casesense')
|
||||||
|
self.assertEqual(traverse_obj(mobj, 'fail'), None,
|
||||||
|
msg='failing str key on a `re.Match` should return `default`')
|
||||||
|
self.assertEqual(traverse_obj(mobj, 'gRoUpS', casesense=False), None,
|
||||||
|
msg='failing str key on a `re.Match` should return `default`')
|
||||||
|
self.assertEqual(traverse_obj(mobj, 8), None,
|
||||||
|
msg='failing int key on a `re.Match` should return `default`')
|
||||||
|
|
||||||
|
def test_get_first(self):
|
||||||
|
self.assertEqual(get_first([{'a': None}, {'a': 'spam'}], 'a'), 'spam')
|
||||||
|
|
||||||
|
def test_join_nonempty(self):
|
||||||
|
self.assertEqual(join_nonempty('a', 'b'), 'a-b')
|
||||||
|
self.assertEqual(join_nonempty(
|
||||||
|
'a', 'b', 'c', 'd',
|
||||||
|
from_dict={'a': 'c', 'c': [], 'b': 'd', 'd': None}), 'c-d')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -1,275 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# coding: utf-8
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
# Allow direct execution
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import unittest
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
from test.helper import expect_value
|
|
||||||
from youtube_dl.extractor import YoutubeIE
|
|
||||||
|
|
||||||
|
|
||||||
class TestYoutubeChapters(unittest.TestCase):
|
|
||||||
|
|
||||||
_TEST_CASES = [
|
|
||||||
(
|
|
||||||
# https://www.youtube.com/watch?v=A22oy8dFjqc
|
|
||||||
# pattern: 00:00 - <title>
|
|
||||||
'''This is the absolute ULTIMATE experience of Queen's set at LIVE AID, this is the best video mixed to the absolutely superior stereo radio broadcast. This vastly superior audio mix takes a huge dump on all of the official mixes. Best viewed in 1080p. ENJOY! ***MAKE SURE TO READ THE DESCRIPTION***<br /><a href="#" onclick="yt.www.watch.player.seekTo(00*60+36);return false;">00:36</a> - Bohemian Rhapsody<br /><a href="#" onclick="yt.www.watch.player.seekTo(02*60+42);return false;">02:42</a> - Radio Ga Ga<br /><a href="#" onclick="yt.www.watch.player.seekTo(06*60+53);return false;">06:53</a> - Ay Oh!<br /><a href="#" onclick="yt.www.watch.player.seekTo(07*60+34);return false;">07:34</a> - Hammer To Fall<br /><a href="#" onclick="yt.www.watch.player.seekTo(12*60+08);return false;">12:08</a> - Crazy Little Thing Called Love<br /><a href="#" onclick="yt.www.watch.player.seekTo(16*60+03);return false;">16:03</a> - We Will Rock You<br /><a href="#" onclick="yt.www.watch.player.seekTo(17*60+18);return false;">17:18</a> - We Are The Champions<br /><a href="#" onclick="yt.www.watch.player.seekTo(21*60+12);return false;">21:12</a> - Is This The World We Created...?<br /><br />Short song analysis:<br /><br />- "Bohemian Rhapsody": Although it's a short medley version, it's one of the best performances of the ballad section, with Freddie nailing the Bb4s with the correct studio phrasing (for the first time ever!).<br /><br />- "Radio Ga Ga": Although it's missing one chorus, this is one of - if not the best - the best versions ever, Freddie nails all the Bb4s and sounds very clean! Spike Edney's Roland Jupiter 8 also really shines through on this mix, compared to the DVD releases!<br /><br />- "Audience Improv": A great improv, Freddie sounds strong and confident. You gotta love when he sustains that A4 for 4 seconds!<br /><br />- "Hammer To Fall": Despite missing a verse and a chorus, it's a strong version (possibly the best ever). Freddie sings the song amazingly, and even ad-libs a C#5 and a C5! Also notice how heavy Brian's guitar sounds compared to the thin DVD mixes - it roars!<br /><br />- "Crazy Little Thing Called Love": A great version, the crowd loves the song, the jam is great as well! Only downside to this is the slight feedback issues.<br /><br />- "We Will Rock You": Although cut down to the 1st verse and chorus, Freddie sounds strong. He nails the A4, and the solo from Dr. May is brilliant!<br /><br />- "We Are the Champions": Perhaps the high-light of the performance - Freddie is very daring on this version, he sustains the pre-chorus Bb4s, nails the 1st C5, belts great A4s, but most importantly: He nails the chorus Bb4s, in all 3 choruses! This is the only time he has ever done so! It has to be said though, the last one sounds a bit rough, but that's a side effect of belting high notes for the past 18 minutes, with nodules AND laryngitis!<br /><br />- "Is This The World We Created... ?": Freddie and Brian perform a beautiful version of this, and it is one of the best versions ever. It's both sad and hilarious that a couple of BBC engineers are talking over the song, one of them being completely oblivious of the fact that he is interrupting the performance, on live television... Which was being televised to almost 2 billion homes.<br /><br /><br />All rights go to their respective owners!<br />-----Copyright Disclaimer Under Section 107 of the Copyright Act 1976, allowance is made for fair use for purposes such as criticism, comment, news reporting, teaching, scholarship, and research. Fair use is a use permitted by copyright statute that might otherwise be infringing. Non-profit, educational or personal use tips the balance in favor of fair use''',
|
|
||||||
1477,
|
|
||||||
[{
|
|
||||||
'start_time': 36,
|
|
||||||
'end_time': 162,
|
|
||||||
'title': 'Bohemian Rhapsody',
|
|
||||||
}, {
|
|
||||||
'start_time': 162,
|
|
||||||
'end_time': 413,
|
|
||||||
'title': 'Radio Ga Ga',
|
|
||||||
}, {
|
|
||||||
'start_time': 413,
|
|
||||||
'end_time': 454,
|
|
||||||
'title': 'Ay Oh!',
|
|
||||||
}, {
|
|
||||||
'start_time': 454,
|
|
||||||
'end_time': 728,
|
|
||||||
'title': 'Hammer To Fall',
|
|
||||||
}, {
|
|
||||||
'start_time': 728,
|
|
||||||
'end_time': 963,
|
|
||||||
'title': 'Crazy Little Thing Called Love',
|
|
||||||
}, {
|
|
||||||
'start_time': 963,
|
|
||||||
'end_time': 1038,
|
|
||||||
'title': 'We Will Rock You',
|
|
||||||
}, {
|
|
||||||
'start_time': 1038,
|
|
||||||
'end_time': 1272,
|
|
||||||
'title': 'We Are The Champions',
|
|
||||||
}, {
|
|
||||||
'start_time': 1272,
|
|
||||||
'end_time': 1477,
|
|
||||||
'title': 'Is This The World We Created...?',
|
|
||||||
}]
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# https://www.youtube.com/watch?v=ekYlRhALiRQ
|
|
||||||
# pattern: <num>. <title> 0:00
|
|
||||||
'1. Those Beaten Paths of Confusion <a href="#" onclick="yt.www.watch.player.seekTo(0*60+00);return false;">0:00</a><br />2. Beyond the Shadows of Emptiness & Nothingness <a href="#" onclick="yt.www.watch.player.seekTo(11*60+47);return false;">11:47</a><br />3. Poison Yourself...With Thought <a href="#" onclick="yt.www.watch.player.seekTo(26*60+30);return false;">26:30</a><br />4. The Agents of Transformation <a href="#" onclick="yt.www.watch.player.seekTo(35*60+57);return false;">35:57</a><br />5. Drowning in the Pain of Consciousness <a href="#" onclick="yt.www.watch.player.seekTo(44*60+32);return false;">44:32</a><br />6. Deny the Disease of Life <a href="#" onclick="yt.www.watch.player.seekTo(53*60+07);return false;">53:07</a><br /><br />More info/Buy: http://crepusculonegro.storenvy.com/products/257645-cn-03-arizmenda-within-the-vacuum-of-infinity<br /><br />No copyright is intended. The rights to this video are assumed by the owner and its affiliates.',
|
|
||||||
4009,
|
|
||||||
[{
|
|
||||||
'start_time': 0,
|
|
||||||
'end_time': 707,
|
|
||||||
'title': '1. Those Beaten Paths of Confusion',
|
|
||||||
}, {
|
|
||||||
'start_time': 707,
|
|
||||||
'end_time': 1590,
|
|
||||||
'title': '2. Beyond the Shadows of Emptiness & Nothingness',
|
|
||||||
}, {
|
|
||||||
'start_time': 1590,
|
|
||||||
'end_time': 2157,
|
|
||||||
'title': '3. Poison Yourself...With Thought',
|
|
||||||
}, {
|
|
||||||
'start_time': 2157,
|
|
||||||
'end_time': 2672,
|
|
||||||
'title': '4. The Agents of Transformation',
|
|
||||||
}, {
|
|
||||||
'start_time': 2672,
|
|
||||||
'end_time': 3187,
|
|
||||||
'title': '5. Drowning in the Pain of Consciousness',
|
|
||||||
}, {
|
|
||||||
'start_time': 3187,
|
|
||||||
'end_time': 4009,
|
|
||||||
'title': '6. Deny the Disease of Life',
|
|
||||||
}]
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# https://www.youtube.com/watch?v=WjL4pSzog9w
|
|
||||||
# pattern: 00:00 <title>
|
|
||||||
'<a href="https://arizmenda.bandcamp.com/merch/despairs-depths-descended-cd" class="yt-uix-servicelink " data-target-new-window="True" data-servicelink="CDAQ6TgYACITCNf1raqT2dMCFdRjGAod_o0CBSj4HQ" data-url="https://arizmenda.bandcamp.com/merch/despairs-depths-descended-cd" rel="nofollow noopener" target="_blank">https://arizmenda.bandcamp.com/merch/...</a><br /><br /><a href="#" onclick="yt.www.watch.player.seekTo(00*60+00);return false;">00:00</a> Christening Unborn Deformities <br /><a href="#" onclick="yt.www.watch.player.seekTo(07*60+08);return false;">07:08</a> Taste of Purity<br /><a href="#" onclick="yt.www.watch.player.seekTo(16*60+16);return false;">16:16</a> Sculpting Sins of a Universal Tongue<br /><a href="#" onclick="yt.www.watch.player.seekTo(24*60+45);return false;">24:45</a> Birth<br /><a href="#" onclick="yt.www.watch.player.seekTo(31*60+24);return false;">31:24</a> Neves<br /><a href="#" onclick="yt.www.watch.player.seekTo(37*60+55);return false;">37:55</a> Libations in Limbo',
|
|
||||||
2705,
|
|
||||||
[{
|
|
||||||
'start_time': 0,
|
|
||||||
'end_time': 428,
|
|
||||||
'title': 'Christening Unborn Deformities',
|
|
||||||
}, {
|
|
||||||
'start_time': 428,
|
|
||||||
'end_time': 976,
|
|
||||||
'title': 'Taste of Purity',
|
|
||||||
}, {
|
|
||||||
'start_time': 976,
|
|
||||||
'end_time': 1485,
|
|
||||||
'title': 'Sculpting Sins of a Universal Tongue',
|
|
||||||
}, {
|
|
||||||
'start_time': 1485,
|
|
||||||
'end_time': 1884,
|
|
||||||
'title': 'Birth',
|
|
||||||
}, {
|
|
||||||
'start_time': 1884,
|
|
||||||
'end_time': 2275,
|
|
||||||
'title': 'Neves',
|
|
||||||
}, {
|
|
||||||
'start_time': 2275,
|
|
||||||
'end_time': 2705,
|
|
||||||
'title': 'Libations in Limbo',
|
|
||||||
}]
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# https://www.youtube.com/watch?v=o3r1sn-t3is
|
|
||||||
# pattern: <title> 00:00 <note>
|
|
||||||
'Download this show in MP3: <a href="http://sh.st/njZKK" class="yt-uix-servicelink " data-url="http://sh.st/njZKK" data-target-new-window="True" data-servicelink="CDAQ6TgYACITCK3j8_6o2dMCFVDCGAoduVAKKij4HQ" rel="nofollow noopener" target="_blank">http://sh.st/njZKK</a><br /><br />Setlist:<br />I-E-A-I-A-I-O <a href="#" onclick="yt.www.watch.player.seekTo(00*60+45);return false;">00:45</a><br />Suite-Pee <a href="#" onclick="yt.www.watch.player.seekTo(4*60+26);return false;">4:26</a> (Incomplete)<br />Attack <a href="#" onclick="yt.www.watch.player.seekTo(5*60+31);return false;">5:31</a> (First live performance since 2011)<br />Prison Song <a href="#" onclick="yt.www.watch.player.seekTo(8*60+42);return false;">8:42</a><br />Know <a href="#" onclick="yt.www.watch.player.seekTo(12*60+32);return false;">12:32</a> (First live performance since 2011)<br />Aerials <a href="#" onclick="yt.www.watch.player.seekTo(15*60+32);return false;">15:32</a><br />Soldier Side - Intro <a href="#" onclick="yt.www.watch.player.seekTo(19*60+13);return false;">19:13</a><br />B.Y.O.B. <a href="#" onclick="yt.www.watch.player.seekTo(20*60+09);return false;">20:09</a><br />Soil <a href="#" onclick="yt.www.watch.player.seekTo(24*60+32);return false;">24:32</a><br />Darts <a href="#" onclick="yt.www.watch.player.seekTo(27*60+48);return false;">27:48</a><br />Radio/Video <a href="#" onclick="yt.www.watch.player.seekTo(30*60+38);return false;">30:38</a><br />Hypnotize <a href="#" onclick="yt.www.watch.player.seekTo(35*60+05);return false;">35:05</a><br />Temper <a href="#" onclick="yt.www.watch.player.seekTo(38*60+08);return false;">38:08</a> (First live performance since 1999)<br />CUBErt <a href="#" onclick="yt.www.watch.player.seekTo(41*60+00);return false;">41:00</a><br />Needles <a href="#" onclick="yt.www.watch.player.seekTo(42*60+57);return false;">42:57</a><br />Deer Dance <a href="#" onclick="yt.www.watch.player.seekTo(46*60+27);return false;">46:27</a><br />Bounce <a href="#" onclick="yt.www.watch.player.seekTo(49*60+38);return false;">49:38</a><br />Suggestions <a href="#" onclick="yt.www.watch.player.seekTo(51*60+25);return false;">51:25</a><br />Psycho <a href="#" onclick="yt.www.watch.player.seekTo(53*60+52);return false;">53:52</a><br />Chop Suey! <a href="#" onclick="yt.www.watch.player.seekTo(58*60+13);return false;">58:13</a><br />Lonely Day <a href="#" onclick="yt.www.watch.player.seekTo(1*3600+01*60+15);return false;">1:01:15</a><br />Question! <a href="#" onclick="yt.www.watch.player.seekTo(1*3600+04*60+14);return false;">1:04:14</a><br />Lost in Hollywood <a href="#" onclick="yt.www.watch.player.seekTo(1*3600+08*60+10);return false;">1:08:10</a><br />Vicinity of Obscenity <a href="#" onclick="yt.www.watch.player.seekTo(1*3600+13*60+40);return false;">1:13:40</a>(First live performance since 2012)<br />Forest <a href="#" onclick="yt.www.watch.player.seekTo(1*3600+16*60+17);return false;">1:16:17</a><br />Cigaro <a href="#" onclick="yt.www.watch.player.seekTo(1*3600+20*60+02);return false;">1:20:02</a><br />Toxicity <a href="#" onclick="yt.www.watch.player.seekTo(1*3600+23*60+57);return false;">1:23:57</a>(with Chino Moreno)<br />Sugar <a href="#" onclick="yt.www.watch.player.seekTo(1*3600+27*60+53);return false;">1:27:53</a>',
|
|
||||||
5640,
|
|
||||||
[{
|
|
||||||
'start_time': 45,
|
|
||||||
'end_time': 266,
|
|
||||||
'title': 'I-E-A-I-A-I-O',
|
|
||||||
}, {
|
|
||||||
'start_time': 266,
|
|
||||||
'end_time': 331,
|
|
||||||
'title': 'Suite-Pee (Incomplete)',
|
|
||||||
}, {
|
|
||||||
'start_time': 331,
|
|
||||||
'end_time': 522,
|
|
||||||
'title': 'Attack (First live performance since 2011)',
|
|
||||||
}, {
|
|
||||||
'start_time': 522,
|
|
||||||
'end_time': 752,
|
|
||||||
'title': 'Prison Song',
|
|
||||||
}, {
|
|
||||||
'start_time': 752,
|
|
||||||
'end_time': 932,
|
|
||||||
'title': 'Know (First live performance since 2011)',
|
|
||||||
}, {
|
|
||||||
'start_time': 932,
|
|
||||||
'end_time': 1153,
|
|
||||||
'title': 'Aerials',
|
|
||||||
}, {
|
|
||||||
'start_time': 1153,
|
|
||||||
'end_time': 1209,
|
|
||||||
'title': 'Soldier Side - Intro',
|
|
||||||
}, {
|
|
||||||
'start_time': 1209,
|
|
||||||
'end_time': 1472,
|
|
||||||
'title': 'B.Y.O.B.',
|
|
||||||
}, {
|
|
||||||
'start_time': 1472,
|
|
||||||
'end_time': 1668,
|
|
||||||
'title': 'Soil',
|
|
||||||
}, {
|
|
||||||
'start_time': 1668,
|
|
||||||
'end_time': 1838,
|
|
||||||
'title': 'Darts',
|
|
||||||
}, {
|
|
||||||
'start_time': 1838,
|
|
||||||
'end_time': 2105,
|
|
||||||
'title': 'Radio/Video',
|
|
||||||
}, {
|
|
||||||
'start_time': 2105,
|
|
||||||
'end_time': 2288,
|
|
||||||
'title': 'Hypnotize',
|
|
||||||
}, {
|
|
||||||
'start_time': 2288,
|
|
||||||
'end_time': 2460,
|
|
||||||
'title': 'Temper (First live performance since 1999)',
|
|
||||||
}, {
|
|
||||||
'start_time': 2460,
|
|
||||||
'end_time': 2577,
|
|
||||||
'title': 'CUBErt',
|
|
||||||
}, {
|
|
||||||
'start_time': 2577,
|
|
||||||
'end_time': 2787,
|
|
||||||
'title': 'Needles',
|
|
||||||
}, {
|
|
||||||
'start_time': 2787,
|
|
||||||
'end_time': 2978,
|
|
||||||
'title': 'Deer Dance',
|
|
||||||
}, {
|
|
||||||
'start_time': 2978,
|
|
||||||
'end_time': 3085,
|
|
||||||
'title': 'Bounce',
|
|
||||||
}, {
|
|
||||||
'start_time': 3085,
|
|
||||||
'end_time': 3232,
|
|
||||||
'title': 'Suggestions',
|
|
||||||
}, {
|
|
||||||
'start_time': 3232,
|
|
||||||
'end_time': 3493,
|
|
||||||
'title': 'Psycho',
|
|
||||||
}, {
|
|
||||||
'start_time': 3493,
|
|
||||||
'end_time': 3675,
|
|
||||||
'title': 'Chop Suey!',
|
|
||||||
}, {
|
|
||||||
'start_time': 3675,
|
|
||||||
'end_time': 3854,
|
|
||||||
'title': 'Lonely Day',
|
|
||||||
}, {
|
|
||||||
'start_time': 3854,
|
|
||||||
'end_time': 4090,
|
|
||||||
'title': 'Question!',
|
|
||||||
}, {
|
|
||||||
'start_time': 4090,
|
|
||||||
'end_time': 4420,
|
|
||||||
'title': 'Lost in Hollywood',
|
|
||||||
}, {
|
|
||||||
'start_time': 4420,
|
|
||||||
'end_time': 4577,
|
|
||||||
'title': 'Vicinity of Obscenity (First live performance since 2012)',
|
|
||||||
}, {
|
|
||||||
'start_time': 4577,
|
|
||||||
'end_time': 4802,
|
|
||||||
'title': 'Forest',
|
|
||||||
}, {
|
|
||||||
'start_time': 4802,
|
|
||||||
'end_time': 5037,
|
|
||||||
'title': 'Cigaro',
|
|
||||||
}, {
|
|
||||||
'start_time': 5037,
|
|
||||||
'end_time': 5273,
|
|
||||||
'title': 'Toxicity (with Chino Moreno)',
|
|
||||||
}, {
|
|
||||||
'start_time': 5273,
|
|
||||||
'end_time': 5640,
|
|
||||||
'title': 'Sugar',
|
|
||||||
}]
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# https://www.youtube.com/watch?v=PkYLQbsqCE8
|
|
||||||
# pattern: <num> - <title> [<latinized title>] 0:00:00
|
|
||||||
'''Затемно (Zatemno) is an Obscure Black Metal Band from Russia.<br /><br />"Во прах (Vo prakh)'' Into The Ashes", Debut mini-album released may 6, 2016, by Death Knell Productions<br />Released on 6 panel digipak CD, limited to 100 copies only<br />And digital format on Bandcamp<br /><br />Tracklist<br /><br />1 - Во прах [Vo prakh] <a href="#" onclick="yt.www.watch.player.seekTo(0*3600+00*60+00);return false;">0:00:00</a><br />2 - Искупление [Iskupleniye] <a href="#" onclick="yt.www.watch.player.seekTo(0*3600+08*60+10);return false;">0:08:10</a><br />3 - Из серпов луны...[Iz serpov luny] <a href="#" onclick="yt.www.watch.player.seekTo(0*3600+14*60+30);return false;">0:14:30</a><br /><br />Links:<br /><a href="https://deathknellprod.bandcamp.com/album/--2" class="yt-uix-servicelink " data-target-new-window="True" data-url="https://deathknellprod.bandcamp.com/album/--2" data-servicelink="CC8Q6TgYACITCNP234Kr2dMCFcNxGAodQqsIwSj4HQ" target="_blank" rel="nofollow noopener">https://deathknellprod.bandcamp.com/a...</a><br /><a href="https://www.facebook.com/DeathKnellProd/" class="yt-uix-servicelink " data-target-new-window="True" data-url="https://www.facebook.com/DeathKnellProd/" data-servicelink="CC8Q6TgYACITCNP234Kr2dMCFcNxGAodQqsIwSj4HQ" target="_blank" rel="nofollow noopener">https://www.facebook.com/DeathKnellProd/</a><br /><br /><br />I don't have any right about this artifact, my only intention is to spread the music of the band, all rights are reserved to the Затемно (Zatemno) and his producers, Death Knell Productions.<br /><br />------------------------------------------------------------------<br /><br />Subscribe for more videos like this.<br />My link: <a href="https://web.facebook.com/AttackOfTheDragons" class="yt-uix-servicelink " data-target-new-window="True" data-url="https://web.facebook.com/AttackOfTheDragons" data-servicelink="CC8Q6TgYACITCNP234Kr2dMCFcNxGAodQqsIwSj4HQ" target="_blank" rel="nofollow noopener">https://web.facebook.com/AttackOfTheD...</a>''',
|
|
||||||
1138,
|
|
||||||
[{
|
|
||||||
'start_time': 0,
|
|
||||||
'end_time': 490,
|
|
||||||
'title': '1 - Во прах [Vo prakh]',
|
|
||||||
}, {
|
|
||||||
'start_time': 490,
|
|
||||||
'end_time': 870,
|
|
||||||
'title': '2 - Искупление [Iskupleniye]',
|
|
||||||
}, {
|
|
||||||
'start_time': 870,
|
|
||||||
'end_time': 1138,
|
|
||||||
'title': '3 - Из серпов луны...[Iz serpov luny]',
|
|
||||||
}]
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# https://www.youtube.com/watch?v=xZW70zEasOk
|
|
||||||
# time point more than duration
|
|
||||||
'''● LCS Spring finals: Saturday and Sunday from <a href="#" onclick="yt.www.watch.player.seekTo(13*60+30);return false;">13:30</a> outside the venue! <br />● PAX East: Fri, Sat & Sun - more info in tomorrows video on the main channel!''',
|
|
||||||
283,
|
|
||||||
[]
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
def test_youtube_chapters(self):
|
|
||||||
for description, duration, expected_chapters in self._TEST_CASES:
|
|
||||||
ie = YoutubeIE()
|
|
||||||
expect_value(
|
|
||||||
self, ie._extract_chapters(description, duration),
|
|
||||||
expected_chapters, None)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
# Allow direct execution
|
# Allow direct execution
|
||||||
@@ -9,10 +10,10 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|||||||
|
|
||||||
from test.helper import FakeYDL
|
from test.helper import FakeYDL
|
||||||
|
|
||||||
|
|
||||||
from youtube_dl.extractor import (
|
from youtube_dl.extractor import (
|
||||||
YoutubePlaylistIE,
|
|
||||||
YoutubeIE,
|
YoutubeIE,
|
||||||
|
YoutubePlaylistIE,
|
||||||
|
YoutubeTabIE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -24,47 +25,40 @@ 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
|
||||||
|
dl.params['format'] = 'best'
|
||||||
ie = YoutubePlaylistIE(dl)
|
ie = YoutubePlaylistIE(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')
|
||||||
|
result = dl.extract_info(result['url'], download=False, ie_key=result.get('ie_key'), process=False)
|
||||||
self.assertEqual(YoutubeIE().extract_id(result['url']), 'FXxLjLQi3Fg')
|
self.assertEqual(YoutubeIE().extract_id(result['url']), 'FXxLjLQi3Fg')
|
||||||
|
|
||||||
def test_youtube_course(self):
|
|
||||||
dl = FakeYDL()
|
|
||||||
ie = YoutubePlaylistIE(dl)
|
|
||||||
# TODO find a > 100 (paginating?) videos course
|
|
||||||
result = ie.extract('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')
|
|
||||||
entries = list(result['entries'])
|
|
||||||
self.assertEqual(YoutubeIE().extract_id(entries[0]['url']), 'j9WZyLZCBzs')
|
|
||||||
self.assertEqual(len(entries), 25)
|
|
||||||
self.assertEqual(YoutubeIE().extract_id(entries[-1]['url']), 'rYefUsYuEp0')
|
|
||||||
|
|
||||||
def test_youtube_mix(self):
|
def test_youtube_mix(self):
|
||||||
dl = FakeYDL()
|
dl = FakeYDL()
|
||||||
ie = YoutubePlaylistIE(dl)
|
dl.params['format'] = 'best'
|
||||||
result = ie.extract('https://www.youtube.com/watch?v=W01L70IGBgE&index=2&list=RDOQpdSVF_k_w')
|
ie = YoutubeTabIE(dl)
|
||||||
entries = result['entries']
|
result = dl.extract_info('https://www.youtube.com/watch?v=tyITL_exICo&list=RDCLAK5uy_kLWIr9gv1XLlPbaDS965-Db4TrBoUTxQ8',
|
||||||
self.assertTrue(len(entries) >= 50)
|
download=False, ie_key=ie.ie_key(), process=True)
|
||||||
|
entries = (result or {}).get('entries', [{'id': 'not_found', }])
|
||||||
|
self.assertTrue(len(entries) >= 25)
|
||||||
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_flat_playlist_extraction(self):
|
||||||
print('Skipping: The playlist page gives error 500')
|
|
||||||
return
|
|
||||||
dl = FakeYDL()
|
|
||||||
ie = YoutubePlaylistIE(dl)
|
|
||||||
result = ie.extract('https://www.youtube.com/playlist?list=MCUS')
|
|
||||||
entries = result['entries']
|
|
||||||
self.assertEqual(len(entries), 100)
|
|
||||||
|
|
||||||
def test_youtube_flat_playlist_titles(self):
|
|
||||||
dl = FakeYDL()
|
dl = FakeYDL()
|
||||||
dl.params['extract_flat'] = True
|
dl.params['extract_flat'] = True
|
||||||
ie = YoutubePlaylistIE(dl)
|
ie = YoutubeTabIE(dl)
|
||||||
result = ie.extract('https://www.youtube.com/playlist?list=PL-KKIb8rvtMSrAO9YFbeM6UQrAqoFTUWv')
|
result = ie.extract('https://www.youtube.com/playlist?list=PL4lCao7KL_QFVb7Iudeipvc2BCavECqzc')
|
||||||
self.assertIsPlaylist(result)
|
self.assertIsPlaylist(result)
|
||||||
for entry in result['entries']:
|
entries = list(result['entries'])
|
||||||
self.assertTrue(entry.get('title'))
|
self.assertTrue(len(entries) == 1)
|
||||||
|
video = entries[0]
|
||||||
|
self.assertEqual(video['_type'], 'url')
|
||||||
|
self.assertEqual(video['ie_key'], 'Youtube')
|
||||||
|
self.assertEqual(video['id'], 'BaW_jenozKc')
|
||||||
|
self.assertEqual(video['url'], 'BaW_jenozKc')
|
||||||
|
self.assertEqual(video['title'], 'youtube-dl test video "\'/\\ä↭𝕐')
|
||||||
|
self.assertEqual(video['duration'], 10)
|
||||||
|
self.assertEqual(video['uploader'], 'Philipp Hagemeister')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
26
test/test_youtube_misc.py
Normal file
26
test/test_youtube_misc.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
# Allow direct execution
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
|
||||||
|
from youtube_dl.extractor import YoutubeIE
|
||||||
|
|
||||||
|
|
||||||
|
class TestYoutubeMisc(unittest.TestCase):
|
||||||
|
def test_youtube_extract(self):
|
||||||
|
assertExtractId = lambda url, id: self.assertEqual(YoutubeIE.extract_id(url), id)
|
||||||
|
assertExtractId('http://www.youtube.com/watch?&v=BaW_jenozKc', 'BaW_jenozKc')
|
||||||
|
assertExtractId('https://www.youtube.com/watch?&v=BaW_jenozKc', 'BaW_jenozKc')
|
||||||
|
assertExtractId('https://www.youtube.com/watch?feature=player_embedded&v=BaW_jenozKc', 'BaW_jenozKc')
|
||||||
|
assertExtractId('https://www.youtube.com/watch_popup?v=BaW_jenozKc', 'BaW_jenozKc')
|
||||||
|
assertExtractId('http://www.youtube.com/watch?v=BaW_jenozKcsharePLED17F32AD9753930', 'BaW_jenozKc')
|
||||||
|
assertExtractId('BaW_jenozKc', 'BaW_jenozKc')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -12,111 +12,213 @@ import io
|
|||||||
import re
|
import re
|
||||||
import string
|
import string
|
||||||
|
|
||||||
from test.helper import FakeYDL
|
|
||||||
from youtube_dl.extractor import YoutubeIE
|
|
||||||
from youtube_dl.compat import compat_str, compat_urlretrieve
|
from youtube_dl.compat import compat_str, compat_urlretrieve
|
||||||
|
|
||||||
_TESTS = [
|
from test.helper import FakeYDL
|
||||||
|
from youtube_dl.extractor import YoutubeIE
|
||||||
|
from youtube_dl.jsinterp import JSInterpreter
|
||||||
|
|
||||||
|
_SIG_TESTS = [
|
||||||
(
|
(
|
||||||
'https://s.ytimg.com/yts/jsbin/html5player-vflHOr_nV.js',
|
'https://s.ytimg.com/yts/jsbin/html5player-vflHOr_nV.js',
|
||||||
'js',
|
|
||||||
86,
|
86,
|
||||||
'>=<;:/.-[+*)(\'&%$#"!ZYX0VUTSRQPONMLKJIHGFEDCBA\\yxwvutsrqponmlkjihgfedcba987654321',
|
'>=<;:/.-[+*)(\'&%$#"!ZYX0VUTSRQPONMLKJIHGFEDCBA\\yxwvutsrqponmlkjihgfedcba987654321',
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'https://s.ytimg.com/yts/jsbin/html5player-vfldJ8xgI.js',
|
'https://s.ytimg.com/yts/jsbin/html5player-vfldJ8xgI.js',
|
||||||
'js',
|
|
||||||
85,
|
85,
|
||||||
'3456789a0cdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRS[UVWXYZ!"#$%&\'()*+,-./:;<=>?@',
|
'3456789a0cdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRS[UVWXYZ!"#$%&\'()*+,-./:;<=>?@',
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'https://s.ytimg.com/yts/jsbin/html5player-vfle-mVwz.js',
|
'https://s.ytimg.com/yts/jsbin/html5player-vfle-mVwz.js',
|
||||||
'js',
|
|
||||||
90,
|
90,
|
||||||
']\\[@?>=<;:/.-,+*)(\'&%$#"hZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjiagfedcb39876',
|
']\\[@?>=<;:/.-,+*)(\'&%$#"hZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjiagfedcb39876',
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'https://s.ytimg.com/yts/jsbin/html5player-en_US-vfl0Cbn9e.js',
|
'https://s.ytimg.com/yts/jsbin/html5player-en_US-vfl0Cbn9e.js',
|
||||||
'js',
|
|
||||||
84,
|
84,
|
||||||
'O1I3456789abcde0ghijklmnopqrstuvwxyzABCDEFGHfJKLMN2PQRSTUVW@YZ!"#$%&\'()*+,-./:;<=',
|
'O1I3456789abcde0ghijklmnopqrstuvwxyzABCDEFGHfJKLMN2PQRSTUVW@YZ!"#$%&\'()*+,-./:;<=',
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'https://s.ytimg.com/yts/jsbin/html5player-en_US-vflXGBaUN.js',
|
'https://s.ytimg.com/yts/jsbin/html5player-en_US-vflXGBaUN.js',
|
||||||
'js',
|
|
||||||
'2ACFC7A61CA478CD21425E5A57EBD73DDC78E22A.2094302436B2D377D14A3BBA23022D023B8BC25AA',
|
'2ACFC7A61CA478CD21425E5A57EBD73DDC78E22A.2094302436B2D377D14A3BBA23022D023B8BC25AA',
|
||||||
'A52CB8B320D22032ABB3A41D773D2B6342034902.A22E87CDD37DBE75A5E52412DC874AC16A7CFCA2',
|
'A52CB8B320D22032ABB3A41D773D2B6342034902.A22E87CDD37DBE75A5E52412DC874AC16A7CFCA2',
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'https://s.ytimg.com/yts/jsbin/html5player-en_US-vflBb0OQx.js',
|
'https://s.ytimg.com/yts/jsbin/html5player-en_US-vflBb0OQx.js',
|
||||||
'js',
|
|
||||||
84,
|
84,
|
||||||
'123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQ0STUVWXYZ!"#$%&\'()*+,@./:;<=>'
|
'123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQ0STUVWXYZ!"#$%&\'()*+,@./:;<=>'
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'https://s.ytimg.com/yts/jsbin/html5player-en_US-vfl9FYC6l.js',
|
'https://s.ytimg.com/yts/jsbin/html5player-en_US-vfl9FYC6l.js',
|
||||||
'js',
|
|
||||||
83,
|
83,
|
||||||
'123456789abcdefghijklmnopqr0tuvwxyzABCDETGHIJKLMNOPQRS>UVWXYZ!"#$%&\'()*+,-./:;<=F'
|
'123456789abcdefghijklmnopqr0tuvwxyzABCDETGHIJKLMNOPQRS>UVWXYZ!"#$%&\'()*+,-./:;<=F'
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'https://s.ytimg.com/yts/jsbin/html5player-en_US-vflCGk6yw/html5player.js',
|
'https://s.ytimg.com/yts/jsbin/html5player-en_US-vflCGk6yw/html5player.js',
|
||||||
'js',
|
|
||||||
'4646B5181C6C3020DF1D9C7FCFEA.AD80ABF70C39BD369CCCAE780AFBB98FA6B6CB42766249D9488C288',
|
'4646B5181C6C3020DF1D9C7FCFEA.AD80ABF70C39BD369CCCAE780AFBB98FA6B6CB42766249D9488C288',
|
||||||
'82C8849D94266724DC6B6AF89BBFA087EACCD963.B93C07FBA084ACAEFCF7C9D1FD0203C6C1815B6B'
|
'82C8849D94266724DC6B6AF89BBFA087EACCD963.B93C07FBA084ACAEFCF7C9D1FD0203C6C1815B6B'
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'https://s.ytimg.com/yts/jsbin/html5player-en_US-vflKjOTVq/html5player.js',
|
'https://s.ytimg.com/yts/jsbin/html5player-en_US-vflKjOTVq/html5player.js',
|
||||||
'js',
|
|
||||||
'312AA52209E3623129A412D56A40F11CB0AF14AE.3EE09501CB14E3BCDC3B2AE808BF3F1D14E7FBF12',
|
'312AA52209E3623129A412D56A40F11CB0AF14AE.3EE09501CB14E3BCDC3B2AE808BF3F1D14E7FBF12',
|
||||||
'112AA5220913623229A412D56A40F11CB0AF14AE.3EE0950FCB14EEBCDC3B2AE808BF331D14E7FBF3',
|
'112AA5220913623229A412D56A40F11CB0AF14AE.3EE0950FCB14EEBCDC3B2AE808BF331D14E7FBF3',
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
_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',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/e06dea74/player_ias.vflset/en_US/base.js',
|
||||||
|
'AiuodmaDDYw8d3y4bf', 'ankd8eza2T6Qmw',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/5dd88d1d/player-plasma-ias-phone-en_US.vflset/base.js',
|
||||||
|
'kSxKFLeqzv_ZyHSAt', 'n8gS8oRlHOxPFA',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/324f67b9/player_ias.vflset/en_US/base.js',
|
||||||
|
'xdftNy7dh9QGnhW', '22qLGxrmX8F1rA',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/4c3f79c5/player_ias.vflset/en_US/base.js',
|
||||||
|
'TDCstCG66tEAO5pR9o', 'dbxNtZ14c-yWyw',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/c81bbb4a/player_ias.vflset/en_US/base.js',
|
||||||
|
'gre3EcLurNY2vqp94', 'Z9DfGxWP115WTg',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/1f7d5369/player_ias.vflset/en_US/base.js',
|
||||||
|
'batNX7sYqIJdkJ', 'IhOkL_zxbkOZBw',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/009f1d77/player_ias.vflset/en_US/base.js',
|
||||||
|
'5dwFHw8aFWQUQtffRq', 'audescmLUzI3jw',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/dc0c6770/player_ias.vflset/en_US/base.js',
|
||||||
|
'5EHDMgYLV6HPGk_Mu-kk', 'n9lUJLHbxUI0GQ',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/c2199353/player_ias.vflset/en_US/base.js',
|
||||||
|
'5EHDMgYLV6HPGk_Mu-kk', 'AD5rgS85EkrE7',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/113ca41c/player_ias.vflset/en_US/base.js',
|
||||||
|
'cgYl-tlYkhjT7A', 'hI7BBr2zUgcmMg',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/c57c113c/player_ias.vflset/en_US/base.js',
|
||||||
|
'-Txvy6bT5R6LqgnQNx', 'dcklJCnRUHbgSg',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/5a3b6271/player_ias.vflset/en_US/base.js',
|
||||||
|
'B2j7f_UPT4rfje85Lu_e', 'm5DmNymaGQ5RdQ',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class TestPlayerInfo(unittest.TestCase):
|
||||||
|
def test_youtube_extract_player_info(self):
|
||||||
|
PLAYER_URLS = (
|
||||||
|
('https://www.youtube.com/s/player/4c3f79c5/player_ias.vflset/en_US/base.js', '4c3f79c5'),
|
||||||
|
('https://www.youtube.com/s/player/64dddad9/player_ias.vflset/en_US/base.js', '64dddad9'),
|
||||||
|
('https://www.youtube.com/s/player/64dddad9/player_ias.vflset/fr_FR/base.js', '64dddad9'),
|
||||||
|
('https://www.youtube.com/s/player/64dddad9/player-plasma-ias-phone-en_US.vflset/base.js', '64dddad9'),
|
||||||
|
('https://www.youtube.com/s/player/64dddad9/player-plasma-ias-phone-de_DE.vflset/base.js', '64dddad9'),
|
||||||
|
('https://www.youtube.com/s/player/64dddad9/player-plasma-ias-tablet-en_US.vflset/base.js', '64dddad9'),
|
||||||
|
# obsolete
|
||||||
|
('https://www.youtube.com/yts/jsbin/player_ias-vfle4-e03/en_US/base.js', 'vfle4-e03'),
|
||||||
|
('https://www.youtube.com/yts/jsbin/player_ias-vfl49f_g4/en_US/base.js', 'vfl49f_g4'),
|
||||||
|
('https://www.youtube.com/yts/jsbin/player_ias-vflCPQUIL/en_US/base.js', 'vflCPQUIL'),
|
||||||
|
('https://www.youtube.com/yts/jsbin/player-vflzQZbt7/en_US/base.js', 'vflzQZbt7'),
|
||||||
|
('https://www.youtube.com/yts/jsbin/player-en_US-vflaxXRn1/base.js', 'vflaxXRn1'),
|
||||||
|
('https://s.ytimg.com/yts/jsbin/html5player-en_US-vflXGBaUN.js', 'vflXGBaUN'),
|
||||||
|
('https://s.ytimg.com/yts/jsbin/html5player-en_US-vflKjOTVq/html5player.js', 'vflKjOTVq'),
|
||||||
|
)
|
||||||
|
for player_url, expected_player_id in PLAYER_URLS:
|
||||||
|
player_id = YoutubeIE._extract_player_info(player_url)
|
||||||
|
self.assertEqual(player_id, expected_player_id)
|
||||||
|
|
||||||
|
|
||||||
class TestSignature(unittest.TestCase):
|
class TestSignature(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
TEST_DIR = os.path.dirname(os.path.abspath(__file__))
|
TEST_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
self.TESTDATA_DIR = os.path.join(TEST_DIR, 'testdata')
|
self.TESTDATA_DIR = os.path.join(TEST_DIR, 'testdata/sigs')
|
||||||
if not os.path.exists(self.TESTDATA_DIR):
|
if not os.path.exists(self.TESTDATA_DIR):
|
||||||
os.mkdir(self.TESTDATA_DIR)
|
os.mkdir(self.TESTDATA_DIR)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
try:
|
||||||
|
for f in os.listdir(self.TESTDATA_DIR):
|
||||||
|
os.remove(f)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
def make_tfunc(url, stype, sig_input, expected_sig):
|
|
||||||
m = re.match(r'.*-([a-zA-Z0-9_-]+)(?:/watch_as3|/html5player)?\.[a-z]+$', url)
|
def t_factory(name, sig_func, url_pattern):
|
||||||
|
def make_tfunc(url, sig_input, expected_sig):
|
||||||
|
m = url_pattern.match(url)
|
||||||
assert m, '%r should follow URL format' % url
|
assert m, '%r should follow URL format' % url
|
||||||
test_id = m.group(1)
|
test_id = m.group('id')
|
||||||
|
|
||||||
def test_func(self):
|
def test_func(self):
|
||||||
basename = 'player-%s.%s' % (test_id, stype)
|
basename = 'player-{0}-{1}.js'.format(name, test_id)
|
||||||
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)
|
||||||
|
|
||||||
ydl = FakeYDL()
|
|
||||||
ie = YoutubeIE(ydl)
|
|
||||||
if stype == 'js':
|
|
||||||
with io.open(fn, encoding='utf-8') as testf:
|
with io.open(fn, encoding='utf-8') as testf:
|
||||||
jscode = testf.read()
|
jscode = testf.read()
|
||||||
func = ie._parse_sig_js(jscode)
|
self.assertEqual(sig_func(jscode, sig_input), expected_sig)
|
||||||
else:
|
|
||||||
assert stype == 'swf'
|
test_func.__name__ = str('test_{0}_js_{1}'.format(name, test_id))
|
||||||
with open(fn, 'rb') as testf:
|
setattr(TestSignature, test_func.__name__, test_func)
|
||||||
swfcode = testf.read()
|
return make_tfunc
|
||||||
func = ie._parse_sig_swf(swfcode)
|
|
||||||
|
|
||||||
|
def signature(jscode, sig_input):
|
||||||
|
func = YoutubeIE(FakeYDL())._parse_sig_js(jscode)
|
||||||
src_sig = (
|
src_sig = (
|
||||||
compat_str(string.printable[:sig_input])
|
compat_str(string.printable[:sig_input])
|
||||||
if isinstance(sig_input, int) else sig_input)
|
if isinstance(sig_input, int) else sig_input)
|
||||||
got_sig = func(src_sig)
|
return func(src_sig)
|
||||||
self.assertEqual(got_sig, expected_sig)
|
|
||||||
|
|
||||||
test_func.__name__ = str('test_signature_' + stype + '_' + test_id)
|
|
||||||
setattr(TestSignature, test_func.__name__, test_func)
|
|
||||||
|
|
||||||
|
|
||||||
for test_spec in _TESTS:
|
def n_sig(jscode, sig_input):
|
||||||
make_tfunc(*test_spec)
|
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__':
|
||||||
|
|||||||
9
test/testdata/cookies/malformed_cookies.txt
vendored
Normal file
9
test/testdata/cookies/malformed_cookies.txt
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Netscape HTTP Cookie File
|
||||||
|
# http://curl.haxx.se/rfc/cookie_spec.html
|
||||||
|
# This is a generated file! Do not edit.
|
||||||
|
|
||||||
|
# Cookie file entry with invalid number of fields - 6 instead of 7
|
||||||
|
www.foobar.foobar FALSE / FALSE 0 COOKIE
|
||||||
|
|
||||||
|
# Cookie file entry with invalid expires at
|
||||||
|
www.foobar.foobar FALSE / FALSE 1.7976931348623157e+308 COOKIE VALUE
|
||||||
@@ -73,6 +73,7 @@ from .utils import (
|
|||||||
PostProcessingError,
|
PostProcessingError,
|
||||||
preferredencoding,
|
preferredencoding,
|
||||||
prepend_extension,
|
prepend_extension,
|
||||||
|
process_communicate_or_kill,
|
||||||
register_socks_protocols,
|
register_socks_protocols,
|
||||||
render_table,
|
render_table,
|
||||||
replace_extension,
|
replace_extension,
|
||||||
@@ -86,6 +87,7 @@ from .utils import (
|
|||||||
subtitles_filename,
|
subtitles_filename,
|
||||||
UnavailableVideoError,
|
UnavailableVideoError,
|
||||||
url_basename,
|
url_basename,
|
||||||
|
variadic,
|
||||||
version_tuple,
|
version_tuple,
|
||||||
write_json_file,
|
write_json_file,
|
||||||
write_string,
|
write_string,
|
||||||
@@ -163,6 +165,7 @@ class YoutubeDL(object):
|
|||||||
simulate: Do not download the video files.
|
simulate: Do not download the video files.
|
||||||
format: Video format code. See options.py for more information.
|
format: Video format code. See options.py for more information.
|
||||||
outtmpl: Template for output names.
|
outtmpl: Template for output names.
|
||||||
|
outtmpl_na_placeholder: Placeholder for unavailable meta fields.
|
||||||
restrictfilenames: Do not allow "&" and spaces in file names
|
restrictfilenames: Do not allow "&" and spaces in file names
|
||||||
ignoreerrors: Do not stop on download errors.
|
ignoreerrors: Do not stop on download errors.
|
||||||
force_generic_extractor: Force downloader to use the generic extractor
|
force_generic_extractor: Force downloader to use the generic extractor
|
||||||
@@ -338,6 +341,8 @@ class YoutubeDL(object):
|
|||||||
_pps = []
|
_pps = []
|
||||||
_download_retcode = None
|
_download_retcode = None
|
||||||
_num_downloads = None
|
_num_downloads = None
|
||||||
|
_playlist_level = 0
|
||||||
|
_playlist_urls = set()
|
||||||
_screen_file = None
|
_screen_file = None
|
||||||
|
|
||||||
def __init__(self, params=None, auto_init=True):
|
def __init__(self, params=None, auto_init=True):
|
||||||
@@ -656,7 +661,7 @@ class YoutubeDL(object):
|
|||||||
template_dict = dict((k, v if isinstance(v, compat_numeric_types) else sanitize(k, v))
|
template_dict = dict((k, v if isinstance(v, compat_numeric_types) else sanitize(k, v))
|
||||||
for k, v in template_dict.items()
|
for k, v in template_dict.items()
|
||||||
if v is not None and not isinstance(v, (list, tuple, dict)))
|
if v is not None and not isinstance(v, (list, tuple, dict)))
|
||||||
template_dict = collections.defaultdict(lambda: 'NA', template_dict)
|
template_dict = collections.defaultdict(lambda: self.params.get('outtmpl_na_placeholder', 'NA'), template_dict)
|
||||||
|
|
||||||
outtmpl = self.params.get('outtmpl', DEFAULT_OUTTMPL)
|
outtmpl = self.params.get('outtmpl', DEFAULT_OUTTMPL)
|
||||||
|
|
||||||
@@ -676,8 +681,8 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
# Missing numeric fields used together with integer presentation types
|
# Missing numeric fields used together with integer presentation types
|
||||||
# in format specification will break the argument substitution since
|
# in format specification will break the argument substitution since
|
||||||
# string 'NA' is returned for missing fields. We will patch output
|
# string NA placeholder is returned for missing fields. We will patch
|
||||||
# template for missing fields to meet string presentation type.
|
# output template for missing fields to meet string presentation type.
|
||||||
for numeric_field in self._NUMERIC_FIELDS:
|
for numeric_field in self._NUMERIC_FIELDS:
|
||||||
if numeric_field not in template_dict:
|
if numeric_field not in template_dict:
|
||||||
# As of [1] format syntax is:
|
# As of [1] format syntax is:
|
||||||
@@ -717,7 +722,7 @@ class YoutubeDL(object):
|
|||||||
filename = encodeFilename(filename, True).decode(preferredencoding())
|
filename = encodeFilename(filename, True).decode(preferredencoding())
|
||||||
return sanitize_path(filename)
|
return sanitize_path(filename)
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
self.report_error('Error in output template: ' + str(err) + ' (encoding: ' + repr(preferredencoding()) + ')')
|
self.report_error('Error in output template: ' + error_to_compat_str(err) + ' (encoding: ' + repr(preferredencoding()) + ')')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _match_entry(self, info_dict, incomplete):
|
def _match_entry(self, info_dict, incomplete):
|
||||||
@@ -770,11 +775,20 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
def extract_info(self, url, download=True, ie_key=None, extra_info={},
|
def extract_info(self, url, download=True, ie_key=None, extra_info={},
|
||||||
process=True, force_generic_extractor=False):
|
process=True, force_generic_extractor=False):
|
||||||
'''
|
"""
|
||||||
Returns a list with a dictionary for each video we find.
|
Return a list with a dictionary for each video extracted.
|
||||||
If 'download', also downloads the videos.
|
|
||||||
extra_info is a dict containing the extra values to add to each result
|
Arguments:
|
||||||
'''
|
url -- URL to extract
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
download -- whether to download videos during extraction
|
||||||
|
ie_key -- extractor key hint
|
||||||
|
extra_info -- dictionary containing the extra values to add to each result
|
||||||
|
process -- whether to resolve all unresolved references (URLs, playlist items),
|
||||||
|
must be True for download to work.
|
||||||
|
force_generic_extractor -- force using the generic extractor
|
||||||
|
"""
|
||||||
|
|
||||||
if not ie_key and force_generic_extractor:
|
if not ie_key and force_generic_extractor:
|
||||||
ie_key = 'Generic'
|
ie_key = 'Generic'
|
||||||
@@ -793,10 +807,37 @@ class YoutubeDL(object):
|
|||||||
self.report_warning('The program functionality for this site has been marked as broken, '
|
self.report_warning('The program functionality for this site has been marked as broken, '
|
||||||
'and will probably not work.')
|
'and will probably not work.')
|
||||||
|
|
||||||
|
return self.__extract_info(url, ie, download, extra_info, process)
|
||||||
|
else:
|
||||||
|
self.report_error('no suitable InfoExtractor for URL %s' % url)
|
||||||
|
|
||||||
|
def __handle_extraction_exceptions(func):
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
|
return func(self, *args, **kwargs)
|
||||||
|
except GeoRestrictedError as e:
|
||||||
|
msg = e.msg
|
||||||
|
if e.countries:
|
||||||
|
msg += '\nThis video is available in %s.' % ', '.join(
|
||||||
|
map(ISO3166Utils.short2full, e.countries))
|
||||||
|
msg += '\nYou might want to use a VPN or a proxy server (with --proxy) to workaround.'
|
||||||
|
self.report_error(msg)
|
||||||
|
except ExtractorError as e: # An error we somewhat expected
|
||||||
|
self.report_error(compat_str(e), e.format_traceback())
|
||||||
|
except MaxDownloadsReached:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
if self.params.get('ignoreerrors', False):
|
||||||
|
self.report_error(error_to_compat_str(e), tb=encode_compat_str(traceback.format_exc()))
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
@__handle_extraction_exceptions
|
||||||
|
def __extract_info(self, url, ie, download, extra_info, process):
|
||||||
ie_result = ie.extract(url)
|
ie_result = ie.extract(url)
|
||||||
if ie_result is None: # Finished already (backwards compatibility; listformats and friends should be moved here)
|
if ie_result is None: # Finished already (backwards compatibility; listformats and friends should be moved here)
|
||||||
break
|
return
|
||||||
if isinstance(ie_result, list):
|
if isinstance(ie_result, list):
|
||||||
# Backwards compatibility: old IE result format
|
# Backwards compatibility: old IE result format
|
||||||
ie_result = {
|
ie_result = {
|
||||||
@@ -808,27 +849,6 @@ class YoutubeDL(object):
|
|||||||
return self.process_ie_result(ie_result, download, extra_info)
|
return self.process_ie_result(ie_result, download, extra_info)
|
||||||
else:
|
else:
|
||||||
return ie_result
|
return ie_result
|
||||||
except GeoRestrictedError as e:
|
|
||||||
msg = e.msg
|
|
||||||
if e.countries:
|
|
||||||
msg += '\nThis video is available in %s.' % ', '.join(
|
|
||||||
map(ISO3166Utils.short2full, e.countries))
|
|
||||||
msg += '\nYou might want to use a VPN or a proxy server (with --proxy) to workaround.'
|
|
||||||
self.report_error(msg)
|
|
||||||
break
|
|
||||||
except ExtractorError as e: # An error we somewhat expected
|
|
||||||
self.report_error(compat_str(e), e.format_traceback())
|
|
||||||
break
|
|
||||||
except MaxDownloadsReached:
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
if self.params.get('ignoreerrors', False):
|
|
||||||
self.report_error(error_to_compat_str(e), tb=encode_compat_str(traceback.format_exc()))
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
self.report_error('no suitable InfoExtractor for URL %s' % url)
|
|
||||||
|
|
||||||
def add_default_extra_info(self, ie_result, ie, url):
|
def add_default_extra_info(self, ie_result, ie, url):
|
||||||
self.add_extra_info(ie_result, {
|
self.add_extra_info(ie_result, {
|
||||||
@@ -900,8 +920,51 @@ class YoutubeDL(object):
|
|||||||
return self.process_ie_result(
|
return self.process_ie_result(
|
||||||
new_result, download=download, extra_info=extra_info)
|
new_result, download=download, extra_info=extra_info)
|
||||||
elif result_type in ('playlist', 'multi_video'):
|
elif result_type in ('playlist', 'multi_video'):
|
||||||
|
# Protect from infinite recursion due to recursively nested playlists
|
||||||
|
# (see https://github.com/ytdl-org/youtube-dl/issues/27833)
|
||||||
|
webpage_url = ie_result['webpage_url']
|
||||||
|
if webpage_url in self._playlist_urls:
|
||||||
|
self.to_screen(
|
||||||
|
'[download] Skipping already downloaded playlist: %s'
|
||||||
|
% ie_result.get('title') or ie_result.get('id'))
|
||||||
|
return
|
||||||
|
|
||||||
|
self._playlist_level += 1
|
||||||
|
self._playlist_urls.add(webpage_url)
|
||||||
|
try:
|
||||||
|
return self.__process_playlist(ie_result, download)
|
||||||
|
finally:
|
||||||
|
self._playlist_level -= 1
|
||||||
|
if not self._playlist_level:
|
||||||
|
self._playlist_urls.clear()
|
||||||
|
elif result_type == 'compat_list':
|
||||||
|
self.report_warning(
|
||||||
|
'Extractor %s returned a compat_list result. '
|
||||||
|
'It needs to be updated.' % ie_result.get('extractor'))
|
||||||
|
|
||||||
|
def _fixup(r):
|
||||||
|
self.add_extra_info(
|
||||||
|
r,
|
||||||
|
{
|
||||||
|
'extractor': ie_result['extractor'],
|
||||||
|
'webpage_url': ie_result['webpage_url'],
|
||||||
|
'webpage_url_basename': url_basename(ie_result['webpage_url']),
|
||||||
|
'extractor_key': ie_result['extractor_key'],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return r
|
||||||
|
ie_result['entries'] = [
|
||||||
|
self.process_ie_result(_fixup(r), download, extra_info)
|
||||||
|
for r in ie_result['entries']
|
||||||
|
]
|
||||||
|
return ie_result
|
||||||
|
else:
|
||||||
|
raise Exception('Invalid result type: %s' % result_type)
|
||||||
|
|
||||||
|
def __process_playlist(self, ie_result, download):
|
||||||
# We process each entry in the playlist
|
# We process each entry in the playlist
|
||||||
playlist = ie_result.get('title') or ie_result.get('id')
|
playlist = ie_result.get('title') or ie_result.get('id')
|
||||||
|
|
||||||
self.to_screen('[download] Downloading playlist: %s' % playlist)
|
self.to_screen('[download] Downloading playlist: %s' % playlist)
|
||||||
|
|
||||||
playlist_results = []
|
playlist_results = []
|
||||||
@@ -1003,36 +1066,17 @@ class YoutubeDL(object):
|
|||||||
self.to_screen('[download] ' + reason)
|
self.to_screen('[download] ' + reason)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
entry_result = self.process_ie_result(entry,
|
entry_result = self.__process_iterable_entry(entry, download, extra)
|
||||||
download=download,
|
# TODO: skip failed (empty) entries?
|
||||||
extra_info=extra)
|
|
||||||
playlist_results.append(entry_result)
|
playlist_results.append(entry_result)
|
||||||
ie_result['entries'] = playlist_results
|
ie_result['entries'] = playlist_results
|
||||||
self.to_screen('[download] Finished downloading playlist: %s' % playlist)
|
self.to_screen('[download] Finished downloading playlist: %s' % playlist)
|
||||||
return ie_result
|
return ie_result
|
||||||
elif result_type == 'compat_list':
|
|
||||||
self.report_warning(
|
|
||||||
'Extractor %s returned a compat_list result. '
|
|
||||||
'It needs to be updated.' % ie_result.get('extractor'))
|
|
||||||
|
|
||||||
def _fixup(r):
|
@__handle_extraction_exceptions
|
||||||
self.add_extra_info(
|
def __process_iterable_entry(self, entry, download, extra_info):
|
||||||
r,
|
return self.process_ie_result(
|
||||||
{
|
entry, download=download, extra_info=extra_info)
|
||||||
'extractor': ie_result['extractor'],
|
|
||||||
'webpage_url': ie_result['webpage_url'],
|
|
||||||
'webpage_url_basename': url_basename(ie_result['webpage_url']),
|
|
||||||
'extractor_key': ie_result['extractor_key'],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return r
|
|
||||||
ie_result['entries'] = [
|
|
||||||
self.process_ie_result(_fixup(r), download, extra_info)
|
|
||||||
for r in ie_result['entries']
|
|
||||||
]
|
|
||||||
return ie_result
|
|
||||||
else:
|
|
||||||
raise Exception('Invalid result type: %s' % result_type)
|
|
||||||
|
|
||||||
def _build_format_filter(self, filter_spec):
|
def _build_format_filter(self, filter_spec):
|
||||||
" Returns a function to filter the formats according to the filter_spec "
|
" Returns a function to filter the formats according to the filter_spec "
|
||||||
@@ -1073,7 +1117,7 @@ class YoutubeDL(object):
|
|||||||
'*=': lambda attr, value: value in attr,
|
'*=': lambda attr, value: value in attr,
|
||||||
}
|
}
|
||||||
str_operator_rex = re.compile(r'''(?x)
|
str_operator_rex = re.compile(r'''(?x)
|
||||||
\s*(?P<key>ext|acodec|vcodec|container|protocol|format_id)
|
\s*(?P<key>ext|acodec|vcodec|container|protocol|format_id|language)
|
||||||
\s*(?P<negation>!\s*)?(?P<op>%s)(?P<none_inclusive>\s*\?)?
|
\s*(?P<negation>!\s*)?(?P<op>%s)(?P<none_inclusive>\s*\?)?
|
||||||
\s*(?P<value>[a-zA-Z0-9._-]+)
|
\s*(?P<value>[a-zA-Z0-9._-]+)
|
||||||
\s*$
|
\s*$
|
||||||
@@ -1216,6 +1260,8 @@ class YoutubeDL(object):
|
|||||||
group = _parse_format_selection(tokens, inside_group=True)
|
group = _parse_format_selection(tokens, inside_group=True)
|
||||||
current_selector = FormatSelector(GROUP, group, [])
|
current_selector = FormatSelector(GROUP, group, [])
|
||||||
elif string == '+':
|
elif string == '+':
|
||||||
|
if inside_merge:
|
||||||
|
raise syntax_error('Unexpected "+"', start)
|
||||||
video_selector = current_selector
|
video_selector = current_selector
|
||||||
audio_selector = _parse_format_selection(tokens, inside_merge=True)
|
audio_selector = _parse_format_selection(tokens, inside_merge=True)
|
||||||
if not video_selector or not audio_selector:
|
if not video_selector or not audio_selector:
|
||||||
@@ -1253,57 +1299,46 @@ class YoutubeDL(object):
|
|||||||
format_spec = selector.selector
|
format_spec = selector.selector
|
||||||
|
|
||||||
def selector_function(ctx):
|
def selector_function(ctx):
|
||||||
formats = list(ctx['formats'])
|
|
||||||
if not formats:
|
def best_worst(fmts, fmt_spec='best'):
|
||||||
return
|
format_idx = 0 if fmt_spec == 'worst' else -1
|
||||||
if format_spec == 'all':
|
|
||||||
for f in formats:
|
|
||||||
yield f
|
|
||||||
elif format_spec in ['best', 'worst', None]:
|
|
||||||
format_idx = 0 if format_spec == 'worst' else -1
|
|
||||||
audiovideo_formats = [
|
audiovideo_formats = [
|
||||||
f for f in formats
|
f for f in fmts
|
||||||
if f.get('vcodec') != 'none' and f.get('acodec') != 'none']
|
if f.get('vcodec') != 'none' and f.get('acodec') != 'none']
|
||||||
if audiovideo_formats:
|
if audiovideo_formats:
|
||||||
yield audiovideo_formats[format_idx]
|
return audiovideo_formats[format_idx]
|
||||||
# for extractors with incomplete formats (audio only (soundcloud)
|
# for extractors with incomplete formats (audio only (soundcloud)
|
||||||
# or video only (imgur)) we will fallback to best/worst
|
# or video only (imgur)) we will fallback to best/worst
|
||||||
# {video,audio}-only format
|
# {video,audio}-only format
|
||||||
elif ctx['incomplete_formats']:
|
elif ctx['incomplete_formats']:
|
||||||
yield formats[format_idx]
|
return fmts[format_idx]
|
||||||
elif format_spec == 'bestaudio':
|
|
||||||
|
formats = list(ctx['formats'])
|
||||||
|
if not formats:
|
||||||
|
return
|
||||||
|
if format_spec == 'all':
|
||||||
|
pass
|
||||||
|
elif format_spec in ('best', 'worst', None):
|
||||||
|
formats = best_worst(formats, format_spec)
|
||||||
|
elif format_spec in ('bestaudio', 'worstaudio'):
|
||||||
audio_formats = [
|
audio_formats = [
|
||||||
f for f in formats
|
f for f in formats
|
||||||
if f.get('vcodec') == 'none']
|
if f.get('vcodec') == 'none']
|
||||||
if audio_formats:
|
formats = audio_formats[:1] if format_spec == 'worstaudio' else audio_formats[-1:]
|
||||||
yield audio_formats[-1]
|
elif format_spec in ('bestvideo', 'worstvideo'):
|
||||||
elif format_spec == 'worstaudio':
|
|
||||||
audio_formats = [
|
|
||||||
f for f in formats
|
|
||||||
if f.get('vcodec') == 'none']
|
|
||||||
if audio_formats:
|
|
||||||
yield audio_formats[0]
|
|
||||||
elif format_spec == 'bestvideo':
|
|
||||||
video_formats = [
|
video_formats = [
|
||||||
f for f in formats
|
f for f in formats
|
||||||
if f.get('acodec') == 'none']
|
if f.get('acodec') == 'none']
|
||||||
if video_formats:
|
formats = video_formats[:1] if format_spec == 'worstvideo' else video_formats[-1:]
|
||||||
yield video_formats[-1]
|
|
||||||
elif format_spec == 'worstvideo':
|
|
||||||
video_formats = [
|
|
||||||
f for f in formats
|
|
||||||
if f.get('acodec') == 'none']
|
|
||||||
if video_formats:
|
|
||||||
yield video_formats[0]
|
|
||||||
else:
|
else:
|
||||||
extensions = ['mp4', 'flv', 'webm', '3gp', 'm4a', 'mp3', 'ogg', 'aac', 'wav']
|
extensions = ['mp4', 'flv', 'webm', '3gp', 'm4a', 'mp3', 'ogg', 'aac', 'wav']
|
||||||
if format_spec in extensions:
|
if format_spec in extensions:
|
||||||
filter_f = lambda f: f['ext'] == format_spec
|
filter_f = lambda f: f['ext'] == format_spec
|
||||||
else:
|
else:
|
||||||
filter_f = lambda f: f['format_id'] == format_spec
|
filter_f = lambda f: f['format_id'] == format_spec
|
||||||
matches = list(filter(filter_f, formats))
|
formats = best_worst(list(filter(filter_f, formats)))
|
||||||
if matches:
|
for f in variadic(formats or []):
|
||||||
yield matches[-1]
|
yield f
|
||||||
elif selector.type == MERGE:
|
elif selector.type == MERGE:
|
||||||
def _merge(formats_info):
|
def _merge(formats_info):
|
||||||
format_1, format_2 = [f['format_id'] for f in formats_info]
|
format_1, format_2 = [f['format_id'] for f in formats_info]
|
||||||
@@ -1476,12 +1511,16 @@ class YoutubeDL(object):
|
|||||||
if 'display_id' not in info_dict and 'id' in info_dict:
|
if 'display_id' not in info_dict and 'id' in info_dict:
|
||||||
info_dict['display_id'] = info_dict['id']
|
info_dict['display_id'] = info_dict['id']
|
||||||
|
|
||||||
if info_dict.get('upload_date') is None and info_dict.get('timestamp') is not None:
|
for ts_key, date_key in (
|
||||||
|
('timestamp', 'upload_date'),
|
||||||
|
('release_timestamp', 'release_date'),
|
||||||
|
):
|
||||||
|
if info_dict.get(date_key) is None and info_dict.get(ts_key) is not None:
|
||||||
# Working around out-of-range timestamp values (e.g. negative ones on Windows,
|
# Working around out-of-range timestamp values (e.g. negative ones on Windows,
|
||||||
# see http://bugs.python.org/issue1646728)
|
# see http://bugs.python.org/issue1646728)
|
||||||
try:
|
try:
|
||||||
upload_date = datetime.datetime.utcfromtimestamp(info_dict['timestamp'])
|
upload_date = datetime.datetime.utcfromtimestamp(info_dict[ts_key])
|
||||||
info_dict['upload_date'] = upload_date.strftime('%Y%m%d')
|
info_dict[date_key] = compat_str(upload_date.strftime('%Y%m%d'))
|
||||||
except (ValueError, OverflowError, OSError):
|
except (ValueError, OverflowError, OSError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -1521,9 +1560,6 @@ class YoutubeDL(object):
|
|||||||
else:
|
else:
|
||||||
formats = info_dict['formats']
|
formats = info_dict['formats']
|
||||||
|
|
||||||
if not formats:
|
|
||||||
raise ExtractorError('No video formats found!')
|
|
||||||
|
|
||||||
def is_wellformed(f):
|
def is_wellformed(f):
|
||||||
url = f.get('url')
|
url = f.get('url')
|
||||||
if not url:
|
if not url:
|
||||||
@@ -1536,7 +1572,10 @@ class YoutubeDL(object):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
# Filter out malformed formats for better extraction robustness
|
# Filter out malformed formats for better extraction robustness
|
||||||
formats = list(filter(is_wellformed, formats))
|
formats = list(filter(is_wellformed, formats or []))
|
||||||
|
|
||||||
|
if not formats:
|
||||||
|
raise ExtractorError('No video formats found!')
|
||||||
|
|
||||||
formats_dict = {}
|
formats_dict = {}
|
||||||
|
|
||||||
@@ -1600,7 +1639,7 @@ class YoutubeDL(object):
|
|||||||
if req_format is None:
|
if req_format is None:
|
||||||
req_format = self._default_format_spec(info_dict, download=download)
|
req_format = self._default_format_spec(info_dict, download=download)
|
||||||
if self.params.get('verbose'):
|
if self.params.get('verbose'):
|
||||||
self.to_stdout('[debug] Default format spec: %s' % req_format)
|
self._write_string('[debug] Default format spec: %s\n' % req_format)
|
||||||
|
|
||||||
format_selector = self.build_format_selector(req_format)
|
format_selector = self.build_format_selector(req_format)
|
||||||
|
|
||||||
@@ -1730,9 +1769,8 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
assert info_dict.get('_type', 'video') == 'video'
|
assert info_dict.get('_type', 'video') == 'video'
|
||||||
|
|
||||||
max_downloads = self.params.get('max_downloads')
|
max_downloads = int_or_none(self.params.get('max_downloads')) or float('inf')
|
||||||
if max_downloads is not None:
|
if self._num_downloads >= max_downloads:
|
||||||
if self._num_downloads >= int(max_downloads):
|
|
||||||
raise MaxDownloadsReached()
|
raise MaxDownloadsReached()
|
||||||
|
|
||||||
# TODO: backward compatibility, to be removed
|
# TODO: backward compatibility, to be removed
|
||||||
@@ -1767,6 +1805,8 @@ class YoutubeDL(object):
|
|||||||
os.makedirs(dn)
|
os.makedirs(dn)
|
||||||
return True
|
return True
|
||||||
except (OSError, IOError) as err:
|
except (OSError, IOError) as err:
|
||||||
|
if isinstance(err, OSError) and err.errno == errno.EEXIST:
|
||||||
|
return True
|
||||||
self.report_error('unable to create directory ' + error_to_compat_str(err))
|
self.report_error('unable to create directory ' + error_to_compat_str(err))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -1856,12 +1896,21 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
if not self.params.get('skip_download', False):
|
if not self.params.get('skip_download', False):
|
||||||
try:
|
try:
|
||||||
|
def checked_get_suitable_downloader(info_dict, params):
|
||||||
|
ed_args = params.get('external_downloader_args')
|
||||||
|
dler = get_suitable_downloader(info_dict, params)
|
||||||
|
if ed_args and not params.get('external_downloader_args'):
|
||||||
|
# external_downloader_args was cleared because external_downloader was rejected
|
||||||
|
self.report_warning('Requested external downloader cannot be used: '
|
||||||
|
'ignoring --external-downloader-args.')
|
||||||
|
return dler
|
||||||
|
|
||||||
def dl(name, info):
|
def dl(name, info):
|
||||||
fd = get_suitable_downloader(info, self.params)(self, self.params)
|
fd = checked_get_suitable_downloader(info, self.params)(self, self.params)
|
||||||
for ph in self._progress_hooks:
|
for ph in self._progress_hooks:
|
||||||
fd.add_progress_hook(ph)
|
fd.add_progress_hook(ph)
|
||||||
if self.params.get('verbose'):
|
if self.params.get('verbose'):
|
||||||
self.to_stdout('[debug] Invoking downloader on %r' % info.get('url'))
|
self.to_screen('[debug] Invoking downloader on %r' % info.get('url'))
|
||||||
return fd.download(name, info)
|
return fd.download(name, info)
|
||||||
|
|
||||||
if info_dict.get('requested_formats') is not None:
|
if info_dict.get('requested_formats') is not None:
|
||||||
@@ -1999,9 +2048,12 @@ class YoutubeDL(object):
|
|||||||
try:
|
try:
|
||||||
self.post_process(filename, info_dict)
|
self.post_process(filename, info_dict)
|
||||||
except (PostProcessingError) as err:
|
except (PostProcessingError) as err:
|
||||||
self.report_error('postprocessing: %s' % str(err))
|
self.report_error('postprocessing: %s' % error_to_compat_str(err))
|
||||||
return
|
return
|
||||||
self.record_download_archive(info_dict)
|
self.record_download_archive(info_dict)
|
||||||
|
# avoid possible nugatory search for further items (PR #26638)
|
||||||
|
if self._num_downloads >= max_downloads:
|
||||||
|
raise MaxDownloadsReached()
|
||||||
|
|
||||||
def download(self, url_list):
|
def download(self, url_list):
|
||||||
"""Download a given list of URLs."""
|
"""Download a given list of URLs."""
|
||||||
@@ -2264,7 +2316,7 @@ class YoutubeDL(object):
|
|||||||
['git', 'rev-parse', '--short', 'HEAD'],
|
['git', 'rev-parse', '--short', 'HEAD'],
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||||
cwd=os.path.dirname(os.path.abspath(__file__)))
|
cwd=os.path.dirname(os.path.abspath(__file__)))
|
||||||
out, err = sp.communicate()
|
out, err = process_communicate_or_kill(sp)
|
||||||
out = out.decode().strip()
|
out = out.decode().strip()
|
||||||
if re.match('[0-9a-f]+', out):
|
if re.match('[0-9a-f]+', out):
|
||||||
self._write_string('[debug] Git HEAD: ' + out + '\n')
|
self._write_string('[debug] Git HEAD: ' + out + '\n')
|
||||||
@@ -2400,7 +2452,7 @@ class YoutubeDL(object):
|
|||||||
thumb_ext = determine_ext(t['url'], 'jpg')
|
thumb_ext = determine_ext(t['url'], 'jpg')
|
||||||
suffix = '_%s' % t['id'] if len(thumbnails) > 1 else ''
|
suffix = '_%s' % t['id'] if len(thumbnails) > 1 else ''
|
||||||
thumb_display_id = '%s ' % t['id'] if len(thumbnails) > 1 else ''
|
thumb_display_id = '%s ' % t['id'] if len(thumbnails) > 1 else ''
|
||||||
t['filename'] = thumb_filename = os.path.splitext(filename)[0] + suffix + '.' + thumb_ext
|
t['filename'] = thumb_filename = replace_extension(filename + suffix, thumb_ext, info_dict.get('ext'))
|
||||||
|
|
||||||
if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(thumb_filename)):
|
if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(thumb_filename)):
|
||||||
self.to_screen('[%s] %s: Thumbnail %sis already present' %
|
self.to_screen('[%s] %s: Thumbnail %sis already present' %
|
||||||
|
|||||||
@@ -340,6 +340,7 @@ def _real_main(argv=None):
|
|||||||
'format': opts.format,
|
'format': opts.format,
|
||||||
'listformats': opts.listformats,
|
'listformats': opts.listformats,
|
||||||
'outtmpl': outtmpl,
|
'outtmpl': outtmpl,
|
||||||
|
'outtmpl_na_placeholder': opts.outtmpl_na_placeholder,
|
||||||
'autonumber_size': opts.autonumber_size,
|
'autonumber_size': opts.autonumber_size,
|
||||||
'autonumber_start': opts.autonumber_start,
|
'autonumber_start': opts.autonumber_start,
|
||||||
'restrictfilenames': opts.restrictfilenames,
|
'restrictfilenames': opts.restrictfilenames,
|
||||||
|
|||||||
@@ -8,6 +8,18 @@ from .utils import bytes_to_intlist, intlist_to_bytes
|
|||||||
BLOCK_SIZE_BYTES = 16
|
BLOCK_SIZE_BYTES = 16
|
||||||
|
|
||||||
|
|
||||||
|
def pkcs7_padding(data):
|
||||||
|
"""
|
||||||
|
PKCS#7 padding
|
||||||
|
|
||||||
|
@param {int[]} data cleartext
|
||||||
|
@returns {int[]} padding data
|
||||||
|
"""
|
||||||
|
|
||||||
|
remaining_length = BLOCK_SIZE_BYTES - len(data) % BLOCK_SIZE_BYTES
|
||||||
|
return data + [remaining_length] * remaining_length
|
||||||
|
|
||||||
|
|
||||||
def aes_ctr_decrypt(data, key, counter):
|
def aes_ctr_decrypt(data, key, counter):
|
||||||
"""
|
"""
|
||||||
Decrypt with aes in counter mode
|
Decrypt with aes in counter mode
|
||||||
@@ -76,8 +88,7 @@ def aes_cbc_encrypt(data, key, iv):
|
|||||||
previous_cipher_block = iv
|
previous_cipher_block = iv
|
||||||
for i in range(block_count):
|
for i in range(block_count):
|
||||||
block = data[i * BLOCK_SIZE_BYTES: (i + 1) * BLOCK_SIZE_BYTES]
|
block = data[i * BLOCK_SIZE_BYTES: (i + 1) * BLOCK_SIZE_BYTES]
|
||||||
remaining_length = BLOCK_SIZE_BYTES - len(block)
|
block = pkcs7_padding(block)
|
||||||
block += [remaining_length] * remaining_length
|
|
||||||
mixed_block = xor(block, previous_cipher_block)
|
mixed_block = xor(block, previous_cipher_block)
|
||||||
|
|
||||||
encrypted_block = aes_encrypt(mixed_block, expanded_key)
|
encrypted_block = aes_encrypt(mixed_block, expanded_key)
|
||||||
@@ -88,6 +99,28 @@ def aes_cbc_encrypt(data, key, iv):
|
|||||||
return encrypted_data
|
return encrypted_data
|
||||||
|
|
||||||
|
|
||||||
|
def aes_ecb_encrypt(data, key):
|
||||||
|
"""
|
||||||
|
Encrypt with aes in ECB mode. Using PKCS#7 padding
|
||||||
|
|
||||||
|
@param {int[]} data cleartext
|
||||||
|
@param {int[]} key 16/24/32-Byte cipher key
|
||||||
|
@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]
|
||||||
|
block = pkcs7_padding(block)
|
||||||
|
|
||||||
|
encrypted_block = aes_encrypt(block, expanded_key)
|
||||||
|
encrypted_data += encrypted_block
|
||||||
|
|
||||||
|
return encrypted_data
|
||||||
|
|
||||||
|
|
||||||
def key_expansion(data):
|
def key_expansion(data):
|
||||||
"""
|
"""
|
||||||
Generate key schedule
|
Generate key schedule
|
||||||
@@ -303,7 +336,7 @@ def xor(data1, data2):
|
|||||||
|
|
||||||
|
|
||||||
def rijndael_mul(a, b):
|
def rijndael_mul(a, b):
|
||||||
if(a == 0 or b == 0):
|
if (a == 0 or b == 0):
|
||||||
return 0
|
return 0
|
||||||
return RIJNDAEL_EXP_TABLE[(RIJNDAEL_LOG_TABLE[a] + RIJNDAEL_LOG_TABLE[b]) % 0xFF]
|
return RIJNDAEL_EXP_TABLE[(RIJNDAEL_LOG_TABLE[a] + RIJNDAEL_LOG_TABLE[b]) % 0xFF]
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,21 @@ import traceback
|
|||||||
|
|
||||||
from .compat import compat_getenv
|
from .compat import compat_getenv
|
||||||
from .utils import (
|
from .utils import (
|
||||||
|
error_to_compat_str,
|
||||||
expand_path,
|
expand_path,
|
||||||
|
is_outdated_version,
|
||||||
|
try_get,
|
||||||
write_json_file,
|
write_json_file,
|
||||||
)
|
)
|
||||||
|
from .version import __version__
|
||||||
|
|
||||||
|
|
||||||
class Cache(object):
|
class Cache(object):
|
||||||
|
|
||||||
|
_YTDL_DIR = 'youtube-dl'
|
||||||
|
_VERSION_KEY = _YTDL_DIR + '_version'
|
||||||
|
_DEFAULT_VERSION = '2021.12.17'
|
||||||
|
|
||||||
def __init__(self, ydl):
|
def __init__(self, ydl):
|
||||||
self._ydl = ydl
|
self._ydl = ydl
|
||||||
|
|
||||||
@@ -23,7 +32,7 @@ class Cache(object):
|
|||||||
res = self._ydl.params.get('cachedir')
|
res = self._ydl.params.get('cachedir')
|
||||||
if res is None:
|
if res is None:
|
||||||
cache_root = compat_getenv('XDG_CACHE_HOME', '~/.cache')
|
cache_root = compat_getenv('XDG_CACHE_HOME', '~/.cache')
|
||||||
res = os.path.join(cache_root, 'youtube-dl')
|
res = os.path.join(cache_root, self._YTDL_DIR)
|
||||||
return expand_path(res)
|
return expand_path(res)
|
||||||
|
|
||||||
def _get_cache_fn(self, section, key, dtype):
|
def _get_cache_fn(self, section, key, dtype):
|
||||||
@@ -50,13 +59,22 @@ class Cache(object):
|
|||||||
except OSError as ose:
|
except OSError as ose:
|
||||||
if ose.errno != errno.EEXIST:
|
if ose.errno != errno.EEXIST:
|
||||||
raise
|
raise
|
||||||
write_json_file(data, fn)
|
write_json_file({self._VERSION_KEY: __version__, 'data': data}, fn)
|
||||||
except Exception:
|
except Exception:
|
||||||
tb = traceback.format_exc()
|
tb = traceback.format_exc()
|
||||||
self._ydl.report_warning(
|
self._ydl.report_warning(
|
||||||
'Writing cache to %r failed: %s' % (fn, tb))
|
'Writing cache to %r failed: %s' % (fn, tb))
|
||||||
|
|
||||||
def load(self, section, key, dtype='json', default=None):
|
def _validate(self, data, min_ver):
|
||||||
|
version = try_get(data, lambda x: x[self._VERSION_KEY])
|
||||||
|
if not version: # Backward compatibility
|
||||||
|
data, version = {'data': data}, self._DEFAULT_VERSION
|
||||||
|
if not is_outdated_version(version, min_ver or '0', assume_new=False):
|
||||||
|
return data['data']
|
||||||
|
self._ydl.to_screen(
|
||||||
|
'Discarding old cache from version {version} (needs {min_ver})'.format(**locals()))
|
||||||
|
|
||||||
|
def load(self, section, key, dtype='json', default=None, min_ver=None):
|
||||||
assert dtype in ('json',)
|
assert dtype in ('json',)
|
||||||
|
|
||||||
if not self.enabled:
|
if not self.enabled:
|
||||||
@@ -66,12 +84,12 @@ class Cache(object):
|
|||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
with io.open(cache_fn, 'r', encoding='utf-8') as cachef:
|
with io.open(cache_fn, 'r', encoding='utf-8') as cachef:
|
||||||
return json.load(cachef)
|
return self._validate(json.load(cachef), min_ver)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
try:
|
try:
|
||||||
file_size = os.path.getsize(cache_fn)
|
file_size = os.path.getsize(cache_fn)
|
||||||
except (OSError, IOError) as oe:
|
except (OSError, IOError) as oe:
|
||||||
file_size = str(oe)
|
file_size = error_to_compat_str(oe)
|
||||||
self._ydl.report_warning(
|
self._ydl.report_warning(
|
||||||
'Cache retrieval from %s failed (%s)' % (cache_fn, file_size))
|
'Cache retrieval from %s failed (%s)' % (cache_fn, file_size))
|
||||||
except IOError:
|
except IOError:
|
||||||
|
|||||||
1667
youtube_dl/casefold.py
Normal file
1667
youtube_dl/casefold.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,23 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import xml.etree.ElementTree
|
import xml.etree.ElementTree
|
||||||
|
|
||||||
|
# deal with critical unicode/str things first
|
||||||
|
try:
|
||||||
|
# Python 2
|
||||||
|
compat_str, compat_basestring, compat_chr = (
|
||||||
|
unicode, basestring, unichr
|
||||||
|
)
|
||||||
|
from .casefold import casefold as compat_casefold
|
||||||
|
except NameError:
|
||||||
|
compat_str, compat_basestring, compat_chr = (
|
||||||
|
str, str, chr
|
||||||
|
)
|
||||||
|
compat_casefold = lambda s: s.casefold()
|
||||||
|
|
||||||
|
try:
|
||||||
|
import collections.abc as compat_collections_abc
|
||||||
|
except ImportError:
|
||||||
|
import collections as compat_collections_abc
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import urllib.request as compat_urllib_request
|
import urllib.request as compat_urllib_request
|
||||||
@@ -57,11 +74,31 @@ try:
|
|||||||
except ImportError: # Python 2
|
except ImportError: # Python 2
|
||||||
import cookielib as compat_cookiejar
|
import cookielib as compat_cookiejar
|
||||||
|
|
||||||
|
if sys.version_info[0] == 2:
|
||||||
|
class compat_cookiejar_Cookie(compat_cookiejar.Cookie):
|
||||||
|
def __init__(self, version, name, value, *args, **kwargs):
|
||||||
|
if isinstance(name, compat_str):
|
||||||
|
name = name.encode()
|
||||||
|
if isinstance(value, compat_str):
|
||||||
|
value = value.encode()
|
||||||
|
compat_cookiejar.Cookie.__init__(self, version, name, value, *args, **kwargs)
|
||||||
|
else:
|
||||||
|
compat_cookiejar_Cookie = compat_cookiejar.Cookie
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import http.cookies as compat_cookies
|
import http.cookies as compat_cookies
|
||||||
except ImportError: # Python 2
|
except ImportError: # Python 2
|
||||||
import Cookie as compat_cookies
|
import Cookie as compat_cookies
|
||||||
|
|
||||||
|
if sys.version_info[0] == 2:
|
||||||
|
class compat_cookies_SimpleCookie(compat_cookies.SimpleCookie):
|
||||||
|
def load(self, rawdata):
|
||||||
|
if isinstance(rawdata, compat_str):
|
||||||
|
rawdata = str(rawdata)
|
||||||
|
return super(compat_cookies_SimpleCookie, self).load(rawdata)
|
||||||
|
else:
|
||||||
|
compat_cookies_SimpleCookie = compat_cookies.SimpleCookie
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import html.entities as compat_html_entities
|
import html.entities as compat_html_entities
|
||||||
except ImportError: # Python 2
|
except ImportError: # Python 2
|
||||||
@@ -2334,7 +2371,7 @@ except ImportError: # Python <3.4
|
|||||||
|
|
||||||
# HTMLParseError has been deprecated in Python 3.3 and removed in
|
# HTMLParseError has been deprecated in Python 3.3 and removed in
|
||||||
# Python 3.5. Introducing dummy exception for Python >3.5 for compatible
|
# Python 3.5. Introducing dummy exception for Python >3.5 for compatible
|
||||||
# and uniform cross-version exceptiong handling
|
# and uniform cross-version exception handling
|
||||||
class compat_HTMLParseError(Exception):
|
class compat_HTMLParseError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -2349,11 +2386,6 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
import BaseHTTPServer as compat_http_server
|
import BaseHTTPServer as compat_http_server
|
||||||
|
|
||||||
try:
|
|
||||||
compat_str = unicode # Python 2
|
|
||||||
except NameError:
|
|
||||||
compat_str = str
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from urllib.parse import unquote_to_bytes as compat_urllib_parse_unquote_to_bytes
|
from urllib.parse import unquote_to_bytes as compat_urllib_parse_unquote_to_bytes
|
||||||
from urllib.parse import unquote as compat_urllib_parse_unquote
|
from urllib.parse import unquote as compat_urllib_parse_unquote
|
||||||
@@ -2484,22 +2516,11 @@ except ImportError: # Python < 3.4
|
|||||||
|
|
||||||
return compat_urllib_response.addinfourl(io.BytesIO(data), headers, url)
|
return compat_urllib_response.addinfourl(io.BytesIO(data), headers, url)
|
||||||
|
|
||||||
try:
|
|
||||||
compat_basestring = basestring # Python 2
|
|
||||||
except NameError:
|
|
||||||
compat_basestring = str
|
|
||||||
|
|
||||||
try:
|
|
||||||
compat_chr = unichr # Python 2
|
|
||||||
except NameError:
|
|
||||||
compat_chr = chr
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from xml.etree.ElementTree import ParseError as compat_xml_parse_error
|
from xml.etree.ElementTree import ParseError as compat_xml_parse_error
|
||||||
except ImportError: # Python 2.6
|
except ImportError: # Python 2.6
|
||||||
from xml.parsers.expat import ExpatError as compat_xml_parse_error
|
from xml.parsers.expat import ExpatError as compat_xml_parse_error
|
||||||
|
|
||||||
|
|
||||||
etree = xml.etree.ElementTree
|
etree = xml.etree.ElementTree
|
||||||
|
|
||||||
|
|
||||||
@@ -2866,6 +2887,7 @@ else:
|
|||||||
_terminal_size = collections.namedtuple('terminal_size', ['columns', 'lines'])
|
_terminal_size = collections.namedtuple('terminal_size', ['columns', 'lines'])
|
||||||
|
|
||||||
def compat_get_terminal_size(fallback=(80, 24)):
|
def compat_get_terminal_size(fallback=(80, 24)):
|
||||||
|
from .utils import process_communicate_or_kill
|
||||||
columns = compat_getenv('COLUMNS')
|
columns = compat_getenv('COLUMNS')
|
||||||
if columns:
|
if columns:
|
||||||
columns = int(columns)
|
columns = int(columns)
|
||||||
@@ -2882,7 +2904,7 @@ else:
|
|||||||
sp = subprocess.Popen(
|
sp = subprocess.Popen(
|
||||||
['stty', 'size'],
|
['stty', 'size'],
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
out, err = sp.communicate()
|
out, err = process_communicate_or_kill(sp)
|
||||||
_lines, _columns = map(int, out.split())
|
_lines, _columns = map(int, out.split())
|
||||||
except Exception:
|
except Exception:
|
||||||
_columns, _lines = _terminal_size(*fallback)
|
_columns, _lines = _terminal_size(*fallback)
|
||||||
@@ -2942,6 +2964,24 @@ else:
|
|||||||
compat_Struct = struct.Struct
|
compat_Struct = struct.Struct
|
||||||
|
|
||||||
|
|
||||||
|
# compat_map/filter() returning an iterator, supposedly the
|
||||||
|
# same versioning as for zip below
|
||||||
|
try:
|
||||||
|
from future_builtins import map as compat_map
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
from itertools import imap as compat_map
|
||||||
|
except ImportError:
|
||||||
|
compat_map = map
|
||||||
|
|
||||||
|
try:
|
||||||
|
from future_builtins import filter as compat_filter
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
from itertools import ifilter as compat_filter
|
||||||
|
except ImportError:
|
||||||
|
compat_filter = filter
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from future_builtins import zip as compat_zip
|
from future_builtins import zip as compat_zip
|
||||||
except ImportError: # not 2.6+ or is 3.x
|
except ImportError: # not 2.6+ or is 3.x
|
||||||
@@ -2951,6 +2991,82 @@ except ImportError: # not 2.6+ or is 3.x
|
|||||||
compat_zip = zip
|
compat_zip = zip
|
||||||
|
|
||||||
|
|
||||||
|
# method renamed between Py2/3
|
||||||
|
try:
|
||||||
|
from itertools import zip_longest as compat_itertools_zip_longest
|
||||||
|
except ImportError:
|
||||||
|
from itertools import izip_longest as compat_itertools_zip_longest
|
||||||
|
|
||||||
|
|
||||||
|
# new class in collections
|
||||||
|
try:
|
||||||
|
from collections import ChainMap as compat_collections_chain_map
|
||||||
|
# Py3.3's ChainMap is deficient
|
||||||
|
if sys.version_info < (3, 4):
|
||||||
|
raise ImportError
|
||||||
|
except ImportError:
|
||||||
|
# Py <= 3.3
|
||||||
|
class compat_collections_chain_map(compat_collections_abc.MutableMapping):
|
||||||
|
|
||||||
|
maps = [{}]
|
||||||
|
|
||||||
|
def __init__(self, *maps):
|
||||||
|
self.maps = list(maps) or [{}]
|
||||||
|
|
||||||
|
def __getitem__(self, k):
|
||||||
|
for m in self.maps:
|
||||||
|
if k in m:
|
||||||
|
return m[k]
|
||||||
|
raise KeyError(k)
|
||||||
|
|
||||||
|
def __setitem__(self, k, v):
|
||||||
|
self.maps[0].__setitem__(k, v)
|
||||||
|
return
|
||||||
|
|
||||||
|
def __contains__(self, k):
|
||||||
|
return any((k in m) for m in self.maps)
|
||||||
|
|
||||||
|
def __delitem(self, k):
|
||||||
|
if k in self.maps[0]:
|
||||||
|
del self.maps[0][k]
|
||||||
|
return
|
||||||
|
raise KeyError(k)
|
||||||
|
|
||||||
|
def __delitem__(self, k):
|
||||||
|
self.__delitem(k)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return itertools.chain(*reversed(self.maps))
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(iter(self))
|
||||||
|
|
||||||
|
# to match Py3, don't del directly
|
||||||
|
def pop(self, k, *args):
|
||||||
|
if self.__contains__(k):
|
||||||
|
off = self.__getitem__(k)
|
||||||
|
self.__delitem(k)
|
||||||
|
return off
|
||||||
|
elif len(args) > 0:
|
||||||
|
return args[0]
|
||||||
|
raise KeyError(k)
|
||||||
|
|
||||||
|
def new_child(self, m=None, **kwargs):
|
||||||
|
m = m or {}
|
||||||
|
m.update(kwargs)
|
||||||
|
return compat_collections_chain_map(m, *self.maps)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parents(self):
|
||||||
|
return compat_collections_chain_map(*(self.maps[1:]))
|
||||||
|
|
||||||
|
|
||||||
|
# Pythons disagree on the type of a pattern (RegexObject, _sre.SRE_Pattern, Pattern, ...?)
|
||||||
|
compat_re_Pattern = type(re.compile(''))
|
||||||
|
# and on the type of a match
|
||||||
|
compat_re_Match = type(re.match('a', 'a'))
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info < (3, 3):
|
if sys.version_info < (3, 3):
|
||||||
def compat_b64decode(s, *args, **kwargs):
|
def compat_b64decode(s, *args, **kwargs):
|
||||||
if isinstance(s, compat_str):
|
if isinstance(s, compat_str):
|
||||||
@@ -2985,14 +3101,20 @@ __all__ = [
|
|||||||
'compat_Struct',
|
'compat_Struct',
|
||||||
'compat_b64decode',
|
'compat_b64decode',
|
||||||
'compat_basestring',
|
'compat_basestring',
|
||||||
|
'compat_casefold',
|
||||||
'compat_chr',
|
'compat_chr',
|
||||||
|
'compat_collections_abc',
|
||||||
|
'compat_collections_chain_map',
|
||||||
'compat_cookiejar',
|
'compat_cookiejar',
|
||||||
|
'compat_cookiejar_Cookie',
|
||||||
'compat_cookies',
|
'compat_cookies',
|
||||||
|
'compat_cookies_SimpleCookie',
|
||||||
'compat_ctypes_WINFUNCTYPE',
|
'compat_ctypes_WINFUNCTYPE',
|
||||||
'compat_etree_Element',
|
'compat_etree_Element',
|
||||||
'compat_etree_fromstring',
|
'compat_etree_fromstring',
|
||||||
'compat_etree_register_namespace',
|
'compat_etree_register_namespace',
|
||||||
'compat_expanduser',
|
'compat_expanduser',
|
||||||
|
'compat_filter',
|
||||||
'compat_get_terminal_size',
|
'compat_get_terminal_size',
|
||||||
'compat_getenv',
|
'compat_getenv',
|
||||||
'compat_getpass',
|
'compat_getpass',
|
||||||
@@ -3003,12 +3125,16 @@ __all__ = [
|
|||||||
'compat_input',
|
'compat_input',
|
||||||
'compat_integer_types',
|
'compat_integer_types',
|
||||||
'compat_itertools_count',
|
'compat_itertools_count',
|
||||||
|
'compat_itertools_zip_longest',
|
||||||
'compat_kwargs',
|
'compat_kwargs',
|
||||||
|
'compat_map',
|
||||||
'compat_numeric_types',
|
'compat_numeric_types',
|
||||||
'compat_ord',
|
'compat_ord',
|
||||||
'compat_os_name',
|
'compat_os_name',
|
||||||
'compat_parse_qs',
|
'compat_parse_qs',
|
||||||
'compat_print',
|
'compat_print',
|
||||||
|
'compat_re_Match',
|
||||||
|
'compat_re_Pattern',
|
||||||
'compat_realpath',
|
'compat_realpath',
|
||||||
'compat_setenv',
|
'compat_setenv',
|
||||||
'compat_shlex_quote',
|
'compat_shlex_quote',
|
||||||
|
|||||||
@@ -1,22 +1,31 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from ..utils import (
|
||||||
|
determine_protocol,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_suitable_downloader(info_dict, params={}):
|
||||||
|
info_dict['protocol'] = determine_protocol(info_dict)
|
||||||
|
info_copy = info_dict.copy()
|
||||||
|
return _get_suitable_downloader(info_copy, params)
|
||||||
|
|
||||||
|
|
||||||
|
# Some of these require get_suitable_downloader
|
||||||
from .common import FileDownloader
|
from .common import FileDownloader
|
||||||
|
from .dash import DashSegmentsFD
|
||||||
from .f4m import F4mFD
|
from .f4m import F4mFD
|
||||||
from .hls import HlsFD
|
from .hls import HlsFD
|
||||||
from .http import HttpFD
|
from .http import HttpFD
|
||||||
from .rtmp import RtmpFD
|
from .rtmp import RtmpFD
|
||||||
from .dash import DashSegmentsFD
|
|
||||||
from .rtsp import RtspFD
|
from .rtsp import RtspFD
|
||||||
from .ism import IsmFD
|
from .ism import IsmFD
|
||||||
|
from .niconico import NiconicoDmcFD
|
||||||
from .external import (
|
from .external import (
|
||||||
get_external_downloader,
|
get_external_downloader,
|
||||||
FFmpegFD,
|
FFmpegFD,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..utils import (
|
|
||||||
determine_protocol,
|
|
||||||
)
|
|
||||||
|
|
||||||
PROTOCOL_MAP = {
|
PROTOCOL_MAP = {
|
||||||
'rtmp': RtmpFD,
|
'rtmp': RtmpFD,
|
||||||
'm3u8_native': HlsFD,
|
'm3u8_native': HlsFD,
|
||||||
@@ -26,13 +35,12 @@ PROTOCOL_MAP = {
|
|||||||
'f4m': F4mFD,
|
'f4m': F4mFD,
|
||||||
'http_dash_segments': DashSegmentsFD,
|
'http_dash_segments': DashSegmentsFD,
|
||||||
'ism': IsmFD,
|
'ism': IsmFD,
|
||||||
|
'niconico_dmc': NiconicoDmcFD,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_suitable_downloader(info_dict, params={}):
|
def _get_suitable_downloader(info_dict, params={}):
|
||||||
"""Get the downloader class that can handle the info dict."""
|
"""Get the downloader class that can handle the info dict."""
|
||||||
protocol = determine_protocol(info_dict)
|
|
||||||
info_dict['protocol'] = protocol
|
|
||||||
|
|
||||||
# if (info_dict.get('start_time') or info_dict.get('end_time')) and not info_dict.get('requested_formats') and FFmpegFD.can_download(info_dict):
|
# if (info_dict.get('start_time') or info_dict.get('end_time')) and not info_dict.get('requested_formats') and FFmpegFD.can_download(info_dict):
|
||||||
# return FFmpegFD
|
# return FFmpegFD
|
||||||
@@ -42,7 +50,11 @@ def get_suitable_downloader(info_dict, params={}):
|
|||||||
ed = get_external_downloader(external_downloader)
|
ed = get_external_downloader(external_downloader)
|
||||||
if ed.can_download(info_dict):
|
if ed.can_download(info_dict):
|
||||||
return ed
|
return ed
|
||||||
|
# Avoid using unwanted args since external_downloader was rejected
|
||||||
|
if params.get('external_downloader_args'):
|
||||||
|
params['external_downloader_args'] = None
|
||||||
|
|
||||||
|
protocol = info_dict['protocol']
|
||||||
if protocol.startswith('m3u8') and info_dict.get('is_live'):
|
if protocol.startswith('m3u8') and info_dict.get('is_live'):
|
||||||
return FFmpegFD
|
return FFmpegFD
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from ..utils import (
|
|||||||
handle_youtubedl_headers,
|
handle_youtubedl_headers,
|
||||||
check_executable,
|
check_executable,
|
||||||
is_outdated_version,
|
is_outdated_version,
|
||||||
|
process_communicate_or_kill,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -104,7 +105,7 @@ class ExternalFD(FileDownloader):
|
|||||||
|
|
||||||
p = subprocess.Popen(
|
p = subprocess.Popen(
|
||||||
cmd, stderr=subprocess.PIPE)
|
cmd, stderr=subprocess.PIPE)
|
||||||
_, stderr = p.communicate()
|
_, stderr = process_communicate_or_kill(p)
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
self.to_stderr(stderr.decode('utf-8', 'replace'))
|
self.to_stderr(stderr.decode('utf-8', 'replace'))
|
||||||
return p.returncode
|
return p.returncode
|
||||||
@@ -141,7 +142,7 @@ class CurlFD(ExternalFD):
|
|||||||
|
|
||||||
# curl writes the progress to stderr so don't capture it.
|
# curl writes the progress to stderr so don't capture it.
|
||||||
p = subprocess.Popen(cmd)
|
p = subprocess.Popen(cmd)
|
||||||
p.communicate()
|
process_communicate_or_kill(p)
|
||||||
return p.returncode
|
return p.returncode
|
||||||
|
|
||||||
|
|
||||||
@@ -336,14 +337,17 @@ class FFmpegFD(ExternalFD):
|
|||||||
proc = subprocess.Popen(args, stdin=subprocess.PIPE, env=env)
|
proc = subprocess.Popen(args, stdin=subprocess.PIPE, env=env)
|
||||||
try:
|
try:
|
||||||
retval = proc.wait()
|
retval = proc.wait()
|
||||||
except KeyboardInterrupt:
|
except BaseException as e:
|
||||||
# subprocces.run would send the SIGKILL signal to ffmpeg and the
|
# subprocess.run would send the SIGKILL signal to ffmpeg and the
|
||||||
# mp4 file couldn't be played, but if we ask ffmpeg to quit it
|
# mp4 file couldn't be played, but if we ask ffmpeg to quit it
|
||||||
# produces a file that is playable (this is mostly useful for live
|
# produces a file that is playable (this is mostly useful for live
|
||||||
# streams). Note that Windows is not affected and produces playable
|
# streams). Note that Windows is not affected and produces playable
|
||||||
# files (see https://github.com/ytdl-org/youtube-dl/issues/8300).
|
# files (see https://github.com/ytdl-org/youtube-dl/issues/8300).
|
||||||
if sys.platform != 'win32':
|
if isinstance(e, KeyboardInterrupt) and sys.platform != 'win32':
|
||||||
proc.communicate(b'q')
|
process_communicate_or_kill(proc, b'q')
|
||||||
|
else:
|
||||||
|
proc.kill()
|
||||||
|
proc.wait()
|
||||||
raise
|
raise
|
||||||
return retval
|
return retval
|
||||||
|
|
||||||
|
|||||||
@@ -97,12 +97,15 @@ class FragmentFD(FileDownloader):
|
|||||||
|
|
||||||
def _download_fragment(self, ctx, frag_url, info_dict, headers=None):
|
def _download_fragment(self, ctx, frag_url, info_dict, headers=None):
|
||||||
fragment_filename = '%s-Frag%d' % (ctx['tmpfilename'], ctx['fragment_index'])
|
fragment_filename = '%s-Frag%d' % (ctx['tmpfilename'], ctx['fragment_index'])
|
||||||
success = ctx['dl'].download(fragment_filename, {
|
fragment_info_dict = {
|
||||||
'url': frag_url,
|
'url': frag_url,
|
||||||
'http_headers': headers or info_dict.get('http_headers'),
|
'http_headers': headers or info_dict.get('http_headers'),
|
||||||
})
|
}
|
||||||
|
success = ctx['dl'].download(fragment_filename, fragment_info_dict)
|
||||||
if not success:
|
if not success:
|
||||||
return False, None
|
return False, None
|
||||||
|
if fragment_info_dict.get('filetime'):
|
||||||
|
ctx['fragment_filetime'] = fragment_info_dict.get('filetime')
|
||||||
down, frag_sanitized = sanitize_open(fragment_filename, 'rb')
|
down, frag_sanitized = sanitize_open(fragment_filename, 'rb')
|
||||||
ctx['fragment_filename_sanitized'] = frag_sanitized
|
ctx['fragment_filename_sanitized'] = frag_sanitized
|
||||||
frag_content = down.read()
|
frag_content = down.read()
|
||||||
@@ -258,6 +261,13 @@ class FragmentFD(FileDownloader):
|
|||||||
downloaded_bytes = ctx['complete_frags_downloaded_bytes']
|
downloaded_bytes = ctx['complete_frags_downloaded_bytes']
|
||||||
else:
|
else:
|
||||||
self.try_rename(ctx['tmpfilename'], ctx['filename'])
|
self.try_rename(ctx['tmpfilename'], ctx['filename'])
|
||||||
|
if self.params.get('updatetime', True):
|
||||||
|
filetime = ctx.get('fragment_filetime')
|
||||||
|
if filetime:
|
||||||
|
try:
|
||||||
|
os.utime(ctx['filename'], (time.time(), filetime))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
downloaded_bytes = os.path.getsize(encodeFilename(ctx['filename']))
|
downloaded_bytes = os.path.getsize(encodeFilename(ctx['filename']))
|
||||||
|
|
||||||
self._hook_progress({
|
self._hook_progress({
|
||||||
|
|||||||
@@ -42,11 +42,13 @@ class HlsFD(FragmentFD):
|
|||||||
# no segments will definitely be appended to the end of the playlist.
|
# no segments will definitely be appended to the end of the playlist.
|
||||||
# r'#EXT-X-PLAYLIST-TYPE:EVENT', # media segments may be appended to the end of
|
# r'#EXT-X-PLAYLIST-TYPE:EVENT', # media segments may be appended to the end of
|
||||||
# # event media playlists [4]
|
# # event media playlists [4]
|
||||||
|
r'#EXT-X-MAP:', # media initialization [5]
|
||||||
|
|
||||||
# 1. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.2.4
|
# 1. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.2.4
|
||||||
# 2. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.2.2
|
# 2. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.2.2
|
||||||
# 3. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.3.2
|
# 3. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.3.2
|
||||||
# 4. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.3.5
|
# 4. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.3.5
|
||||||
|
# 5. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.2.5
|
||||||
)
|
)
|
||||||
check_results = [not re.search(feature, manifest) for feature in UNSUPPORTED_FEATURES]
|
check_results = [not re.search(feature, manifest) for feature in UNSUPPORTED_FEATURES]
|
||||||
is_aes128_enc = '#EXT-X-KEY:METHOD=AES-128' in manifest
|
is_aes128_enc = '#EXT-X-KEY:METHOD=AES-128' in manifest
|
||||||
@@ -141,7 +143,7 @@ class HlsFD(FragmentFD):
|
|||||||
count = 0
|
count = 0
|
||||||
headers = info_dict.get('http_headers', {})
|
headers = info_dict.get('http_headers', {})
|
||||||
if byte_range:
|
if byte_range:
|
||||||
headers['Range'] = 'bytes=%d-%d' % (byte_range['start'], byte_range['end'])
|
headers['Range'] = 'bytes=%d-%d' % (byte_range['start'], byte_range['end'] - 1)
|
||||||
while count <= fragment_retries:
|
while count <= fragment_retries:
|
||||||
try:
|
try:
|
||||||
success, frag_content = self._download_fragment(
|
success, frag_content = self._download_fragment(
|
||||||
@@ -170,6 +172,10 @@ class HlsFD(FragmentFD):
|
|||||||
iv = decrypt_info.get('IV') or compat_struct_pack('>8xq', media_sequence)
|
iv = decrypt_info.get('IV') or compat_struct_pack('>8xq', media_sequence)
|
||||||
decrypt_info['KEY'] = decrypt_info.get('KEY') or self.ydl.urlopen(
|
decrypt_info['KEY'] = decrypt_info.get('KEY') or self.ydl.urlopen(
|
||||||
self._prepare_url(info_dict, info_dict.get('_decryption_key_url') or decrypt_info['URI'])).read()
|
self._prepare_url(info_dict, info_dict.get('_decryption_key_url') or decrypt_info['URI'])).read()
|
||||||
|
# Don't decrypt the content in tests since the data is explicitly truncated and it's not to a valid block
|
||||||
|
# size (see https://github.com/ytdl-org/youtube-dl/pull/27660). Tests only care that the correct data downloaded,
|
||||||
|
# not what it decrypts to.
|
||||||
|
if not test:
|
||||||
frag_content = AES.new(
|
frag_content = AES.new(
|
||||||
decrypt_info['KEY'], AES.MODE_CBC, iv).decrypt(frag_content)
|
decrypt_info['KEY'], AES.MODE_CBC, iv).decrypt(frag_content)
|
||||||
self._append_fragment(ctx, frag_content)
|
self._append_fragment(ctx, frag_content)
|
||||||
|
|||||||
@@ -105,8 +105,15 @@ class HttpFD(FileDownloader):
|
|||||||
if has_range:
|
if has_range:
|
||||||
set_range(request, range_start, range_end)
|
set_range(request, range_start, range_end)
|
||||||
# Establish connection
|
# Establish connection
|
||||||
|
try:
|
||||||
try:
|
try:
|
||||||
ctx.data = self.ydl.urlopen(request)
|
ctx.data = self.ydl.urlopen(request)
|
||||||
|
except (compat_urllib_error.URLError, ) as err:
|
||||||
|
# reason may not be available, e.g. for urllib2.HTTPError on python 2.6
|
||||||
|
reason = getattr(err, 'reason', None)
|
||||||
|
if isinstance(reason, socket.timeout):
|
||||||
|
raise RetryDownload(err)
|
||||||
|
raise err
|
||||||
# When trying to resume, Content-Range HTTP header of response has to be checked
|
# When trying to resume, Content-Range HTTP header of response has to be checked
|
||||||
# to match the value of requested Range HTTP header. This is due to a webservers
|
# to match the value of requested Range HTTP header. This is due to a webservers
|
||||||
# that don't support resuming and serve a whole file with no Content-Range
|
# that don't support resuming and serve a whole file with no Content-Range
|
||||||
@@ -218,6 +225,7 @@ class HttpFD(FileDownloader):
|
|||||||
|
|
||||||
def retry(e):
|
def retry(e):
|
||||||
to_stdout = ctx.tmpfilename == '-'
|
to_stdout = ctx.tmpfilename == '-'
|
||||||
|
if ctx.stream is not None:
|
||||||
if not to_stdout:
|
if not to_stdout:
|
||||||
ctx.stream.close()
|
ctx.stream.close()
|
||||||
ctx.stream = None
|
ctx.stream = None
|
||||||
@@ -227,15 +235,17 @@ class HttpFD(FileDownloader):
|
|||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
# Download and write
|
# Download and write
|
||||||
data_block = ctx.data.read(block_size if not is_test else min(block_size, data_len - byte_counter))
|
data_block = ctx.data.read(block_size if data_len is None else min(block_size, data_len - byte_counter))
|
||||||
# socket.timeout is a subclass of socket.error but may not have
|
# socket.timeout is a subclass of socket.error but may not have
|
||||||
# errno set
|
# errno set
|
||||||
except socket.timeout as e:
|
except socket.timeout as e:
|
||||||
retry(e)
|
retry(e)
|
||||||
except socket.error as e:
|
except socket.error as e:
|
||||||
if e.errno not in (errno.ECONNRESET, errno.ETIMEDOUT):
|
# SSLError on python 2 (inherits socket.error) may have
|
||||||
raise
|
# no errno set but this error message
|
||||||
|
if e.errno in (errno.ECONNRESET, errno.ETIMEDOUT) or getattr(e, 'message', None) == 'The read operation timed out':
|
||||||
retry(e)
|
retry(e)
|
||||||
|
raise
|
||||||
|
|
||||||
byte_counter += len(data_block)
|
byte_counter += len(data_block)
|
||||||
|
|
||||||
@@ -299,7 +309,7 @@ class HttpFD(FileDownloader):
|
|||||||
'elapsed': now - ctx.start_time,
|
'elapsed': now - ctx.start_time,
|
||||||
})
|
})
|
||||||
|
|
||||||
if is_test and byte_counter == data_len:
|
if data_len is not None and byte_counter == data_len:
|
||||||
break
|
break
|
||||||
|
|
||||||
if not is_test and ctx.chunk_size and ctx.data_len is not None and byte_counter < ctx.data_len:
|
if not is_test and ctx.chunk_size and ctx.data_len is not None and byte_counter < ctx.data_len:
|
||||||
|
|||||||
66
youtube_dl/downloader/niconico.py
Normal file
66
youtube_dl/downloader/niconico.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
try:
|
||||||
|
import threading
|
||||||
|
except ImportError:
|
||||||
|
threading = None
|
||||||
|
|
||||||
|
from .common import FileDownloader
|
||||||
|
from ..downloader import get_suitable_downloader
|
||||||
|
from ..extractor.niconico import NiconicoIE
|
||||||
|
from ..utils import sanitized_Request
|
||||||
|
|
||||||
|
|
||||||
|
class NiconicoDmcFD(FileDownloader):
|
||||||
|
""" Downloading niconico douga from DMC with heartbeat """
|
||||||
|
|
||||||
|
FD_NAME = 'niconico_dmc'
|
||||||
|
|
||||||
|
def real_download(self, filename, info_dict):
|
||||||
|
self.to_screen('[%s] Downloading from DMC' % self.FD_NAME)
|
||||||
|
|
||||||
|
ie = NiconicoIE(self.ydl)
|
||||||
|
info_dict, heartbeat_info_dict = ie._get_heartbeat_info(info_dict)
|
||||||
|
|
||||||
|
fd = get_suitable_downloader(info_dict, params=self.params)(self.ydl, self.params)
|
||||||
|
for ph in self._progress_hooks:
|
||||||
|
fd.add_progress_hook(ph)
|
||||||
|
|
||||||
|
if not threading:
|
||||||
|
self.to_screen('[%s] Threading for Heartbeat not available' % self.FD_NAME)
|
||||||
|
return fd.real_download(filename, info_dict)
|
||||||
|
|
||||||
|
success = download_complete = False
|
||||||
|
timer = [None]
|
||||||
|
heartbeat_lock = threading.Lock()
|
||||||
|
heartbeat_url = heartbeat_info_dict['url']
|
||||||
|
heartbeat_data = heartbeat_info_dict['data'].encode()
|
||||||
|
heartbeat_interval = heartbeat_info_dict.get('interval', 30)
|
||||||
|
|
||||||
|
request = sanitized_Request(heartbeat_url, heartbeat_data)
|
||||||
|
|
||||||
|
def heartbeat():
|
||||||
|
try:
|
||||||
|
self.ydl.urlopen(request).read()
|
||||||
|
except Exception:
|
||||||
|
self.to_screen('[%s] Heartbeat failed' % self.FD_NAME)
|
||||||
|
|
||||||
|
with heartbeat_lock:
|
||||||
|
if not download_complete:
|
||||||
|
timer[0] = threading.Timer(heartbeat_interval, heartbeat)
|
||||||
|
timer[0].start()
|
||||||
|
|
||||||
|
heartbeat_info_dict['ping']()
|
||||||
|
self.to_screen('[%s] Heartbeat with %d second interval ...' % (self.FD_NAME, heartbeat_interval))
|
||||||
|
try:
|
||||||
|
heartbeat()
|
||||||
|
if type(fd).__name__ == 'HlsFD':
|
||||||
|
info_dict.update(ie._extract_m3u8_formats(info_dict['url'], info_dict['id'])[0])
|
||||||
|
success = fd.real_download(filename, info_dict)
|
||||||
|
finally:
|
||||||
|
if heartbeat_lock:
|
||||||
|
with heartbeat_lock:
|
||||||
|
timer[0].cancel()
|
||||||
|
download_complete = True
|
||||||
|
return success
|
||||||
@@ -89,11 +89,13 @@ class RtmpFD(FileDownloader):
|
|||||||
self.to_screen('')
|
self.to_screen('')
|
||||||
cursor_in_new_line = True
|
cursor_in_new_line = True
|
||||||
self.to_screen('[rtmpdump] ' + line)
|
self.to_screen('[rtmpdump] ' + line)
|
||||||
finally:
|
|
||||||
proc.wait()
|
|
||||||
if not cursor_in_new_line:
|
if not cursor_in_new_line:
|
||||||
self.to_screen('')
|
self.to_screen('')
|
||||||
return proc.returncode
|
return proc.wait()
|
||||||
|
except BaseException: # Including KeyboardInterrupt
|
||||||
|
proc.kill()
|
||||||
|
proc.wait()
|
||||||
|
raise
|
||||||
|
|
||||||
url = info_dict['url']
|
url = info_dict['url']
|
||||||
player_url = info_dict.get('player_url')
|
player_url = info_dict.get('player_url')
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import calendar
|
|
||||||
import re
|
import re
|
||||||
import time
|
|
||||||
|
|
||||||
from .amp import AMPIE
|
from .amp import AMPIE
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from .youtube import YoutubeIE
|
from ..utils import (
|
||||||
from ..compat import compat_urlparse
|
parse_duration,
|
||||||
|
parse_iso8601,
|
||||||
|
try_get,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AbcNewsVideoIE(AMPIE):
|
class AbcNewsVideoIE(AMPIE):
|
||||||
@@ -18,8 +19,8 @@ class AbcNewsVideoIE(AMPIE):
|
|||||||
(?:
|
(?:
|
||||||
abcnews\.go\.com/
|
abcnews\.go\.com/
|
||||||
(?:
|
(?:
|
||||||
[^/]+/video/(?P<display_id>[0-9a-z-]+)-|
|
(?:[^/]+/)*video/(?P<display_id>[0-9a-z-]+)-|
|
||||||
video/embed\?.*?\bid=
|
video/(?:embed|itemfeed)\?.*?\bid=
|
||||||
)|
|
)|
|
||||||
fivethirtyeight\.abcnews\.go\.com/video/embed/\d+/
|
fivethirtyeight\.abcnews\.go\.com/video/embed/\d+/
|
||||||
)
|
)
|
||||||
@@ -36,6 +37,8 @@ class AbcNewsVideoIE(AMPIE):
|
|||||||
'description': 'George Stephanopoulos goes one-on-one with Iranian Foreign Minister Dr. Javad Zarif.',
|
'description': 'George Stephanopoulos goes one-on-one with Iranian Foreign Minister Dr. Javad Zarif.',
|
||||||
'duration': 180,
|
'duration': 180,
|
||||||
'thumbnail': r're:^https?://.*\.jpg$',
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
|
'timestamp': 1380454200,
|
||||||
|
'upload_date': '20130929',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
# m3u8 download
|
# m3u8 download
|
||||||
@@ -47,6 +50,12 @@ class AbcNewsVideoIE(AMPIE):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'http://abcnews.go.com/2020/video/2020-husband-stands-teacher-jail-student-affairs-26119478',
|
'url': 'http://abcnews.go.com/2020/video/2020-husband-stands-teacher-jail-student-affairs-26119478',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://abcnews.go.com/video/itemfeed?id=46979033',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://abcnews.go.com/GMA/News/video/history-christmas-story-67894761',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@@ -67,28 +76,23 @@ class AbcNewsIE(InfoExtractor):
|
|||||||
_VALID_URL = r'https?://abcnews\.go\.com/(?:[^/]+/)+(?P<display_id>[0-9a-z-]+)/story\?id=(?P<id>\d+)'
|
_VALID_URL = r'https?://abcnews\.go\.com/(?:[^/]+/)+(?P<display_id>[0-9a-z-]+)/story\?id=(?P<id>\d+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://abcnews.go.com/Blotter/News/dramatic-video-rare-death-job-america/story?id=10498713#.UIhwosWHLjY',
|
# Youtube Embeds
|
||||||
|
'url': 'https://abcnews.go.com/Entertainment/peter-billingsley-child-actor-christmas-story-hollywood-power/story?id=51286501',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '10505354',
|
'id': '51286501',
|
||||||
'ext': 'flv',
|
'title': "Peter Billingsley: From child actor in 'A Christmas Story' to Hollywood power player",
|
||||||
'display_id': 'dramatic-video-rare-death-job-america',
|
'description': 'Billingsley went from a child actor to Hollywood power player.',
|
||||||
'title': 'Occupational Hazards',
|
|
||||||
'description': 'Nightline investigates the dangers that lurk at various jobs.',
|
|
||||||
'thumbnail': r're:^https?://.*\.jpg$',
|
|
||||||
'upload_date': '20100428',
|
|
||||||
'timestamp': 1272412800,
|
|
||||||
},
|
},
|
||||||
'add_ie': ['AbcNewsVideo'],
|
'playlist_count': 5,
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://abcnews.go.com/Entertainment/justin-timberlake-performs-stop-feeling-eurovision-2016/story?id=39125818',
|
'url': 'http://abcnews.go.com/Entertainment/justin-timberlake-performs-stop-feeling-eurovision-2016/story?id=39125818',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '38897857',
|
'id': '38897857',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'display_id': 'justin-timberlake-performs-stop-feeling-eurovision-2016',
|
|
||||||
'title': 'Justin Timberlake Drops Hints For Secret Single',
|
'title': 'Justin Timberlake Drops Hints For Secret Single',
|
||||||
'description': 'Lara Spencer reports the buzziest stories of the day in "GMA" Pop News.',
|
'description': 'Lara Spencer reports the buzziest stories of the day in "GMA" Pop News.',
|
||||||
'upload_date': '20160515',
|
'upload_date': '20160505',
|
||||||
'timestamp': 1463329500,
|
'timestamp': 1462442280,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
# m3u8 download
|
# m3u8 download
|
||||||
@@ -100,49 +104,55 @@ class AbcNewsIE(InfoExtractor):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'http://abcnews.go.com/Technology/exclusive-apple-ceo-tim-cook-iphone-cracking-software/story?id=37173343',
|
'url': 'http://abcnews.go.com/Technology/exclusive-apple-ceo-tim-cook-iphone-cracking-software/story?id=37173343',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# inline.type == 'video'
|
||||||
|
'url': 'http://abcnews.go.com/Technology/exclusive-apple-ceo-tim-cook-iphone-cracking-software/story?id=37173343',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
story_id = self._match_id(url)
|
||||||
display_id = mobj.group('display_id')
|
webpage = self._download_webpage(url, story_id)
|
||||||
video_id = mobj.group('id')
|
story = self._parse_json(self._search_regex(
|
||||||
|
r"window\['__abcnews__'\]\s*=\s*({.+?});",
|
||||||
|
webpage, 'data'), story_id)['page']['content']['story']['everscroll'][0]
|
||||||
|
article_contents = story.get('articleContents') or {}
|
||||||
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
def entries():
|
||||||
video_url = self._search_regex(
|
featured_video = story.get('featuredVideo') or {}
|
||||||
r'window\.abcnvideo\.url\s*=\s*"([^"]+)"', webpage, 'video URL')
|
feed = try_get(featured_video, lambda x: x['video']['feed'])
|
||||||
full_video_url = compat_urlparse.urljoin(url, video_url)
|
if feed:
|
||||||
|
yield {
|
||||||
youtube_url = YoutubeIE._extract_url(webpage)
|
'_type': 'url',
|
||||||
|
'id': featured_video.get('id'),
|
||||||
timestamp = None
|
'title': featured_video.get('name'),
|
||||||
date_str = self._html_search_regex(
|
'url': feed,
|
||||||
r'<span[^>]+class="timestamp">([^<]+)</span>',
|
'thumbnail': featured_video.get('images'),
|
||||||
webpage, 'timestamp', fatal=False)
|
'description': featured_video.get('description'),
|
||||||
if date_str:
|
'timestamp': parse_iso8601(featured_video.get('uploadDate')),
|
||||||
tz_offset = 0
|
'duration': parse_duration(featured_video.get('duration')),
|
||||||
if date_str.endswith(' ET'): # Eastern Time
|
|
||||||
tz_offset = -5
|
|
||||||
date_str = date_str[:-3]
|
|
||||||
date_formats = ['%b. %d, %Y', '%b %d, %Y, %I:%M %p']
|
|
||||||
for date_format in date_formats:
|
|
||||||
try:
|
|
||||||
timestamp = calendar.timegm(time.strptime(date_str.strip(), date_format))
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
if timestamp is not None:
|
|
||||||
timestamp -= tz_offset * 3600
|
|
||||||
|
|
||||||
entry = {
|
|
||||||
'_type': 'url_transparent',
|
|
||||||
'ie_key': AbcNewsVideoIE.ie_key(),
|
'ie_key': AbcNewsVideoIE.ie_key(),
|
||||||
'url': full_video_url,
|
|
||||||
'id': video_id,
|
|
||||||
'display_id': display_id,
|
|
||||||
'timestamp': timestamp,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if youtube_url:
|
for inline in (article_contents.get('inlines') or []):
|
||||||
entries = [entry, self.url_result(youtube_url, ie=YoutubeIE.ie_key())]
|
inline_type = inline.get('type')
|
||||||
return self.playlist_result(entries)
|
if inline_type == 'iframe':
|
||||||
|
iframe_url = try_get(inline, lambda x: x['attrs']['src'])
|
||||||
|
if iframe_url:
|
||||||
|
yield self.url_result(iframe_url)
|
||||||
|
elif inline_type == 'video':
|
||||||
|
video_id = inline.get('id')
|
||||||
|
if video_id:
|
||||||
|
yield {
|
||||||
|
'_type': 'url',
|
||||||
|
'id': video_id,
|
||||||
|
'url': 'http://abcnews.go.com/video/embed?id=' + video_id,
|
||||||
|
'thumbnail': inline.get('imgSrc') or inline.get('imgDefault'),
|
||||||
|
'description': inline.get('description'),
|
||||||
|
'duration': parse_duration(inline.get('duration')),
|
||||||
|
'ie_key': AbcNewsVideoIE.ie_key(),
|
||||||
|
}
|
||||||
|
|
||||||
return entry
|
return self.playlist_result(
|
||||||
|
entries(), story_id, article_contents.get('headline'),
|
||||||
|
article_contents.get('subHead'))
|
||||||
|
|||||||
@@ -2,21 +2,48 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import functools
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import compat_str
|
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
clean_html,
|
clean_html,
|
||||||
float_or_none,
|
clean_podcast_url,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
try_get,
|
parse_iso8601,
|
||||||
unified_timestamp,
|
|
||||||
OnDemandPagedList,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ACastIE(InfoExtractor):
|
class ACastBaseIE(InfoExtractor):
|
||||||
|
def _extract_episode(self, episode, show_info):
|
||||||
|
title = episode['title']
|
||||||
|
info = {
|
||||||
|
'id': episode['id'],
|
||||||
|
'display_id': episode.get('episodeUrl'),
|
||||||
|
'url': clean_podcast_url(episode['url']),
|
||||||
|
'title': title,
|
||||||
|
'description': clean_html(episode.get('description') or episode.get('summary')),
|
||||||
|
'thumbnail': episode.get('image'),
|
||||||
|
'timestamp': parse_iso8601(episode.get('publishDate')),
|
||||||
|
'duration': int_or_none(episode.get('duration')),
|
||||||
|
'filesize': int_or_none(episode.get('contentLength')),
|
||||||
|
'season_number': int_or_none(episode.get('season')),
|
||||||
|
'episode': title,
|
||||||
|
'episode_number': int_or_none(episode.get('episode')),
|
||||||
|
}
|
||||||
|
info.update(show_info)
|
||||||
|
return info
|
||||||
|
|
||||||
|
def _extract_show_info(self, show):
|
||||||
|
return {
|
||||||
|
'creator': show.get('author'),
|
||||||
|
'series': show.get('title'),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _call_api(self, path, video_id, query=None):
|
||||||
|
return self._download_json(
|
||||||
|
'https://feeder.acast.com/api/v1/shows/' + path, video_id, query=query)
|
||||||
|
|
||||||
|
|
||||||
|
class ACastIE(ACastBaseIE):
|
||||||
IE_NAME = 'acast'
|
IE_NAME = 'acast'
|
||||||
_VALID_URL = r'''(?x)
|
_VALID_URL = r'''(?x)
|
||||||
https?://
|
https?://
|
||||||
@@ -28,15 +55,15 @@ class ACastIE(InfoExtractor):
|
|||||||
'''
|
'''
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.acast.com/sparpodcast/2.raggarmordet-rosterurdetforflutna',
|
'url': 'https://www.acast.com/sparpodcast/2.raggarmordet-rosterurdetforflutna',
|
||||||
'md5': '16d936099ec5ca2d5869e3a813ee8dc4',
|
'md5': 'f5598f3ad1e4776fed12ec1407153e4b',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '2a92b283-1a75-4ad8-8396-499c641de0d9',
|
'id': '2a92b283-1a75-4ad8-8396-499c641de0d9',
|
||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'title': '2. Raggarmordet - Röster ur det förflutna',
|
'title': '2. Raggarmordet - Röster ur det förflutna',
|
||||||
'description': 'md5:4f81f6d8cf2e12ee21a321d8bca32db4',
|
'description': 'md5:a992ae67f4d98f1c0141598f7bebbf67',
|
||||||
'timestamp': 1477346700,
|
'timestamp': 1477346700,
|
||||||
'upload_date': '20161024',
|
'upload_date': '20161024',
|
||||||
'duration': 2766.602563,
|
'duration': 2766,
|
||||||
'creator': 'Anton Berg & Martin Johnson',
|
'creator': 'Anton Berg & Martin Johnson',
|
||||||
'series': 'Spår',
|
'series': 'Spår',
|
||||||
'episode': '2. Raggarmordet - Röster ur det förflutna',
|
'episode': '2. Raggarmordet - Röster ur det förflutna',
|
||||||
@@ -45,7 +72,7 @@ class ACastIE(InfoExtractor):
|
|||||||
'url': 'http://embed.acast.com/adambuxton/ep.12-adam-joeschristmaspodcast2015',
|
'url': 'http://embed.acast.com/adambuxton/ep.12-adam-joeschristmaspodcast2015',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://play.acast.com/s/rattegangspodden/s04e09-styckmordet-i-helenelund-del-22',
|
'url': 'https://play.acast.com/s/rattegangspodden/s04e09styckmordetihelenelund-del2-2',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://play.acast.com/s/sparpodcast/2a92b283-1a75-4ad8-8396-499c641de0d9',
|
'url': 'https://play.acast.com/s/sparpodcast/2a92b283-1a75-4ad8-8396-499c641de0d9',
|
||||||
@@ -54,40 +81,14 @@ class ACastIE(InfoExtractor):
|
|||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
channel, display_id = re.match(self._VALID_URL, url).groups()
|
channel, display_id = re.match(self._VALID_URL, url).groups()
|
||||||
s = self._download_json(
|
episode = self._call_api(
|
||||||
'https://feeder.acast.com/api/v1/shows/%s/episodes/%s' % (channel, display_id),
|
'%s/episodes/%s' % (channel, display_id),
|
||||||
display_id)
|
display_id, {'showInfo': 'true'})
|
||||||
media_url = s['url']
|
return self._extract_episode(
|
||||||
if re.search(r'[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}', display_id):
|
episode, self._extract_show_info(episode.get('show') or {}))
|
||||||
episode_url = s.get('episodeUrl')
|
|
||||||
if episode_url:
|
|
||||||
display_id = episode_url
|
|
||||||
else:
|
|
||||||
channel, display_id = re.match(self._VALID_URL, s['link']).groups()
|
|
||||||
cast_data = self._download_json(
|
|
||||||
'https://play-api.acast.com/splash/%s/%s' % (channel, display_id),
|
|
||||||
display_id)['result']
|
|
||||||
e = cast_data['episode']
|
|
||||||
title = e.get('name') or s['title']
|
|
||||||
return {
|
|
||||||
'id': compat_str(e['id']),
|
|
||||||
'display_id': display_id,
|
|
||||||
'url': media_url,
|
|
||||||
'title': title,
|
|
||||||
'description': e.get('summary') or clean_html(e.get('description') or s.get('description')),
|
|
||||||
'thumbnail': e.get('image'),
|
|
||||||
'timestamp': unified_timestamp(e.get('publishingDate') or s.get('publishDate')),
|
|
||||||
'duration': float_or_none(e.get('duration') or s.get('duration')),
|
|
||||||
'filesize': int_or_none(e.get('contentLength')),
|
|
||||||
'creator': try_get(cast_data, lambda x: x['show']['author'], compat_str),
|
|
||||||
'series': try_get(cast_data, lambda x: x['show']['name'], compat_str),
|
|
||||||
'season_number': int_or_none(e.get('seasonNumber')),
|
|
||||||
'episode': title,
|
|
||||||
'episode_number': int_or_none(e.get('episodeNumber')),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ACastChannelIE(InfoExtractor):
|
class ACastChannelIE(ACastBaseIE):
|
||||||
IE_NAME = 'acast:channel'
|
IE_NAME = 'acast:channel'
|
||||||
_VALID_URL = r'''(?x)
|
_VALID_URL = r'''(?x)
|
||||||
https?://
|
https?://
|
||||||
@@ -102,34 +103,24 @@ class ACastChannelIE(InfoExtractor):
|
|||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '4efc5294-5385-4847-98bd-519799ce5786',
|
'id': '4efc5294-5385-4847-98bd-519799ce5786',
|
||||||
'title': 'Today in Focus',
|
'title': 'Today in Focus',
|
||||||
'description': 'md5:9ba5564de5ce897faeb12963f4537a64',
|
'description': 'md5:c09ce28c91002ce4ffce71d6504abaae',
|
||||||
},
|
},
|
||||||
'playlist_mincount': 35,
|
'playlist_mincount': 200,
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://play.acast.com/s/ft-banking-weekly',
|
'url': 'http://play.acast.com/s/ft-banking-weekly',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
_API_BASE_URL = 'https://play.acast.com/api/'
|
|
||||||
_PAGE_SIZE = 10
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def suitable(cls, url):
|
def suitable(cls, url):
|
||||||
return False if ACastIE.suitable(url) else super(ACastChannelIE, cls).suitable(url)
|
return False if ACastIE.suitable(url) else super(ACastChannelIE, cls).suitable(url)
|
||||||
|
|
||||||
def _fetch_page(self, channel_slug, page):
|
|
||||||
casts = self._download_json(
|
|
||||||
self._API_BASE_URL + 'channels/%s/acasts?page=%s' % (channel_slug, page),
|
|
||||||
channel_slug, note='Download page %d of channel data' % page)
|
|
||||||
for cast in casts:
|
|
||||||
yield self.url_result(
|
|
||||||
'https://play.acast.com/s/%s/%s' % (channel_slug, cast['url']),
|
|
||||||
'ACast', cast['id'])
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
channel_slug = self._match_id(url)
|
show_slug = self._match_id(url)
|
||||||
channel_data = self._download_json(
|
show = self._call_api(show_slug, show_slug)
|
||||||
self._API_BASE_URL + 'channels/%s' % channel_slug, channel_slug)
|
show_info = self._extract_show_info(show)
|
||||||
entries = OnDemandPagedList(functools.partial(
|
entries = []
|
||||||
self._fetch_page, channel_slug), self._PAGE_SIZE)
|
for episode in (show.get('episodes') or []):
|
||||||
return self.playlist_result(entries, compat_str(
|
entries.append(self._extract_episode(episode, show_info))
|
||||||
channel_data['id']), channel_data['name'], channel_data.get('description'))
|
return self.playlist_result(
|
||||||
|
entries, show.get('id'), show.get('title'), show.get('description'))
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import random
|
|||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..aes import aes_cbc_decrypt
|
from ..aes import aes_cbc_decrypt
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
|
compat_HTTPError,
|
||||||
compat_b64decode,
|
compat_b64decode,
|
||||||
compat_ord,
|
compat_ord,
|
||||||
)
|
)
|
||||||
@@ -18,29 +19,50 @@ from ..utils import (
|
|||||||
bytes_to_long,
|
bytes_to_long,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
|
int_or_none,
|
||||||
intlist_to_bytes,
|
intlist_to_bytes,
|
||||||
long_to_bytes,
|
long_to_bytes,
|
||||||
pkcs1pad,
|
pkcs1pad,
|
||||||
strip_or_none,
|
strip_or_none,
|
||||||
urljoin,
|
try_get,
|
||||||
|
unified_strdate,
|
||||||
|
urlencode_postdata,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ADNIE(InfoExtractor):
|
class ADNIE(InfoExtractor):
|
||||||
IE_DESC = 'Anime Digital Network'
|
IE_DESC = 'Animation Digital Network'
|
||||||
_VALID_URL = r'https?://(?:www\.)?animedigitalnetwork\.fr/video/[^/]+/(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:www\.)?(?:animation|anime)digitalnetwork\.fr/video/[^/]+/(?P<id>\d+)'
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'http://animedigitalnetwork.fr/video/blue-exorcist-kyoto-saga/7778-episode-1-debut-des-hostilites',
|
'url': 'https://animationdigitalnetwork.fr/video/fruits-basket/9841-episode-1-a-ce-soir',
|
||||||
'md5': 'e497370d847fd79d9d4c74be55575c7a',
|
'md5': '1c9ef066ceb302c86f80c2b371615261',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '7778',
|
'id': '9841',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Blue Exorcist - Kyôto Saga - Épisode 1',
|
'title': 'Fruits Basket - Episode 1',
|
||||||
'description': 'md5:2f7b5aa76edbc1a7a92cedcda8a528d5',
|
'description': 'md5:14be2f72c3c96809b0ca424b0097d336',
|
||||||
}
|
'series': 'Fruits Basket',
|
||||||
}
|
'duration': 1437,
|
||||||
_BASE_URL = 'http://animedigitalnetwork.fr'
|
'release_date': '20190405',
|
||||||
_RSA_KEY = (0xc35ae1e4356b65a73b551493da94b8cb443491c0aa092a357a5aee57ffc14dda85326f42d716e539a34542a0d3f363adf16c5ec222d713d5997194030ee2e4f0d1fb328c01a81cf6868c090d50de8e169c6b13d1675b9eeed1cbc51e1fffca9b38af07f37abd790924cd3bee59d0257cfda4fe5f3f0534877e21ce5821447d1b, 65537)
|
'comment_count': int,
|
||||||
|
'average_rating': float,
|
||||||
|
'season_number': 1,
|
||||||
|
'episode': 'À ce soir !',
|
||||||
|
'episode_number': 1,
|
||||||
|
},
|
||||||
|
'skip': 'Only available in region (FR, ...)',
|
||||||
|
}, {
|
||||||
|
'url': 'http://animedigitalnetwork.fr/video/blue-exorcist-kyoto-saga/7778-episode-1-debut-des-hostilites',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
_NETRC_MACHINE = 'animationdigitalnetwork'
|
||||||
|
_BASE = 'animationdigitalnetwork.fr'
|
||||||
|
_API_BASE_URL = 'https://gw.api.' + _BASE + '/'
|
||||||
|
_PLAYER_BASE_URL = _API_BASE_URL + 'player/'
|
||||||
|
_HEADERS = {}
|
||||||
|
_LOGIN_ERR_MESSAGE = 'Unable to log in'
|
||||||
|
_RSA_KEY = (0x9B42B08905199A5CCE2026274399CA560ECB209EE9878A708B1C0812E1BB8CB5D1FB7441861147C1A1F2F3A0476DD63A9CAC20D3E983613346850AA6CB38F16DC7D720FD7D86FC6E5B3D5BBC72E14CD0BF9E869F2CEA2CCAD648F1DCE38F1FF916CEFB2D339B64AA0264372344BC775E265E8A852F88144AB0BD9AA06C1A4ABB, 65537)
|
||||||
_POS_ALIGN_MAP = {
|
_POS_ALIGN_MAP = {
|
||||||
'start': 1,
|
'start': 1,
|
||||||
'end': 3,
|
'end': 3,
|
||||||
@@ -54,26 +76,24 @@ class ADNIE(InfoExtractor):
|
|||||||
def _ass_subtitles_timecode(seconds):
|
def _ass_subtitles_timecode(seconds):
|
||||||
return '%01d:%02d:%02d.%02d' % (seconds / 3600, (seconds % 3600) / 60, seconds % 60, (seconds % 1) * 100)
|
return '%01d:%02d:%02d.%02d' % (seconds / 3600, (seconds % 3600) / 60, seconds % 60, (seconds % 1) * 100)
|
||||||
|
|
||||||
def _get_subtitles(self, sub_path, video_id):
|
def _get_subtitles(self, sub_url, video_id):
|
||||||
if not sub_path:
|
if not sub_url:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
enc_subtitles = self._download_webpage(
|
enc_subtitles = self._download_webpage(
|
||||||
urljoin(self._BASE_URL, sub_path),
|
sub_url, video_id, 'Downloading subtitles location', fatal=False) or '{}'
|
||||||
video_id, 'Downloading subtitles location', fatal=False) or '{}'
|
|
||||||
subtitle_location = (self._parse_json(enc_subtitles, video_id, fatal=False) or {}).get('location')
|
subtitle_location = (self._parse_json(enc_subtitles, video_id, fatal=False) or {}).get('location')
|
||||||
if subtitle_location:
|
if subtitle_location:
|
||||||
enc_subtitles = self._download_webpage(
|
enc_subtitles = self._download_webpage(
|
||||||
urljoin(self._BASE_URL, subtitle_location),
|
subtitle_location, video_id, 'Downloading subtitles data',
|
||||||
video_id, 'Downloading subtitles data', fatal=False,
|
fatal=False, headers={'Origin': 'https://' + self._BASE})
|
||||||
headers={'Origin': 'https://animedigitalnetwork.fr'})
|
|
||||||
if not enc_subtitles:
|
if not enc_subtitles:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# http://animedigitalnetwork.fr/components/com_vodvideo/videojs/adn-vjs.min.js
|
# http://animationdigitalnetwork.fr/components/com_vodvideo/videojs/adn-vjs.min.js
|
||||||
dec_subtitles = intlist_to_bytes(aes_cbc_decrypt(
|
dec_subtitles = intlist_to_bytes(aes_cbc_decrypt(
|
||||||
bytes_to_intlist(compat_b64decode(enc_subtitles[24:])),
|
bytes_to_intlist(compat_b64decode(enc_subtitles[24:])),
|
||||||
bytes_to_intlist(binascii.unhexlify(self._K + '4b8ef13ec1872730')),
|
bytes_to_intlist(binascii.unhexlify(self._K + '7fac1178830cfe0c')),
|
||||||
bytes_to_intlist(compat_b64decode(enc_subtitles[:24]))
|
bytes_to_intlist(compat_b64decode(enc_subtitles[:24]))
|
||||||
))
|
))
|
||||||
subtitles_json = self._parse_json(
|
subtitles_json = self._parse_json(
|
||||||
@@ -117,61 +137,103 @@ Format: Marked,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text'''
|
|||||||
}])
|
}])
|
||||||
return subtitles
|
return subtitles
|
||||||
|
|
||||||
|
def _real_initialize(self):
|
||||||
|
username, password = self._get_login_info()
|
||||||
|
if not username:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
url = self._API_BASE_URL + 'authentication/login'
|
||||||
|
access_token = (self._download_json(
|
||||||
|
url, None, 'Logging in', self._LOGIN_ERR_MESSAGE, fatal=False,
|
||||||
|
data=urlencode_postdata({
|
||||||
|
'password': password,
|
||||||
|
'rememberMe': False,
|
||||||
|
'source': 'Web',
|
||||||
|
'username': username,
|
||||||
|
})) or {}).get('accessToken')
|
||||||
|
if access_token:
|
||||||
|
self._HEADERS = {'authorization': 'Bearer ' + access_token}
|
||||||
|
except ExtractorError as e:
|
||||||
|
message = None
|
||||||
|
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:
|
||||||
|
resp = self._parse_json(
|
||||||
|
self._webpage_read_content(e.cause, url, username),
|
||||||
|
username, fatal=False) or {}
|
||||||
|
message = resp.get('message') or resp.get('code')
|
||||||
|
self.report_warning(message or self._LOGIN_ERR_MESSAGE)
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, video_id)
|
video_base_url = self._PLAYER_BASE_URL + 'video/%s/' % video_id
|
||||||
player_config = self._parse_json(self._search_regex(
|
player = self._download_json(
|
||||||
r'playerConfig\s*=\s*({.+});', webpage,
|
video_base_url + 'configuration', video_id,
|
||||||
'player config', default='{}'), video_id, fatal=False)
|
'Downloading player config JSON metadata',
|
||||||
if not player_config:
|
headers=self._HEADERS)['player']
|
||||||
config_url = urljoin(self._BASE_URL, self._search_regex(
|
options = player['options']
|
||||||
r'(?:id="player"|class="[^"]*adn-player-container[^"]*")[^>]+data-url="([^"]+)"',
|
|
||||||
webpage, 'config url'))
|
|
||||||
player_config = self._download_json(
|
|
||||||
config_url, video_id,
|
|
||||||
'Downloading player config JSON metadata')['player']
|
|
||||||
|
|
||||||
video_info = {}
|
user = options['user']
|
||||||
video_info_str = self._search_regex(
|
if not user.get('hasAccess'):
|
||||||
r'videoInfo\s*=\s*({.+});', webpage,
|
self.raise_login_required()
|
||||||
'video info', fatal=False)
|
|
||||||
if video_info_str:
|
|
||||||
video_info = self._parse_json(
|
|
||||||
video_info_str, video_id, fatal=False) or {}
|
|
||||||
|
|
||||||
options = player_config.get('options') or {}
|
token = self._download_json(
|
||||||
metas = options.get('metas') or {}
|
user.get('refreshTokenUrl') or (self._PLAYER_BASE_URL + 'refresh/token'),
|
||||||
links = player_config.get('links') or {}
|
video_id, 'Downloading access token', headers={
|
||||||
sub_path = player_config.get('subtitles')
|
'x-player-refresh-token': user['refreshToken']
|
||||||
error = None
|
}, data=b'')['token']
|
||||||
if not links:
|
|
||||||
links_url = player_config.get('linksurl') or options['videoUrl']
|
links_url = try_get(options, lambda x: x['video']['url']) or (video_base_url + 'link')
|
||||||
token = options['token']
|
|
||||||
self._K = ''.join([random.choice('0123456789abcdef') for _ in range(16)])
|
self._K = ''.join([random.choice('0123456789abcdef') for _ in range(16)])
|
||||||
message = bytes_to_intlist(json.dumps({
|
message = bytes_to_intlist(json.dumps({
|
||||||
'k': self._K,
|
'k': self._K,
|
||||||
'e': 60,
|
|
||||||
't': token,
|
't': token,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
# Sometimes authentication fails for no good reason, retry with
|
||||||
|
# a different random padding
|
||||||
|
links_data = None
|
||||||
|
for _ in range(3):
|
||||||
padded_message = intlist_to_bytes(pkcs1pad(message, 128))
|
padded_message = intlist_to_bytes(pkcs1pad(message, 128))
|
||||||
n, e = self._RSA_KEY
|
n, e = self._RSA_KEY
|
||||||
encrypted_message = long_to_bytes(pow(bytes_to_long(padded_message), e, n))
|
encrypted_message = long_to_bytes(pow(bytes_to_long(padded_message), e, n))
|
||||||
authorization = base64.b64encode(encrypted_message).decode()
|
authorization = base64.b64encode(encrypted_message).decode()
|
||||||
|
|
||||||
|
try:
|
||||||
links_data = self._download_json(
|
links_data = self._download_json(
|
||||||
urljoin(self._BASE_URL, links_url), video_id,
|
links_url, video_id, 'Downloading links JSON metadata', headers={
|
||||||
'Downloading links JSON metadata', headers={
|
'X-Player-Token': authorization
|
||||||
'Authorization': 'Bearer ' + authorization,
|
}, query={
|
||||||
|
'freeWithAds': 'true',
|
||||||
|
'adaptive': 'false',
|
||||||
|
'withMetadata': 'true',
|
||||||
|
'source': 'Web'
|
||||||
})
|
})
|
||||||
|
break
|
||||||
|
except ExtractorError as e:
|
||||||
|
if not isinstance(e.cause, compat_HTTPError):
|
||||||
|
raise e
|
||||||
|
|
||||||
|
if e.cause.code == 401:
|
||||||
|
# This usually goes away with a different random pkcs1pad, so retry
|
||||||
|
continue
|
||||||
|
|
||||||
|
error = self._parse_json(
|
||||||
|
self._webpage_read_content(e.cause, links_url, video_id),
|
||||||
|
video_id, fatal=False) or {}
|
||||||
|
message = error.get('message')
|
||||||
|
if e.cause.code == 403 and error.get('code') == 'player-bad-geolocation-country':
|
||||||
|
self.raise_geo_restricted(msg=message)
|
||||||
|
raise ExtractorError(message)
|
||||||
|
else:
|
||||||
|
raise ExtractorError('Giving up retrying')
|
||||||
|
|
||||||
links = links_data.get('links') or {}
|
links = links_data.get('links') or {}
|
||||||
metas = metas or links_data.get('meta') or {}
|
metas = links_data.get('metadata') or {}
|
||||||
sub_path = sub_path or links_data.get('subtitles') or \
|
sub_url = (links.get('subtitles') or {}).get('all')
|
||||||
'index.php?option=com_vodapi&task=subtitles.getJSON&format=json&id=' + video_id
|
video_info = links_data.get('video') or {}
|
||||||
sub_path += '&token=' + token
|
title = metas['title']
|
||||||
error = links_data.get('error')
|
|
||||||
title = metas.get('title') or video_info['title']
|
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
for format_id, qualities in links.items():
|
for format_id, qualities in (links.get('streaming') or {}).items():
|
||||||
if not isinstance(qualities, dict):
|
if not isinstance(qualities, dict):
|
||||||
continue
|
continue
|
||||||
for quality, load_balancer_url in qualities.items():
|
for quality, load_balancer_url in qualities.items():
|
||||||
@@ -189,19 +251,26 @@ Format: Marked,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text'''
|
|||||||
for f in m3u8_formats:
|
for f in m3u8_formats:
|
||||||
f['language'] = 'fr'
|
f['language'] = 'fr'
|
||||||
formats.extend(m3u8_formats)
|
formats.extend(m3u8_formats)
|
||||||
if not error:
|
|
||||||
error = options.get('error')
|
|
||||||
if not formats and error:
|
|
||||||
raise ExtractorError('%s said: %s' % (self.IE_NAME, error), expected=True)
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
video = (self._download_json(
|
||||||
|
self._API_BASE_URL + 'video/%s' % video_id, video_id,
|
||||||
|
'Downloading additional video metadata', fatal=False) or {}).get('video') or {}
|
||||||
|
show = video.get('show') or {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': strip_or_none(metas.get('summary') or video_info.get('resume')),
|
'description': strip_or_none(metas.get('summary') or video.get('summary')),
|
||||||
'thumbnail': video_info.get('image'),
|
'thumbnail': video_info.get('image') or player.get('image'),
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'subtitles': self.extract_subtitles(sub_path, video_id),
|
'subtitles': self.extract_subtitles(sub_url, video_id),
|
||||||
'episode': metas.get('subtitle') or video_info.get('videoTitle'),
|
'episode': metas.get('subtitle') or video.get('name'),
|
||||||
'series': video_info.get('playlistTitle'),
|
'episode_number': int_or_none(video.get('shortNumber')),
|
||||||
|
'series': show.get('title'),
|
||||||
|
'season_number': int_or_none(video.get('season')),
|
||||||
|
'duration': int_or_none(video_info.get('duration') or video.get('duration')),
|
||||||
|
'release_date': unified_strdate(video.get('releaseDate')),
|
||||||
|
'average_rating': float_or_none(video.get('rating') or metas.get('rating')),
|
||||||
|
'comment_count': int_or_none(video.get('commentsCount')),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,20 +5,32 @@ import re
|
|||||||
|
|
||||||
from .theplatform import ThePlatformIE
|
from .theplatform import ThePlatformIE
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
extract_attributes,
|
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
GeoRestrictedError,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
smuggle_url,
|
|
||||||
update_url_query,
|
update_url_query,
|
||||||
)
|
urlencode_postdata,
|
||||||
from ..compat import (
|
|
||||||
compat_urlparse,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AENetworksBaseIE(ThePlatformIE):
|
class AENetworksBaseIE(ThePlatformIE):
|
||||||
_THEPLATFORM_KEY = 'crazyjava'
|
_BASE_URL_REGEX = r'''(?x)https?://
|
||||||
_THEPLATFORM_SECRET = 's3cr3t'
|
(?:(?:www|play|watch)\.)?
|
||||||
|
(?P<domain>
|
||||||
|
(?:history(?:vault)?|aetv|mylifetime|lifetimemovieclub)\.com|
|
||||||
|
fyi\.tv
|
||||||
|
)/'''
|
||||||
|
_THEPLATFORM_KEY = '43jXaGRQud'
|
||||||
|
_THEPLATFORM_SECRET = 'S10BPXHMlb'
|
||||||
|
_DOMAIN_MAP = {
|
||||||
|
'history.com': ('HISTORY', 'history'),
|
||||||
|
'aetv.com': ('AETV', 'aetv'),
|
||||||
|
'mylifetime.com': ('LIFETIME', 'lifetime'),
|
||||||
|
'lifetimemovieclub.com': ('LIFETIMEMOVIECLUB', 'lmc'),
|
||||||
|
'fyi.tv': ('FYI', 'fyi'),
|
||||||
|
'historyvault.com': (None, 'historyvault'),
|
||||||
|
'biography.com': (None, 'biography'),
|
||||||
|
}
|
||||||
|
|
||||||
def _extract_aen_smil(self, smil_url, video_id, auth=None):
|
def _extract_aen_smil(self, smil_url, video_id, auth=None):
|
||||||
query = {'mbr': 'true'}
|
query = {'mbr': 'true'}
|
||||||
@@ -31,7 +43,7 @@ class AENetworksBaseIE(ThePlatformIE):
|
|||||||
'assetTypes': 'high_video_s3'
|
'assetTypes': 'high_video_s3'
|
||||||
}, {
|
}, {
|
||||||
'assetTypes': 'high_video_s3',
|
'assetTypes': 'high_video_s3',
|
||||||
'switch': 'hls_ingest_fastly'
|
'switch': 'hls_high_fastly',
|
||||||
}]
|
}]
|
||||||
formats = []
|
formats = []
|
||||||
subtitles = {}
|
subtitles = {}
|
||||||
@@ -44,6 +56,8 @@ class AENetworksBaseIE(ThePlatformIE):
|
|||||||
tp_formats, tp_subtitles = self._extract_theplatform_smil(
|
tp_formats, tp_subtitles = self._extract_theplatform_smil(
|
||||||
m_url, video_id, 'Downloading %s SMIL data' % (q.get('switch') or q['assetTypes']))
|
m_url, video_id, 'Downloading %s SMIL data' % (q.get('switch') or q['assetTypes']))
|
||||||
except ExtractorError as e:
|
except ExtractorError as e:
|
||||||
|
if isinstance(e, GeoRestrictedError):
|
||||||
|
raise
|
||||||
last_e = e
|
last_e = e
|
||||||
continue
|
continue
|
||||||
formats.extend(tp_formats)
|
formats.extend(tp_formats)
|
||||||
@@ -57,24 +71,45 @@ class AENetworksBaseIE(ThePlatformIE):
|
|||||||
'subtitles': subtitles,
|
'subtitles': subtitles,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _extract_aetn_info(self, domain, filter_key, filter_value, url):
|
||||||
|
requestor_id, brand = self._DOMAIN_MAP[domain]
|
||||||
|
result = self._download_json(
|
||||||
|
'https://feeds.video.aetnd.com/api/v2/%s/videos' % brand,
|
||||||
|
filter_value, query={'filter[%s]' % filter_key: filter_value})['results'][0]
|
||||||
|
title = result['title']
|
||||||
|
video_id = result['id']
|
||||||
|
media_url = result['publicUrl']
|
||||||
|
theplatform_metadata = self._download_theplatform_metadata(self._search_regex(
|
||||||
|
r'https?://link\.theplatform\.com/s/([^?]+)', media_url, 'theplatform_path'), video_id)
|
||||||
|
info = self._parse_theplatform_metadata(theplatform_metadata)
|
||||||
|
auth = None
|
||||||
|
if theplatform_metadata.get('AETN$isBehindWall'):
|
||||||
|
resource = self._get_mvpd_resource(
|
||||||
|
requestor_id, theplatform_metadata['title'],
|
||||||
|
theplatform_metadata.get('AETN$PPL_pplProgramId') or theplatform_metadata.get('AETN$PPL_pplProgramId_OLD'),
|
||||||
|
theplatform_metadata['ratings'][0]['rating'])
|
||||||
|
auth = self._extract_mvpd_auth(
|
||||||
|
url, video_id, requestor_id, resource)
|
||||||
|
info.update(self._extract_aen_smil(media_url, video_id, auth))
|
||||||
|
info.update({
|
||||||
|
'title': title,
|
||||||
|
'series': result.get('seriesName'),
|
||||||
|
'season_number': int_or_none(result.get('tvSeasonNumber')),
|
||||||
|
'episode_number': int_or_none(result.get('tvSeasonEpisodeNumber')),
|
||||||
|
})
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
class AENetworksIE(AENetworksBaseIE):
|
class AENetworksIE(AENetworksBaseIE):
|
||||||
IE_NAME = 'aenetworks'
|
IE_NAME = 'aenetworks'
|
||||||
IE_DESC = 'A+E Networks: A&E, Lifetime, History.com, FYI Network and History Vault'
|
IE_DESC = 'A+E Networks: A&E, Lifetime, History.com, FYI Network and History Vault'
|
||||||
_VALID_URL = r'''(?x)
|
_VALID_URL = AENetworksBaseIE._BASE_URL_REGEX + r'''(?P<id>
|
||||||
https?://
|
shows/[^/]+/season-\d+/episode-\d+|
|
||||||
(?:www\.)?
|
|
||||||
(?P<domain>
|
|
||||||
(?:history(?:vault)?|aetv|mylifetime|lifetimemovieclub)\.com|
|
|
||||||
fyi\.tv
|
|
||||||
)/
|
|
||||||
(?:
|
(?:
|
||||||
shows/(?P<show_path>[^/]+(?:/[^/]+){0,2})|
|
(?:movie|special)s/[^/]+|
|
||||||
movies/(?P<movie_display_id>[^/]+)(?:/full-movie)?|
|
(?:shows/[^/]+/)?videos
|
||||||
specials/(?P<special_display_id>[^/]+)/(?:full-special|preview-)|
|
)/[^/?#&]+
|
||||||
collections/[^/]+/(?P<collection_display_id>[^/]+)
|
)'''
|
||||||
)
|
|
||||||
'''
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.history.com/shows/mountain-men/season-1/episode-1',
|
'url': 'http://www.history.com/shows/mountain-men/season-1/episode-1',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -91,22 +126,23 @@ class AENetworksIE(AENetworksBaseIE):
|
|||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
'add_ie': ['ThePlatform'],
|
'add_ie': ['ThePlatform'],
|
||||||
}, {
|
'skip': 'This video is only available for users of participating TV providers.',
|
||||||
'url': 'http://www.history.com/shows/ancient-aliens/season-1',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '71889446852',
|
|
||||||
},
|
|
||||||
'playlist_mincount': 5,
|
|
||||||
}, {
|
|
||||||
'url': 'http://www.mylifetime.com/shows/atlanta-plastic',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'SERIES4317',
|
|
||||||
'title': 'Atlanta Plastic',
|
|
||||||
},
|
|
||||||
'playlist_mincount': 2,
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.aetv.com/shows/duck-dynasty/season-9/episode-1',
|
'url': 'http://www.aetv.com/shows/duck-dynasty/season-9/episode-1',
|
||||||
'only_matching': True
|
'info_dict': {
|
||||||
|
'id': '600587331957',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Inlawful Entry',
|
||||||
|
'description': 'md5:57c12115a2b384d883fe64ca50529e08',
|
||||||
|
'timestamp': 1452634428,
|
||||||
|
'upload_date': '20160112',
|
||||||
|
'uploader': 'AENE-NEW',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# m3u8 download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
'add_ie': ['ThePlatform'],
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.fyi.tv/shows/tiny-house-nation/season-1/episode-8',
|
'url': 'http://www.fyi.tv/shows/tiny-house-nation/season-1/episode-8',
|
||||||
'only_matching': True
|
'only_matching': True
|
||||||
@@ -117,78 +153,125 @@ class AENetworksIE(AENetworksBaseIE):
|
|||||||
'url': 'http://www.mylifetime.com/movies/center-stage-on-pointe/full-movie',
|
'url': 'http://www.mylifetime.com/movies/center-stage-on-pointe/full-movie',
|
||||||
'only_matching': True
|
'only_matching': True
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.lifetimemovieclub.com/movies/a-killer-among-us',
|
'url': 'https://watch.lifetimemovieclub.com/movies/10-year-reunion/full-movie',
|
||||||
'only_matching': True
|
'only_matching': True
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.history.com/specials/sniper-into-the-kill-zone/full-special',
|
'url': 'http://www.history.com/specials/sniper-into-the-kill-zone/full-special',
|
||||||
'only_matching': True
|
'only_matching': True
|
||||||
}, {
|
|
||||||
'url': 'https://www.historyvault.com/collections/america-the-story-of-us/westward',
|
|
||||||
'only_matching': True
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.aetv.com/specials/hunting-jonbenets-killer-the-untold-story/preview-hunting-jonbenets-killer-the-untold-story',
|
'url': 'https://www.aetv.com/specials/hunting-jonbenets-killer-the-untold-story/preview-hunting-jonbenets-killer-the-untold-story',
|
||||||
'only_matching': True
|
'only_matching': True
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.history.com/videos/history-of-valentines-day',
|
||||||
|
'only_matching': True
|
||||||
|
}, {
|
||||||
|
'url': 'https://play.aetv.com/shows/duck-dynasty/videos/best-of-duck-dynasty-getting-quack-in-shape',
|
||||||
|
'only_matching': True
|
||||||
}]
|
}]
|
||||||
_DOMAIN_TO_REQUESTOR_ID = {
|
|
||||||
'history.com': 'HISTORY',
|
|
||||||
'aetv.com': 'AETV',
|
|
||||||
'mylifetime.com': 'LIFETIME',
|
|
||||||
'lifetimemovieclub.com': 'LIFETIMEMOVIECLUB',
|
|
||||||
'fyi.tv': 'FYI',
|
|
||||||
}
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
domain, show_path, movie_display_id, special_display_id, collection_display_id = re.match(self._VALID_URL, url).groups()
|
domain, canonical = re.match(self._VALID_URL, url).groups()
|
||||||
display_id = show_path or movie_display_id or special_display_id or collection_display_id
|
return self._extract_aetn_info(domain, 'canonical', '/' + canonical, url)
|
||||||
webpage = self._download_webpage(url, display_id, headers=self.geo_verification_headers())
|
|
||||||
if show_path:
|
|
||||||
url_parts = show_path.split('/')
|
|
||||||
url_parts_len = len(url_parts)
|
|
||||||
if url_parts_len == 1:
|
|
||||||
entries = []
|
|
||||||
for season_url_path in re.findall(r'(?s)<li[^>]+data-href="(/shows/%s/season-\d+)"' % url_parts[0], webpage):
|
|
||||||
entries.append(self.url_result(
|
|
||||||
compat_urlparse.urljoin(url, season_url_path), 'AENetworks'))
|
|
||||||
if entries:
|
|
||||||
return self.playlist_result(
|
|
||||||
entries, self._html_search_meta('aetn:SeriesId', webpage),
|
|
||||||
self._html_search_meta('aetn:SeriesTitle', webpage))
|
|
||||||
else:
|
|
||||||
# single season
|
|
||||||
url_parts_len = 2
|
|
||||||
if url_parts_len == 2:
|
|
||||||
entries = []
|
|
||||||
for episode_item in re.findall(r'(?s)<[^>]+class="[^"]*(?:episode|program)-item[^"]*"[^>]*>', webpage):
|
|
||||||
episode_attributes = extract_attributes(episode_item)
|
|
||||||
episode_url = compat_urlparse.urljoin(
|
|
||||||
url, episode_attributes['data-canonical'])
|
|
||||||
entries.append(self.url_result(
|
|
||||||
episode_url, 'AENetworks',
|
|
||||||
episode_attributes.get('data-videoid') or episode_attributes.get('data-video-id')))
|
|
||||||
return self.playlist_result(
|
|
||||||
entries, self._html_search_meta('aetn:SeasonId', webpage))
|
|
||||||
|
|
||||||
video_id = self._html_search_meta('aetn:VideoID', webpage)
|
|
||||||
media_url = self._search_regex(
|
class AENetworksListBaseIE(AENetworksBaseIE):
|
||||||
[r"media_url\s*=\s*'(?P<url>[^']+)'",
|
def _call_api(self, resource, slug, brand, fields):
|
||||||
r'data-media-url=(?P<url>(?:https?:)?//[^\s>]+)',
|
return self._download_json(
|
||||||
r'data-media-url=(["\'])(?P<url>(?:(?!\1).)+?)\1'],
|
'https://yoga.appsvcs.aetnd.com/graphql',
|
||||||
webpage, 'video url', group='url')
|
slug, query={'brand': brand}, data=urlencode_postdata({
|
||||||
theplatform_metadata = self._download_theplatform_metadata(self._search_regex(
|
'query': '''{
|
||||||
r'https?://link\.theplatform\.com/s/([^?]+)', media_url, 'theplatform_path'), video_id)
|
%s(slug: "%s") {
|
||||||
info = self._parse_theplatform_metadata(theplatform_metadata)
|
%s
|
||||||
auth = None
|
}
|
||||||
if theplatform_metadata.get('AETN$isBehindWall'):
|
}''' % (resource, slug, fields),
|
||||||
requestor_id = self._DOMAIN_TO_REQUESTOR_ID[domain]
|
}))['data'][resource]
|
||||||
resource = self._get_mvpd_resource(
|
|
||||||
requestor_id, theplatform_metadata['title'],
|
def _real_extract(self, url):
|
||||||
theplatform_metadata.get('AETN$PPL_pplProgramId') or theplatform_metadata.get('AETN$PPL_pplProgramId_OLD'),
|
domain, slug = re.match(self._VALID_URL, url).groups()
|
||||||
theplatform_metadata['ratings'][0]['rating'])
|
_, brand = self._DOMAIN_MAP[domain]
|
||||||
auth = self._extract_mvpd_auth(
|
playlist = self._call_api(self._RESOURCE, slug, brand, self._FIELDS)
|
||||||
url, video_id, requestor_id, resource)
|
base_url = 'http://watch.%s' % domain
|
||||||
info.update(self._search_json_ld(webpage, video_id, fatal=False))
|
|
||||||
info.update(self._extract_aen_smil(media_url, video_id, auth))
|
entries = []
|
||||||
return info
|
for item in (playlist.get(self._ITEMS_KEY) or []):
|
||||||
|
doc = self._get_doc(item)
|
||||||
|
canonical = doc.get('canonical')
|
||||||
|
if not canonical:
|
||||||
|
continue
|
||||||
|
entries.append(self.url_result(
|
||||||
|
base_url + canonical, AENetworksIE.ie_key(), doc.get('id')))
|
||||||
|
|
||||||
|
description = None
|
||||||
|
if self._PLAYLIST_DESCRIPTION_KEY:
|
||||||
|
description = playlist.get(self._PLAYLIST_DESCRIPTION_KEY)
|
||||||
|
|
||||||
|
return self.playlist_result(
|
||||||
|
entries, playlist.get('id'),
|
||||||
|
playlist.get(self._PLAYLIST_TITLE_KEY), description)
|
||||||
|
|
||||||
|
|
||||||
|
class AENetworksCollectionIE(AENetworksListBaseIE):
|
||||||
|
IE_NAME = 'aenetworks:collection'
|
||||||
|
_VALID_URL = AENetworksBaseIE._BASE_URL_REGEX + r'(?:[^/]+/)*(?:list|collections)/(?P<id>[^/?#&]+)/?(?:[?#&]|$)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://watch.historyvault.com/list/america-the-story-of-us',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '282',
|
||||||
|
'title': 'America The Story of Us',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 12,
|
||||||
|
}, {
|
||||||
|
'url': 'https://watch.historyvault.com/shows/america-the-story-of-us-2/season-1/list/america-the-story-of-us',
|
||||||
|
'only_matching': True
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.historyvault.com/collections/mysteryquest',
|
||||||
|
'only_matching': True
|
||||||
|
}]
|
||||||
|
_RESOURCE = 'list'
|
||||||
|
_ITEMS_KEY = 'items'
|
||||||
|
_PLAYLIST_TITLE_KEY = 'display_title'
|
||||||
|
_PLAYLIST_DESCRIPTION_KEY = None
|
||||||
|
_FIELDS = '''id
|
||||||
|
display_title
|
||||||
|
items {
|
||||||
|
... on ListVideoItem {
|
||||||
|
doc {
|
||||||
|
canonical
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}'''
|
||||||
|
|
||||||
|
def _get_doc(self, item):
|
||||||
|
return item.get('doc') or {}
|
||||||
|
|
||||||
|
|
||||||
|
class AENetworksShowIE(AENetworksListBaseIE):
|
||||||
|
IE_NAME = 'aenetworks:show'
|
||||||
|
_VALID_URL = AENetworksBaseIE._BASE_URL_REGEX + r'shows/(?P<id>[^/?#&]+)/?(?:[?#&]|$)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://www.history.com/shows/ancient-aliens',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'SERIES1574',
|
||||||
|
'title': 'Ancient Aliens',
|
||||||
|
'description': 'md5:3f6d74daf2672ff3ae29ed732e37ea7f',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 150,
|
||||||
|
}]
|
||||||
|
_RESOURCE = 'series'
|
||||||
|
_ITEMS_KEY = 'episodes'
|
||||||
|
_PLAYLIST_TITLE_KEY = 'title'
|
||||||
|
_PLAYLIST_DESCRIPTION_KEY = 'description'
|
||||||
|
_FIELDS = '''description
|
||||||
|
id
|
||||||
|
title
|
||||||
|
episodes {
|
||||||
|
canonical
|
||||||
|
id
|
||||||
|
}'''
|
||||||
|
|
||||||
|
def _get_doc(self, item):
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
class HistoryTopicIE(AENetworksBaseIE):
|
class HistoryTopicIE(AENetworksBaseIE):
|
||||||
@@ -204,6 +287,7 @@ class HistoryTopicIE(AENetworksBaseIE):
|
|||||||
'description': 'md5:7b57ea4829b391995b405fa60bd7b5f7',
|
'description': 'md5:7b57ea4829b391995b405fa60bd7b5f7',
|
||||||
'timestamp': 1375819729,
|
'timestamp': 1375819729,
|
||||||
'upload_date': '20130806',
|
'upload_date': '20130806',
|
||||||
|
'uploader': 'AENE-NEW',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
# m3u8 download
|
# m3u8 download
|
||||||
@@ -212,36 +296,47 @@ class HistoryTopicIE(AENetworksBaseIE):
|
|||||||
'add_ie': ['ThePlatform'],
|
'add_ie': ['ThePlatform'],
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def theplatform_url_result(self, theplatform_url, video_id, query):
|
def _real_extract(self, url):
|
||||||
return {
|
display_id = self._match_id(url)
|
||||||
'_type': 'url_transparent',
|
return self.url_result(
|
||||||
'id': video_id,
|
'http://www.history.com/videos/' + display_id,
|
||||||
'url': smuggle_url(
|
AENetworksIE.ie_key())
|
||||||
update_url_query(theplatform_url, query),
|
|
||||||
{
|
|
||||||
'sig': {
|
class HistoryPlayerIE(AENetworksBaseIE):
|
||||||
'key': self._THEPLATFORM_KEY,
|
IE_NAME = 'history:player'
|
||||||
'secret': self._THEPLATFORM_SECRET,
|
_VALID_URL = r'https?://(?:www\.)?(?P<domain>(?:history|biography)\.com)/player/(?P<id>\d+)'
|
||||||
|
_TESTS = []
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
domain, video_id = re.match(self._VALID_URL, url).groups()
|
||||||
|
return self._extract_aetn_info(domain, 'id', video_id, url)
|
||||||
|
|
||||||
|
|
||||||
|
class BiographyIE(AENetworksBaseIE):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?biography\.com/video/(?P<id>[^/?#&]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.biography.com/video/vincent-van-gogh-full-episode-2075049808',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '30322987',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Vincent Van Gogh - Full Episode',
|
||||||
|
'description': 'A full biography about the most influential 20th century painter, Vincent Van Gogh.',
|
||||||
|
'timestamp': 1311970571,
|
||||||
|
'upload_date': '20110729',
|
||||||
|
'uploader': 'AENE-NEW',
|
||||||
},
|
},
|
||||||
'force_smil_url': True
|
'params': {
|
||||||
}),
|
# m3u8 download
|
||||||
'ie_key': 'ThePlatform',
|
'skip_download': True,
|
||||||
}
|
},
|
||||||
|
'add_ie': ['ThePlatform'],
|
||||||
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id = self._match_id(url)
|
display_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, display_id)
|
webpage = self._download_webpage(url, display_id)
|
||||||
video_id = self._search_regex(
|
player_url = self._search_regex(
|
||||||
r'<phoenix-iframe[^>]+src="[^"]+\btpid=(\d+)', webpage, 'tpid')
|
r'<phoenix-iframe[^>]+src="(%s)' % HistoryPlayerIE._VALID_URL,
|
||||||
result = self._download_json(
|
webpage, 'player URL')
|
||||||
'https://feeds.video.aetnd.com/api/v2/history/videos',
|
return self.url_result(player_url, HistoryPlayerIE.ie_key())
|
||||||
video_id, query={'filter[id]': video_id})['results'][0]
|
|
||||||
title = result['title']
|
|
||||||
info = self._extract_aen_smil(result['publicUrl'], video_id)
|
|
||||||
info.update({
|
|
||||||
'title': title,
|
|
||||||
'description': result.get('description'),
|
|
||||||
'duration': int_or_none(result.get('duration')),
|
|
||||||
'timestamp': int_or_none(result.get('added'), 1000),
|
|
||||||
})
|
|
||||||
return info
|
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ class AfreecaTVIE(InfoExtractor):
|
|||||||
video_element = video_xml.findall(compat_xpath('./track/video'))[-1]
|
video_element = video_xml.findall(compat_xpath('./track/video'))[-1]
|
||||||
if video_element is None or video_element.text is None:
|
if video_element is None or video_element.text is None:
|
||||||
raise ExtractorError(
|
raise ExtractorError(
|
||||||
'Video %s video does not exist' % video_id, expected=True)
|
'Video %s does not exist' % video_id, expected=True)
|
||||||
|
|
||||||
video_url = video_element.text.strip()
|
video_url = video_element.text.strip()
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class AliExpressLiveIE(InfoExtractor):
|
|||||||
'id': '2800002704436634',
|
'id': '2800002704436634',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'CASIMA7.22',
|
'title': 'CASIMA7.22',
|
||||||
'thumbnail': r're:http://.*\.jpg',
|
'thumbnail': r're:https?://.*\.jpg',
|
||||||
'uploader': 'CASIMA Official Store',
|
'uploader': 'CASIMA Official Store',
|
||||||
'timestamp': 1500717600,
|
'timestamp': 1500717600,
|
||||||
'upload_date': '20170722',
|
'upload_date': '20170722',
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
|
||||||
|
|
||||||
class AlJazeeraIE(InfoExtractor):
|
class AlJazeeraIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?aljazeera\.com/(?:programmes|video)/.*?/(?P<id>[^/]+)\.html'
|
_VALID_URL = r'https?://(?:www\.)?aljazeera\.com/(?P<type>program/[^/]+|(?:feature|video)s)/\d{4}/\d{1,2}/\d{1,2}/(?P<id>[^/?&#]+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.aljazeera.com/programmes/the-slum/2014/08/deliverance-201482883754237240.html',
|
'url': 'https://www.aljazeera.com/program/episode/2014/9/19/deliverance',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '3792260579001',
|
'id': '3792260579001',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
@@ -20,14 +23,34 @@ class AlJazeeraIE(InfoExtractor):
|
|||||||
'add_ie': ['BrightcoveNew'],
|
'add_ie': ['BrightcoveNew'],
|
||||||
'skip': 'Not accessible from Travis CI server',
|
'skip': 'Not accessible from Travis CI server',
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.aljazeera.com/video/news/2017/05/sierra-leone-709-carat-diamond-auctioned-170511100111930.html',
|
'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,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/665003303001/default_default/index.html?videoId=%s'
|
BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/%s/%s_default/index.html?videoId=%s'
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
program_name = self._match_id(url)
|
post_type, name = re.match(self._VALID_URL, url).groups()
|
||||||
webpage = self._download_webpage(url, program_name)
|
post_type = {
|
||||||
brightcove_id = self._search_regex(
|
'features': 'post',
|
||||||
r'RenderPagesVideo\(\'(.+?)\'', webpage, 'brightcove id')
|
'program': 'episode',
|
||||||
return self.url_result(self.BRIGHTCOVE_URL_TEMPLATE % brightcove_id, 'BrightcoveNew', brightcove_id)
|
'videos': 'video',
|
||||||
|
}[post_type.split('/')[0]]
|
||||||
|
video = self._download_json(
|
||||||
|
'https://www.aljazeera.com/graphql', name, query={
|
||||||
|
'operationName': 'SingleArticleQuery',
|
||||||
|
'variables': json.dumps({
|
||||||
|
'name': name,
|
||||||
|
'postType': post_type,
|
||||||
|
}),
|
||||||
|
}, headers={
|
||||||
|
'wp-site': 'aje',
|
||||||
|
})['data']['article']['video']
|
||||||
|
video_id = video['id']
|
||||||
|
account_id = video.get('accountId') or '665003303001'
|
||||||
|
player_id = video.get('playerId') or 'BkeSH5BDb'
|
||||||
|
return self.url_result(
|
||||||
|
self.BRIGHTCOVE_URL_TEMPLATE % (account_id, player_id, video_id),
|
||||||
|
'BrightcoveNew', video_id)
|
||||||
|
|||||||
89
youtube_dl/extractor/alsace20tv.py
Normal file
89
youtube_dl/extractor/alsace20tv.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
clean_html,
|
||||||
|
dict_get,
|
||||||
|
get_element_by_class,
|
||||||
|
int_or_none,
|
||||||
|
unified_strdate,
|
||||||
|
url_or_none,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Alsace20TVIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?alsace20\.tv/(?:[\w-]+/)+[\w-]+-(?P<id>[\w]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.alsace20.tv/VOD/Actu/JT/Votre-JT-jeudi-3-fevrier-lyNHCXpYJh.html',
|
||||||
|
# 'md5': 'd91851bf9af73c0ad9b2cdf76c127fbb',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'lyNHCXpYJh',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'description': 'md5:fc0bc4a0692d3d2dba4524053de4c7b7',
|
||||||
|
'title': 'Votre JT du jeudi 3 février',
|
||||||
|
'upload_date': '20220203',
|
||||||
|
'thumbnail': r're:https?://.+\.jpg',
|
||||||
|
'duration': 1073,
|
||||||
|
'view_count': int,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'format': 'bestvideo',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _extract_video(self, video_id, url=None):
|
||||||
|
info = self._download_json(
|
||||||
|
'https://www.alsace20.tv/visionneuse/visio_v9_js.php?key=%s&habillage=0&mode=html' % (video_id, ),
|
||||||
|
video_id) or {}
|
||||||
|
title = info['titre']
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
for res, fmt_url in (info.get('files') or {}).items():
|
||||||
|
formats.extend(
|
||||||
|
self._extract_smil_formats(fmt_url, video_id, fatal=False)
|
||||||
|
if '/smil:_' in fmt_url
|
||||||
|
else self._extract_mpd_formats(fmt_url, video_id, mpd_id=res, fatal=False))
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
webpage = (url and self._download_webpage(url, video_id, fatal=False)) or ''
|
||||||
|
thumbnail = url_or_none(dict_get(info, ('image', 'preview', )) or self._og_search_thumbnail(webpage))
|
||||||
|
upload_date = self._search_regex(r'/(\d{6})_', thumbnail, 'upload_date', default=None)
|
||||||
|
upload_date = unified_strdate('20%s-%s-%s' % (upload_date[:2], upload_date[2:4], upload_date[4:])) if upload_date else None
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'formats': formats,
|
||||||
|
'description': clean_html(get_element_by_class('wysiwyg', webpage)),
|
||||||
|
'upload_date': upload_date,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
|
'duration': int_or_none(self._og_search_property('video:duration', webpage) if webpage else None),
|
||||||
|
'view_count': int_or_none(info.get('nb_vues')),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
return self._extract_video(video_id, url)
|
||||||
|
|
||||||
|
|
||||||
|
class Alsace20TVEmbedIE(Alsace20TVIE):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?alsace20\.tv/emb/(?P<id>[\w]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.alsace20.tv/emb/lyNHCXpYJh',
|
||||||
|
# 'md5': 'd91851bf9af73c0ad9b2cdf76c127fbb',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'lyNHCXpYJh',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Votre JT du jeudi 3 février',
|
||||||
|
'upload_date': '20220203',
|
||||||
|
'thumbnail': r're:https?://.+\.jpg',
|
||||||
|
'view_count': int,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'format': 'bestvideo',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
return self._extract_video(video_id)
|
||||||
103
youtube_dl/extractor/amara.py
Normal file
103
youtube_dl/extractor/amara.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from .youtube import YoutubeIE
|
||||||
|
from .vimeo import VimeoIE
|
||||||
|
from ..utils import (
|
||||||
|
int_or_none,
|
||||||
|
parse_iso8601,
|
||||||
|
update_url_query,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AmaraIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?amara\.org/(?:\w+/)?videos/(?P<id>\w+)'
|
||||||
|
_TESTS = [{
|
||||||
|
# Youtube
|
||||||
|
'url': 'https://amara.org/en/videos/jVx79ZKGK1ky/info/why-jury-trials-are-becoming-less-common/?tab=video',
|
||||||
|
'md5': 'ea10daf2b6154b8c1ecf9922aca5e8ae',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'h6ZuVdvYnfE',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Why jury trials are becoming less common',
|
||||||
|
'description': 'md5:a61811c319943960b6ab1c23e0cbc2c1',
|
||||||
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
|
'subtitles': dict,
|
||||||
|
'upload_date': '20160813',
|
||||||
|
'uploader': 'PBS NewsHour',
|
||||||
|
'uploader_id': 'PBSNewsHour',
|
||||||
|
'timestamp': 1549639570,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
# Vimeo
|
||||||
|
'url': 'https://amara.org/en/videos/kYkK1VUTWW5I/info/vimeo-at-ces-2011',
|
||||||
|
'md5': '99392c75fa05d432a8f11df03612195e',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '18622084',
|
||||||
|
'ext': 'mov',
|
||||||
|
'title': 'Vimeo at CES 2011!',
|
||||||
|
'description': 'md5:d41d8cd98f00b204e9800998ecf8427e',
|
||||||
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
|
'subtitles': dict,
|
||||||
|
'timestamp': 1294763658,
|
||||||
|
'upload_date': '20110111',
|
||||||
|
'uploader': 'Sam Morrill',
|
||||||
|
'uploader_id': 'sammorrill'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
# Direct Link
|
||||||
|
'url': 'https://amara.org/en/videos/s8KL7I3jLmh6/info/the-danger-of-a-single-story/',
|
||||||
|
'md5': 'd3970f08512738ee60c5807311ff5d3f',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 's8KL7I3jLmh6',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'The danger of a single story',
|
||||||
|
'description': 'md5:d769b31139c3b8bb5be9177f62ea3f23',
|
||||||
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
|
'subtitles': dict,
|
||||||
|
'upload_date': '20091007',
|
||||||
|
'timestamp': 1254942511,
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
meta = self._download_json(
|
||||||
|
'https://amara.org/api/videos/%s/' % video_id,
|
||||||
|
video_id, query={'format': 'json'})
|
||||||
|
title = meta['title']
|
||||||
|
video_url = meta['all_urls'][0]
|
||||||
|
|
||||||
|
subtitles = {}
|
||||||
|
for language in (meta.get('languages') or []):
|
||||||
|
subtitles_uri = language.get('subtitles_uri')
|
||||||
|
if not (subtitles_uri and language.get('published')):
|
||||||
|
continue
|
||||||
|
subtitle = subtitles.setdefault(language.get('code') or 'en', [])
|
||||||
|
for f in ('json', 'srt', 'vtt'):
|
||||||
|
subtitle.append({
|
||||||
|
'ext': f,
|
||||||
|
'url': update_url_query(subtitles_uri, {'format': f}),
|
||||||
|
})
|
||||||
|
|
||||||
|
info = {
|
||||||
|
'url': video_url,
|
||||||
|
'id': video_id,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
'title': title,
|
||||||
|
'description': meta.get('description'),
|
||||||
|
'thumbnail': meta.get('thumbnail'),
|
||||||
|
'duration': int_or_none(meta.get('duration')),
|
||||||
|
'timestamp': parse_iso8601(meta.get('created')),
|
||||||
|
}
|
||||||
|
|
||||||
|
for ie in (YoutubeIE, VimeoIE):
|
||||||
|
if ie.suitable(video_url):
|
||||||
|
info.update({
|
||||||
|
'_type': 'url_transparent',
|
||||||
|
'ie_key': ie.ie_key(),
|
||||||
|
})
|
||||||
|
break
|
||||||
|
|
||||||
|
return info
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
from .theplatform import ThePlatformIE
|
from .theplatform import ThePlatformIE
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
int_or_none,
|
int_or_none,
|
||||||
@@ -11,25 +13,22 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class AMCNetworksIE(ThePlatformIE):
|
class AMCNetworksIE(ThePlatformIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?(?:amc|bbcamerica|ifc|(?:we|sundance)tv)\.com/(?:movies|shows(?:/[^/]+)+)/(?P<id>[^/?#]+)'
|
_VALID_URL = r'https?://(?:www\.)?(?P<site>amc|bbcamerica|ifc|(?:we|sundance)tv)\.com/(?P<id>(?:movies|shows(?:/[^/]+)+)/[^/?#&]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.ifc.com/shows/maron/season-04/episode-01/step-1',
|
'url': 'https://www.bbcamerica.com/shows/the-graham-norton-show/videos/tina-feys-adorable-airline-themed-family-dinner--51631',
|
||||||
'md5': '',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 's3MX01Nl4vPH',
|
'id': '4Lq1dzOnZGt0',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Maron - Season 4 - Step 1',
|
'title': "The Graham Norton Show - Season 28 - Tina Fey's Adorable Airline-Themed Family Dinner",
|
||||||
'description': 'In denial about his current situation, Marc is reluctantly convinced by his friends to enter rehab. Starring Marc Maron and Constance Zimmer.',
|
'description': "It turns out child stewardesses are very generous with the wine! All-new episodes of 'The Graham Norton Show' premiere Fridays at 11/10c on BBC America.",
|
||||||
'age_limit': 17,
|
'upload_date': '20201120',
|
||||||
'upload_date': '20160505',
|
'timestamp': 1605904350,
|
||||||
'timestamp': 1462468831,
|
|
||||||
'uploader': 'AMCN',
|
'uploader': 'AMCN',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
# m3u8 download
|
# m3u8 download
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
'skip': 'Requires TV provider accounts',
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.bbcamerica.com/shows/the-hunt/full-episodes/season-1/episode-01-the-hardest-challenge',
|
'url': 'http://www.bbcamerica.com/shows/the-hunt/full-episodes/season-1/episode-01-the-hardest-challenge',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
@@ -55,32 +54,34 @@ class AMCNetworksIE(ThePlatformIE):
|
|||||||
'url': 'https://www.sundancetv.com/shows/riviera/full-episodes/season-1/episode-01-episode-1',
|
'url': 'https://www.sundancetv.com/shows/riviera/full-episodes/season-1/episode-01-episode-1',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
_REQUESTOR_ID_MAP = {
|
||||||
|
'amc': 'AMC',
|
||||||
|
'bbcamerica': 'BBCA',
|
||||||
|
'ifc': 'IFC',
|
||||||
|
'sundancetv': 'SUNDANCE',
|
||||||
|
'wetv': 'WETV',
|
||||||
|
}
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id = self._match_id(url)
|
site, display_id = re.match(self._VALID_URL, url).groups()
|
||||||
webpage = self._download_webpage(url, display_id)
|
requestor_id = self._REQUESTOR_ID_MAP[site]
|
||||||
|
properties = self._download_json(
|
||||||
|
'https://content-delivery-gw.svc.ds.amcn.com/api/v2/content/amcn/%s/url/%s' % (requestor_id.lower(), display_id),
|
||||||
|
display_id)['data']['properties']
|
||||||
query = {
|
query = {
|
||||||
'mbr': 'true',
|
'mbr': 'true',
|
||||||
'manifest': 'm3u',
|
'manifest': 'm3u',
|
||||||
}
|
}
|
||||||
media_url = self._search_regex(
|
tp_path = 'M_UwQC/media/' + properties['videoPid']
|
||||||
r'window\.platformLinkURL\s*=\s*[\'"]([^\'"]+)',
|
media_url = 'https://link.theplatform.com/s/' + tp_path
|
||||||
webpage, 'media url')
|
theplatform_metadata = self._download_theplatform_metadata(tp_path, display_id)
|
||||||
theplatform_metadata = self._download_theplatform_metadata(self._search_regex(
|
|
||||||
r'link\.theplatform\.com/s/([^?]+)',
|
|
||||||
media_url, 'theplatform_path'), display_id)
|
|
||||||
info = self._parse_theplatform_metadata(theplatform_metadata)
|
info = self._parse_theplatform_metadata(theplatform_metadata)
|
||||||
video_id = theplatform_metadata['pid']
|
video_id = theplatform_metadata['pid']
|
||||||
title = theplatform_metadata['title']
|
title = theplatform_metadata['title']
|
||||||
rating = try_get(
|
rating = try_get(
|
||||||
theplatform_metadata, lambda x: x['ratings'][0]['rating'])
|
theplatform_metadata, lambda x: x['ratings'][0]['rating'])
|
||||||
auth_required = self._search_regex(
|
video_category = properties.get('videoCategory')
|
||||||
r'window\.authRequired\s*=\s*(true|false);',
|
if video_category and video_category.endswith('-Auth'):
|
||||||
webpage, 'auth required')
|
|
||||||
if auth_required == 'true':
|
|
||||||
requestor_id = self._search_regex(
|
|
||||||
r'window\.requestor_id\s*=\s*[\'"]([^\'"]+)',
|
|
||||||
webpage, 'requestor id')
|
|
||||||
resource = self._get_mvpd_resource(
|
resource = self._get_mvpd_resource(
|
||||||
requestor_id, title, video_id, rating)
|
requestor_id, title, video_id, rating)
|
||||||
query['auth'] = self._extract_mvpd_auth(
|
query['auth'] = self._extract_mvpd_auth(
|
||||||
|
|||||||
@@ -1,82 +1,159 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
clean_html,
|
clean_html,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
js_to_json,
|
|
||||||
try_get,
|
try_get,
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
|
unified_timestamp,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AmericasTestKitchenIE(InfoExtractor):
|
class AmericasTestKitchenIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?americastestkitchen\.com/(?:episode|videos)/(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:www\.)?(?:americastestkitchen|cooks(?:country|illustrated))\.com/(?P<resource_type>episode|videos)/(?P<id>\d+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.americastestkitchen.com/episode/582-weeknight-japanese-suppers',
|
'url': 'https://www.americastestkitchen.com/episode/582-weeknight-japanese-suppers',
|
||||||
'md5': 'b861c3e365ac38ad319cfd509c30577f',
|
'md5': 'b861c3e365ac38ad319cfd509c30577f',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '5b400b9ee338f922cb06450c',
|
'id': '5b400b9ee338f922cb06450c',
|
||||||
'title': 'Weeknight Japanese Suppers',
|
'title': 'Japanese Suppers',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'description': 'md5:3d0c1a44bb3b27607ce82652db25b4a8',
|
'description': 'md5:64e606bfee910627efc4b5f050de92b3',
|
||||||
'thumbnail': r're:^https?://',
|
'thumbnail': r're:^https?://',
|
||||||
'timestamp': 1523664000,
|
'timestamp': 1523318400,
|
||||||
'upload_date': '20180414',
|
'upload_date': '20180410',
|
||||||
'release_date': '20180414',
|
'release_date': '20180410',
|
||||||
'series': "America's Test Kitchen",
|
'series': "America's Test Kitchen",
|
||||||
'season_number': 18,
|
'season_number': 18,
|
||||||
'episode': 'Weeknight Japanese Suppers',
|
'episode': 'Japanese Suppers',
|
||||||
'episode_number': 15,
|
'episode_number': 15,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
# Metadata parsing behaves differently for newer episodes (705) as opposed to older episodes (582 above)
|
||||||
|
'url': 'https://www.americastestkitchen.com/episode/705-simple-chicken-dinner',
|
||||||
|
'md5': '06451608c57651e985a498e69cec17e5',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '5fbe8c61bda2010001c6763b',
|
||||||
|
'title': 'Simple Chicken Dinner',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'description': 'md5:eb68737cc2fd4c26ca7db30139d109e7',
|
||||||
|
'thumbnail': r're:^https?://',
|
||||||
|
'timestamp': 1610755200,
|
||||||
|
'upload_date': '20210116',
|
||||||
|
'release_date': '20210116',
|
||||||
|
'series': "America's Test Kitchen",
|
||||||
|
'season_number': 21,
|
||||||
|
'episode': 'Simple Chicken Dinner',
|
||||||
|
'episode_number': 3,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.americastestkitchen.com/videos/3420-pan-seared-salmon',
|
'url': 'https://www.americastestkitchen.com/videos/3420-pan-seared-salmon',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.cookscountry.com/episode/564-when-only-chocolate-will-do',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.cooksillustrated.com/videos/4478-beef-wellington',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
resource_type, video_id = re.match(self._VALID_URL, url).groups()
|
||||||
|
is_episode = resource_type == 'episode'
|
||||||
|
if is_episode:
|
||||||
|
resource_type = 'episodes'
|
||||||
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
resource = self._download_json(
|
||||||
|
'https://www.americastestkitchen.com/api/v6/%s/%s' % (resource_type, video_id), video_id)
|
||||||
video_data = self._parse_json(
|
video = resource['video'] if is_episode else resource
|
||||||
self._search_regex(
|
episode = resource if is_episode else resource.get('episode') or {}
|
||||||
r'window\.__INITIAL_STATE__\s*=\s*({.+?})\s*;\s*</script>',
|
|
||||||
webpage, 'initial context'),
|
|
||||||
video_id, js_to_json)
|
|
||||||
|
|
||||||
ep_data = try_get(
|
|
||||||
video_data,
|
|
||||||
(lambda x: x['episodeDetail']['content']['data'],
|
|
||||||
lambda x: x['videoDetail']['content']['data']), dict)
|
|
||||||
ep_meta = ep_data.get('full_video', {})
|
|
||||||
|
|
||||||
zype_id = ep_data.get('zype_id') or ep_meta['zype_id']
|
|
||||||
|
|
||||||
title = ep_data.get('title') or ep_meta.get('title')
|
|
||||||
description = clean_html(ep_meta.get('episode_description') or ep_data.get(
|
|
||||||
'description') or ep_meta.get('description'))
|
|
||||||
thumbnail = try_get(ep_meta, lambda x: x['photo']['image_url'])
|
|
||||||
release_date = unified_strdate(ep_data.get('aired_at'))
|
|
||||||
|
|
||||||
season_number = int_or_none(ep_meta.get('season_number'))
|
|
||||||
episode = ep_meta.get('title')
|
|
||||||
episode_number = int_or_none(ep_meta.get('episode_number'))
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'_type': 'url_transparent',
|
'_type': 'url_transparent',
|
||||||
'url': 'https://player.zype.com/embed/%s.js?api_key=jZ9GUhRmxcPvX7M3SlfejB6Hle9jyHTdk2jVxG7wOHPLODgncEKVdPYBhuz9iWXQ' % zype_id,
|
'url': 'https://player.zype.com/embed/%s.js?api_key=jZ9GUhRmxcPvX7M3SlfejB6Hle9jyHTdk2jVxG7wOHPLODgncEKVdPYBhuz9iWXQ' % video['zypeId'],
|
||||||
'ie_key': 'Zype',
|
'ie_key': 'Zype',
|
||||||
'title': title,
|
'description': clean_html(video.get('description')),
|
||||||
'description': description,
|
'timestamp': unified_timestamp(video.get('publishDate')),
|
||||||
'thumbnail': thumbnail,
|
'release_date': unified_strdate(video.get('publishDate')),
|
||||||
'release_date': release_date,
|
'episode_number': int_or_none(episode.get('number')),
|
||||||
'series': "America's Test Kitchen",
|
'season_number': int_or_none(episode.get('season')),
|
||||||
'season_number': season_number,
|
'series': try_get(episode, lambda x: x['show']['title']),
|
||||||
'episode': episode,
|
'episode': episode.get('title'),
|
||||||
'episode_number': episode_number,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AmericasTestKitchenSeasonIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?(?P<show>americastestkitchen|cookscountry)\.com/episodes/browse/season_(?P<id>\d+)'
|
||||||
|
_TESTS = [{
|
||||||
|
# ATK Season
|
||||||
|
'url': 'https://www.americastestkitchen.com/episodes/browse/season_1',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'season_1',
|
||||||
|
'title': 'Season 1',
|
||||||
|
},
|
||||||
|
'playlist_count': 13,
|
||||||
|
}, {
|
||||||
|
# Cooks Country Season
|
||||||
|
'url': 'https://www.cookscountry.com/episodes/browse/season_12',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'season_12',
|
||||||
|
'title': 'Season 12',
|
||||||
|
},
|
||||||
|
'playlist_count': 13,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
show_name, season_number = re.match(self._VALID_URL, url).groups()
|
||||||
|
season_number = int(season_number)
|
||||||
|
|
||||||
|
slug = 'atk' if show_name == 'americastestkitchen' else 'cco'
|
||||||
|
|
||||||
|
season = 'Season %d' % season_number
|
||||||
|
|
||||||
|
season_search = self._download_json(
|
||||||
|
'https://y1fnzxui30-dsn.algolia.net/1/indexes/everest_search_%s_season_desc_production' % slug,
|
||||||
|
season, headers={
|
||||||
|
'Origin': 'https://www.%s.com' % show_name,
|
||||||
|
'X-Algolia-API-Key': '8d504d0099ed27c1b73708d22871d805',
|
||||||
|
'X-Algolia-Application-Id': 'Y1FNZXUI30',
|
||||||
|
}, query={
|
||||||
|
'facetFilters': json.dumps([
|
||||||
|
'search_season_list:' + season,
|
||||||
|
'search_document_klass:episode',
|
||||||
|
'search_show_slug:' + slug,
|
||||||
|
]),
|
||||||
|
'attributesToRetrieve': 'description,search_%s_episode_number,search_document_date,search_url,title' % slug,
|
||||||
|
'attributesToHighlight': '',
|
||||||
|
'hitsPerPage': 1000,
|
||||||
|
})
|
||||||
|
|
||||||
|
def entries():
|
||||||
|
for episode in (season_search.get('hits') or []):
|
||||||
|
search_url = episode.get('search_url')
|
||||||
|
if not search_url:
|
||||||
|
continue
|
||||||
|
yield {
|
||||||
|
'_type': 'url',
|
||||||
|
'url': 'https://www.%s.com%s' % (show_name, search_url),
|
||||||
|
'id': try_get(episode, lambda e: e['objectID'].split('_')[-1]),
|
||||||
|
'title': episode.get('title'),
|
||||||
|
'description': episode.get('description'),
|
||||||
|
'timestamp': unified_timestamp(episode.get('search_document_date')),
|
||||||
|
'season_number': season_number,
|
||||||
|
'episode_number': int_or_none(episode.get('search_%s_episode_number' % slug)),
|
||||||
|
'ie_key': AmericasTestKitchenIE.ie_key(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.playlist_result(
|
||||||
|
entries(), 'season_%d' % season_number, season)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from ..utils import (
|
|||||||
int_or_none,
|
int_or_none,
|
||||||
mimetype2ext,
|
mimetype2ext,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
|
unified_timestamp,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -88,7 +89,7 @@ class AMPIE(InfoExtractor):
|
|||||||
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
timestamp = parse_iso8601(item.get('pubDate'), ' ') or parse_iso8601(item.get('dc-date'))
|
timestamp = unified_timestamp(item.get('pubDate'), ' ') or parse_iso8601(item.get('dc-date'))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
|
|||||||
@@ -116,8 +116,6 @@ class AnimeOnDemandIE(InfoExtractor):
|
|||||||
r'(?s)<div[^>]+itemprop="description"[^>]*>(.+?)</div>',
|
r'(?s)<div[^>]+itemprop="description"[^>]*>(.+?)</div>',
|
||||||
webpage, 'anime description', default=None)
|
webpage, 'anime description', default=None)
|
||||||
|
|
||||||
entries = []
|
|
||||||
|
|
||||||
def extract_info(html, video_id, num=None):
|
def extract_info(html, video_id, num=None):
|
||||||
title, description = [None] * 2
|
title, description = [None] * 2
|
||||||
formats = []
|
formats = []
|
||||||
@@ -233,7 +231,7 @@ class AnimeOnDemandIE(InfoExtractor):
|
|||||||
self._sort_formats(info['formats'])
|
self._sort_formats(info['formats'])
|
||||||
f = common_info.copy()
|
f = common_info.copy()
|
||||||
f.update(info)
|
f.update(info)
|
||||||
entries.append(f)
|
yield f
|
||||||
|
|
||||||
# Extract teaser/trailer only when full episode is not available
|
# Extract teaser/trailer only when full episode is not available
|
||||||
if not info['formats']:
|
if not info['formats']:
|
||||||
@@ -247,7 +245,7 @@ class AnimeOnDemandIE(InfoExtractor):
|
|||||||
'title': m.group('title'),
|
'title': m.group('title'),
|
||||||
'url': urljoin(url, m.group('href')),
|
'url': urljoin(url, m.group('href')),
|
||||||
})
|
})
|
||||||
entries.append(f)
|
yield f
|
||||||
|
|
||||||
def extract_episodes(html):
|
def extract_episodes(html):
|
||||||
for num, episode_html in enumerate(re.findall(
|
for num, episode_html in enumerate(re.findall(
|
||||||
@@ -275,7 +273,8 @@ class AnimeOnDemandIE(InfoExtractor):
|
|||||||
'episode_number': episode_number,
|
'episode_number': episode_number,
|
||||||
}
|
}
|
||||||
|
|
||||||
extract_entries(episode_html, video_id, common_info)
|
for e in extract_entries(episode_html, video_id, common_info):
|
||||||
|
yield e
|
||||||
|
|
||||||
def extract_film(html, video_id):
|
def extract_film(html, video_id):
|
||||||
common_info = {
|
common_info = {
|
||||||
@@ -283,11 +282,18 @@ class AnimeOnDemandIE(InfoExtractor):
|
|||||||
'title': anime_title,
|
'title': anime_title,
|
||||||
'description': anime_description,
|
'description': anime_description,
|
||||||
}
|
}
|
||||||
extract_entries(html, video_id, common_info)
|
for e in extract_entries(html, video_id, common_info):
|
||||||
|
yield e
|
||||||
|
|
||||||
extract_episodes(webpage)
|
def entries():
|
||||||
|
has_episodes = False
|
||||||
|
for e in extract_episodes(webpage):
|
||||||
|
has_episodes = True
|
||||||
|
yield e
|
||||||
|
|
||||||
if not entries:
|
if not has_episodes:
|
||||||
extract_film(webpage, anime_id)
|
for e in extract_film(webpage, anime_id):
|
||||||
|
yield e
|
||||||
|
|
||||||
return self.playlist_result(entries, anime_id, anime_title, anime_description)
|
return self.playlist_result(
|
||||||
|
entries(), anime_id, anime_title, anime_description)
|
||||||
|
|||||||
@@ -116,7 +116,76 @@ class AnvatoIE(InfoExtractor):
|
|||||||
'anvato_scripps_app_ios_prod_409c41960c60b308db43c3cc1da79cab9f1c3d93': 'WPxj5GraLTkYCyj3M7RozLqIycjrXOEcDGFMIJPn',
|
'anvato_scripps_app_ios_prod_409c41960c60b308db43c3cc1da79cab9f1c3d93': 'WPxj5GraLTkYCyj3M7RozLqIycjrXOEcDGFMIJPn',
|
||||||
'EZqvRyKBJLrgpClDPDF8I7Xpdp40Vx73': '4OxGd2dEakylntVKjKF0UK9PDPYB6A9W',
|
'EZqvRyKBJLrgpClDPDF8I7Xpdp40Vx73': '4OxGd2dEakylntVKjKF0UK9PDPYB6A9W',
|
||||||
'M2v78QkpleXm9hPp9jUXI63x5vA6BogR': 'ka6K32k7ZALmpINkjJUGUo0OE42Md1BQ',
|
'M2v78QkpleXm9hPp9jUXI63x5vA6BogR': 'ka6K32k7ZALmpINkjJUGUo0OE42Md1BQ',
|
||||||
'nbcu_nbcd_desktop_web_prod_93d8ead38ce2024f8f544b78306fbd15895ae5e6_secure': 'NNemUkySjxLyPTKvZRiGntBIjEyK8uqicjMakIaQ'
|
'nbcu_nbcd_desktop_web_prod_93d8ead38ce2024f8f544b78306fbd15895ae5e6_secure': 'NNemUkySjxLyPTKvZRiGntBIjEyK8uqicjMakIaQ',
|
||||||
|
'X8POa4zPPaKVZHqmWjuEzfP31b1QM9VN': 'Dn5vOY9ooDw7VSl9qztjZI5o0g08mA0z',
|
||||||
|
'M2v78QkBMpNJlSPp9diX5F2PBmBy6Bog': 'ka6K32kyo7nDZfNkjQCGWf1lpApXMd1B',
|
||||||
|
'bvJ0dQpav07l0hG5JgfVLF2dv1vARwpP': 'BzoQW24GrJZoJfmNodiJKSPeB9B8NOxj',
|
||||||
|
'lxQMLg2XZKuEZaWgsqubBxV9INZ6bryY': 'Vm2Mx6noKds9jB71h6urazwlTG3m9x8l',
|
||||||
|
'04EnjvXeoSmkbJ9ckPs7oY0mcxv7PlyN': 'aXERQP9LMfQVlEDsgGs6eEA1SWznAQ8P',
|
||||||
|
'mQbO2ge6BFRWVPYCYpU06YvNt80XLvAX': 'E2BV1NGmasN5v7eujECVPJgwflnLPm2A',
|
||||||
|
'g43oeBzJrCml7o6fa5fRL1ErCdeD8z4K': 'RX34mZ6zVH4Nr6whbxIGLv9WSbxEKo8V',
|
||||||
|
'VQrDJoP7mtdBzkxhXbSPwGB1coeElk4x': 'j2VejQx0VFKQepAF7dI0mJLKtOVJE18z',
|
||||||
|
'WxA5NzLRjCrmq0NUgaU5pdMDuZO7RJ4w': 'lyY5ADLKaIOLEgAsGQCveEMAcqnx3rY9',
|
||||||
|
'M4lpMXB71ie0PjMCjdFzVXq0SeRVqz49': 'n2zVkOqaLIv3GbLfBjcwW51LcveWOZ2e',
|
||||||
|
'dyDZGEqN8u8nkJZcJns0oxYmtP7KbGAn': 'VXOEqQW9BtEVLajfZQSLEqxgS5B7qn2D',
|
||||||
|
'E7QNjrVY5u5mGvgu67IoDgV1CjEND8QR': 'rz8AaDmdKIkLmPNhB5ILPJnjS5PnlL8d',
|
||||||
|
'a4zrqjoKlfzg0dwHEWtP31VqcLBpjm4g': 'LY9J16gwETdGWa3hjBu5o0RzuoQDjqXQ',
|
||||||
|
'dQP5BZroMsMVLO1hbmT5r2Enu86GjxA6': '7XR3oOdbPF6x3PRFLDCq9RkgsRjAo48V',
|
||||||
|
'M4lKNBO1NFe0PjMCj1tzVXq0SeRVqzA9': 'n2zoRqGLRUv3GbLfBmTwW51LcveWOZYe',
|
||||||
|
'nAZ7MZdpGCGg1pqFEbsoJOz2C60mv143': 'dYJgdqA9aT4yojETqGi7yNgoFADxqmXP',
|
||||||
|
'3y1MERYgOuE9NzbFgwhV6Wv2F0YKvbyz': '081xpZDQgC4VadLTavhWQxrku56DAgXV',
|
||||||
|
'bmQvmEXr5HWklBMCZOcpE2Z3HBYwqGyl': 'zxXPbVNyMiMAZldhr9FkOmA0fl4aKr2v',
|
||||||
|
'wA7oDNYldfr6050Hwxi52lPZiVlB86Ap': 'ZYK16aA7ni0d3l3c34uwpxD7CbReMm8Q',
|
||||||
|
'g43MbKMWmFml7o7sJoSRkXxZiXRvJ3QK': 'RX3oBJonvs4Nr6rUWBCGn3matRGqJPXV',
|
||||||
|
'mA9VdlqpLS0raGaSDvtoqNrBTzb8XY4q': '0XN4OjBD3fnW7r7IbmtJB4AyfOmlrE2r',
|
||||||
|
'mAajOwgkGt17oGoFmEuklMP9H0GnW54d': 'lXbBLPGyzikNGeGujAuAJGjZiwLRxyXR',
|
||||||
|
'vy8vjJ9kbUwrRqRu59Cj5dWZfzYErlAb': 'K8l7gpwaGcBpnAnCLNCmPZRdin3eaQX0',
|
||||||
|
'xQMWBpR8oHEZaWaSMGUb0avOHjLVYn4Y': 'm2MrN4vEaf9jB7BFy5Srb40jTrN67AYl',
|
||||||
|
'xyKEmVO3miRr6D6UVkt7oB8jtD6aJEAv': 'g2ddDebqDfqdgKgswyUKwGjbTWwzq923',
|
||||||
|
'7Qk0wa2D9FjKapacoJF27aLvUDKkLGA0': 'b2kgBEkephJaMkMTL7s1PLe4Ua6WyP2P',
|
||||||
|
'3QLg6nqmNTJ5VvVTo7f508LPidz1xwyY': 'g2L1GgpraipmAOAUqmIbBnPxHOmw4MYa',
|
||||||
|
'3y1B7zZjXTE9NZNSzZSVNPZaTNLjo6Qz': '081b5G6wzH4VagaURmcWbN5mT4JGEe2V',
|
||||||
|
'lAqnwvkw6SG6D8DSqmUg6DRLUp0w3G4x': 'O2pbP0xPDFNJjpjIEvcdryOJtpkVM4X5',
|
||||||
|
'awA7xd1N0Hr6050Hw2c52lPZiVlB864p': 'GZYKpn4aoT0d3l3c3PiwpxD7CbReMmXQ',
|
||||||
|
'jQVqPLl9YHL1WGWtR1HDgWBGT63qRNyV': '6X03ne6vrU4oWyWUN7tQVoajikxJR3Ye',
|
||||||
|
'GQRMR8mL7uZK797t7xH3eNzPIP5dOny1': 'm2vqPWGd4U31zWzSyasDRAoMT1PKRp8o',
|
||||||
|
'zydq9RdmRhXLkNkfNoTJlMzaF0lWekQB': '3X7LnvE7vH5nkEkSqLiey793Un7dLB8e',
|
||||||
|
'VQrDzwkB2IdBzjzu9MHPbEYkSB50gR4x': 'j2VebLzoKUKQeEesmVh0gM1eIp9jKz8z',
|
||||||
|
'mAa2wMamBs17oGoFmktklMP9H0GnW54d': 'lXbgP74xZTkNGeGujVUAJGjZiwLRxy8R',
|
||||||
|
'7yjB6ZLG6sW8R6RF2xcan1KGfJ5dNoyd': 'wXQkPorvPHZ45N5t4Jf6qwg5Tp4xvw29',
|
||||||
|
'a4zPpNeWGuzg0m0iX3tPeanGSkRKWXQg': 'LY9oa3QAyHdGW9Wu3Ri5JGeEik7l1N8Q',
|
||||||
|
'k2rneA2M38k25cXDwwSknTJlxPxQLZ6M': '61lyA2aEVDzklfdwmmh31saPxQx2VRjp',
|
||||||
|
'bK9Zk4OvPnvxduLgxvi8VUeojnjA02eV': 'o5jANYjbeMb4nfBaQvcLAt1jzLzYx6ze',
|
||||||
|
'5VD6EydM3R9orHmNMGInGCJwbxbQvGRw': 'w3zjmX7g4vnxzCxElvUEOiewkokXprkZ',
|
||||||
|
'70X35QbVYVYNPUmP9YfbzI06YqYQk2R1': 'vG4Aj2BMjMjoztB7zeFOnCVPJpJ8lMOa',
|
||||||
|
'26qYwQVG9p1Bks2GgBckjfDJOXOAMgG1': 'r4ev9X0mv5zqJc0yk5IBDcQOwZw8mnwQ',
|
||||||
|
'rvVKpA56MBXWlSxMw3cobT5pdkd4Dm7q': '1J7ZkY53pZ645c93owcLZuveE7E8B3rL',
|
||||||
|
'qN1zdy1zlYL23IWZGWtDvfV6WeWQWkJo': 'qN1zdy1zlYL23IWZGWtDvfV6WeWQWkJo',
|
||||||
|
'jdKqRGF16dKsBviMDae7IGDl7oTjEbVV': 'Q09l7vhlNxPFErIOK6BVCe7KnwUW5DVV',
|
||||||
|
'3QLkogW1OUJ5VvPsrDH56DY2u7lgZWyY': 'g2LRE1V9espmAOPhE4ubj4ZdUA57yDXa',
|
||||||
|
'wyJvWbXGBSdbkEzhv0CW8meou82aqRy8': 'M2wolPvyBIpQGkbT4juedD4ruzQGdK2y',
|
||||||
|
'7QkdZrzEkFjKap6IYDU2PB0oCNZORmA0': 'b2kN1l96qhJaMkPs9dt1lpjBfwqZoA8P',
|
||||||
|
'pvA05113MHG1w3JTYxc6DVlRCjErVz4O': 'gQXeAbblBUnDJ7vujbHvbRd1cxlz3AXO',
|
||||||
|
'mA9blJDZwT0raG1cvkuoeVjLC7ZWd54q': '0XN9jRPwMHnW7rvumgfJZOD9CJgVkWYr',
|
||||||
|
'5QwRN5qKJTvGKlDTmnf7xwNZcjRmvEy9': 'R2GP6LWBJU1QlnytwGt0B9pytWwAdDYy',
|
||||||
|
'eyn5rPPbkfw2KYxH32fG1q58CbLJzM40': 'p2gyqooZnS56JWeiDgfmOy1VugOQEBXn',
|
||||||
|
'3BABn3b5RfPJGDwilbHe7l82uBoR05Am': '7OYZG7KMVhbPdKJS3xcWEN3AuDlLNmXj',
|
||||||
|
'xA5zNGXD3HrmqMlF6OS5pdMDuZO7RJ4w': 'yY5DAm6r1IOLE3BCVMFveEMAcqnx3r29',
|
||||||
|
'g43PgW3JZfml7o6fDEURL1ErCdeD8zyK': 'RX3aQn1zrS4Nr6whDgCGLv9WSbxEKo2V',
|
||||||
|
'lAqp8WbGgiG6D8LTKJcg3O72CDdre1Qx': 'O2pnm6473HNJjpKuVosd3vVeh975yrX5',
|
||||||
|
'wyJbYEDxKSdbkJ6S6RhW8meou82aqRy8': 'M2wPm7EgRSpQGlAh70CedD4ruzQGdKYy',
|
||||||
|
'M4lgW28nLCe0PVdtaXszVXq0SeRVqzA9': 'n2zmJvg4jHv3G0ETNgiwW51LcveWOZ8e',
|
||||||
|
'5Qw3OVvp9FvGKlDTmOC7xwNZcjRmvEQ9': 'R2GzDdml9F1Qlnytw9s0B9pytWwAdD8y',
|
||||||
|
'vy8a98X7zCwrRqbHrLUjYzwDiK2b70Qb': 'K8lVwzyjZiBpnAaSGeUmnAgxuGOBxmY0',
|
||||||
|
'g4eGjJLLoiqRD3Pf9oT5O03LuNbLRDQp': '6XqD59zzpfN4EwQuaGt67qNpSyRBlnYy',
|
||||||
|
'g43OPp9boIml7o6fDOIRL1ErCdeD8z4K': 'RX33alNB4s4Nr6whDPUGLv9WSbxEKoXV',
|
||||||
|
'xA2ng9OkBcGKzDbTkKsJlx7dUK8R3dA5': 'z2aPnJvzBfObkwGC3vFaPxeBhxoMqZ8K',
|
||||||
|
'xyKEgBajZuRr6DEC0Kt7XpD1cnNW9gAv': 'g2ddlEBvRsqdgKaI4jUK9PrgfMexGZ23',
|
||||||
|
'BAogww51jIMa2JnH1BcYpXM5F658RNAL': 'rYWDmm0KptlkGv4FGJFMdZmjs9RDE6XR',
|
||||||
|
'BAokpg62VtMa2JnH1mHYpXM5F658RNAL': 'rYWryDnlNslkGv4FG4HMdZmjs9RDE62R',
|
||||||
|
'a4z1Px5e2hzg0m0iMMCPeanGSkRKWXAg': 'LY9eorNQGUdGW9WuKKf5JGeEik7l1NYQ',
|
||||||
|
'kAx69R58kF9nY5YcdecJdl2pFXP53WyX': 'gXyRxELpbfPvLeLSaRil0mp6UEzbZJ8L',
|
||||||
|
'BAoY13nwViMa2J2uo2cY6BlETgmdwryL': 'rYWwKzJmNFlkGvGtNoUM9bzwIJVzB1YR',
|
||||||
}
|
}
|
||||||
|
|
||||||
_MCP_TO_ACCESS_KEY_TABLE = {
|
_MCP_TO_ACCESS_KEY_TABLE = {
|
||||||
@@ -189,19 +258,17 @@ class AnvatoIE(InfoExtractor):
|
|||||||
|
|
||||||
video_data_url += '&X-Anvato-Adst-Auth=' + base64.b64encode(auth_secret).decode('ascii')
|
video_data_url += '&X-Anvato-Adst-Auth=' + base64.b64encode(auth_secret).decode('ascii')
|
||||||
anvrid = md5_text(time.time() * 1000 * random.random())[:30]
|
anvrid = md5_text(time.time() * 1000 * random.random())[:30]
|
||||||
payload = {
|
api = {
|
||||||
'api': {
|
|
||||||
'anvrid': anvrid,
|
'anvrid': anvrid,
|
||||||
'anvstk': md5_text('%s|%s|%d|%s' % (
|
|
||||||
access_key, anvrid, server_time,
|
|
||||||
self._ANVACK_TABLE.get(access_key, self._API_KEY))),
|
|
||||||
'anvts': server_time,
|
'anvts': server_time,
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
api['anvstk'] = md5_text('%s|%s|%d|%s' % (
|
||||||
|
access_key, anvrid, server_time,
|
||||||
|
self._ANVACK_TABLE.get(access_key, self._API_KEY)))
|
||||||
|
|
||||||
return self._download_json(
|
return self._download_json(
|
||||||
video_data_url, video_id, transform_source=strip_jsonp,
|
video_data_url, video_id, transform_source=strip_jsonp,
|
||||||
data=json.dumps(payload).encode('utf-8'))
|
data=json.dumps({'api': api}).encode('utf-8'))
|
||||||
|
|
||||||
def _get_anvato_videos(self, access_key, video_id):
|
def _get_anvato_videos(self, access_key, video_id):
|
||||||
video_data = self._get_video_json(access_key, video_id)
|
video_data = self._get_video_json(access_key, video_id)
|
||||||
@@ -259,7 +326,7 @@ class AnvatoIE(InfoExtractor):
|
|||||||
'description': video_data.get('def_description'),
|
'description': video_data.get('def_description'),
|
||||||
'tags': video_data.get('def_tags', '').split(','),
|
'tags': video_data.get('def_tags', '').split(','),
|
||||||
'categories': video_data.get('categories'),
|
'categories': video_data.get('categories'),
|
||||||
'thumbnail': video_data.get('thumbnail'),
|
'thumbnail': video_data.get('src_image_url') or video_data.get('thumbnail'),
|
||||||
'timestamp': int_or_none(video_data.get(
|
'timestamp': int_or_none(video_data.get(
|
||||||
'ts_published') or video_data.get('ts_added')),
|
'ts_published') or video_data.get('ts_added')),
|
||||||
'uploader': video_data.get('mcp_id'),
|
'uploader': video_data.get('mcp_id'),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .yahoo import YahooIE
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
compat_parse_qs,
|
compat_parse_qs,
|
||||||
compat_urllib_parse_urlparse,
|
compat_urllib_parse_urlparse,
|
||||||
@@ -15,9 +15,9 @@ from ..utils import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AolIE(InfoExtractor):
|
class AolIE(YahooIE):
|
||||||
IE_NAME = 'aol.com'
|
IE_NAME = 'aol.com'
|
||||||
_VALID_URL = r'(?:aol-video:|https?://(?:www\.)?aol\.(?:com|ca|co\.uk|de|jp)/video/(?:[^/]+/)*)(?P<id>[0-9a-f]+)'
|
_VALID_URL = r'(?:aol-video:|https?://(?:www\.)?aol\.(?:com|ca|co\.uk|de|jp)/video/(?:[^/]+/)*)(?P<id>\d{9}|[0-9a-f]{24}|[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
# video with 5min ID
|
# video with 5min ID
|
||||||
@@ -76,10 +76,16 @@ class AolIE(InfoExtractor):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'https://www.aol.jp/video/playlist/5a28e936a1334d000137da0c/5a28f3151e642219fde19831/',
|
'url': 'https://www.aol.jp/video/playlist/5a28e936a1334d000137da0c/5a28f3151e642219fde19831/',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# Yahoo video
|
||||||
|
'url': 'https://www.aol.com/video/play/991e6700-ac02-11ea-99ff-357400036f61/24bbc846-3e30-3c46-915e-fe8ccd7fcc46/',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
|
if '-' in video_id:
|
||||||
|
return self._extract_yahoo_video(video_id, 'us')
|
||||||
|
|
||||||
response = self._download_json(
|
response = self._download_json(
|
||||||
'https://feedapi.b2c.on.aol.com/v1.0/app/videos/aolon/%s/details' % video_id,
|
'https://feedapi.b2c.on.aol.com/v1.0/app/videos/aolon/%s/details' % video_id,
|
||||||
|
|||||||
@@ -6,25 +6,21 @@ import re
|
|||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
determine_ext,
|
determine_ext,
|
||||||
js_to_json,
|
int_or_none,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class APAIE(InfoExtractor):
|
class APAIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://[^/]+\.apa\.at/embed/(?P<id>[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12})'
|
_VALID_URL = r'(?P<base_url>https?://[^/]+\.apa\.at)/embed/(?P<id>[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12})'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://uvp.apa.at/embed/293f6d17-692a-44e3-9fd5-7b178f3a1029',
|
'url': 'http://uvp.apa.at/embed/293f6d17-692a-44e3-9fd5-7b178f3a1029',
|
||||||
'md5': '2b12292faeb0a7d930c778c7a5b4759b',
|
'md5': '2b12292faeb0a7d930c778c7a5b4759b',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'jjv85FdZ',
|
'id': '293f6d17-692a-44e3-9fd5-7b178f3a1029',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': '"Blau ist mysteriös": Die Blue Man Group im Interview',
|
'title': '293f6d17-692a-44e3-9fd5-7b178f3a1029',
|
||||||
'description': 'md5:d41d8cd98f00b204e9800998ecf8427e',
|
|
||||||
'thumbnail': r're:^https?://.*\.jpg$',
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
'duration': 254,
|
|
||||||
'timestamp': 1519211149,
|
|
||||||
'upload_date': '20180221',
|
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://uvp-apapublisher.sf.apa.at/embed/2f94e9e6-d945-4db2-9548-f9a41ebf7b78',
|
'url': 'https://uvp-apapublisher.sf.apa.at/embed/2f94e9e6-d945-4db2-9548-f9a41ebf7b78',
|
||||||
@@ -46,9 +42,11 @@ class APAIE(InfoExtractor):
|
|||||||
webpage)]
|
webpage)]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
|
video_id, base_url = mobj.group('id', 'base_url')
|
||||||
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(
|
||||||
|
'%s/player/%s' % (base_url, video_id), video_id)
|
||||||
|
|
||||||
jwplatform_id = self._search_regex(
|
jwplatform_id = self._search_regex(
|
||||||
r'media[iI]d\s*:\s*["\'](?P<id>[a-zA-Z0-9]{8})', webpage,
|
r'media[iI]d\s*:\s*["\'](?P<id>[a-zA-Z0-9]{8})', webpage,
|
||||||
@@ -59,16 +57,18 @@ class APAIE(InfoExtractor):
|
|||||||
'jwplatform:' + jwplatform_id, ie='JWPlatform',
|
'jwplatform:' + jwplatform_id, ie='JWPlatform',
|
||||||
video_id=video_id)
|
video_id=video_id)
|
||||||
|
|
||||||
sources = self._parse_json(
|
def extract(field, name=None):
|
||||||
self._search_regex(
|
return self._search_regex(
|
||||||
r'sources\s*=\s*(\[.+?\])\s*;', webpage, 'sources'),
|
r'\b%s["\']\s*:\s*(["\'])(?P<value>(?:(?!\1).)+)\1' % field,
|
||||||
video_id, transform_source=js_to_json)
|
webpage, name or field, default=None, group='value')
|
||||||
|
|
||||||
|
title = extract('title') or video_id
|
||||||
|
description = extract('description')
|
||||||
|
thumbnail = extract('poster', 'thumbnail')
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
for source in sources:
|
for format_id in ('hls', 'progressive'):
|
||||||
if not isinstance(source, dict):
|
source_url = url_or_none(extract(format_id))
|
||||||
continue
|
|
||||||
source_url = url_or_none(source.get('file'))
|
|
||||||
if not source_url:
|
if not source_url:
|
||||||
continue
|
continue
|
||||||
ext = determine_ext(source_url)
|
ext = determine_ext(source_url)
|
||||||
@@ -77,18 +77,19 @@ class APAIE(InfoExtractor):
|
|||||||
source_url, video_id, 'mp4', entry_protocol='m3u8_native',
|
source_url, video_id, 'mp4', entry_protocol='m3u8_native',
|
||||||
m3u8_id='hls', fatal=False))
|
m3u8_id='hls', fatal=False))
|
||||||
else:
|
else:
|
||||||
|
height = int_or_none(self._search_regex(
|
||||||
|
r'(\d+)\.mp4', source_url, 'height', default=None))
|
||||||
formats.append({
|
formats.append({
|
||||||
'url': source_url,
|
'url': source_url,
|
||||||
|
'format_id': format_id,
|
||||||
|
'height': height,
|
||||||
})
|
})
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
thumbnail = self._search_regex(
|
|
||||||
r'image\s*:\s*(["\'])(?P<url>(?:(?!\1).)+)\1', webpage,
|
|
||||||
'thumbnail', fatal=False, group='url')
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': video_id,
|
'title': title,
|
||||||
|
'description': description,
|
||||||
'thumbnail': thumbnail,
|
'thumbnail': thumbnail,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
get_element_by_id,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
merge_dicts,
|
merge_dicts,
|
||||||
mimetype2ext,
|
mimetype2ext,
|
||||||
@@ -39,23 +40,15 @@ class AparatIE(InfoExtractor):
|
|||||||
webpage = self._download_webpage(url, video_id, fatal=False)
|
webpage = self._download_webpage(url, video_id, fatal=False)
|
||||||
|
|
||||||
if not webpage:
|
if not webpage:
|
||||||
# Note: There is an easier-to-parse configuration at
|
|
||||||
# http://www.aparat.com/video/video/config/videohash/%video_id
|
|
||||||
# but the URL in there does not work
|
|
||||||
webpage = self._download_webpage(
|
webpage = self._download_webpage(
|
||||||
'http://www.aparat.com/video/video/embed/vt/frame/showvideo/yes/videohash/' + video_id,
|
'http://www.aparat.com/video/video/embed/vt/frame/showvideo/yes/videohash/' + video_id,
|
||||||
video_id)
|
video_id)
|
||||||
|
|
||||||
options = self._parse_json(
|
options = self._parse_json(self._search_regex(
|
||||||
self._search_regex(
|
r'options\s*=\s*({.+?})\s*;', webpage, 'options'), video_id)
|
||||||
r'options\s*=\s*JSON\.parse\(\s*(["\'])(?P<value>(?:(?!\1).)+)\1\s*\)',
|
|
||||||
webpage, 'options', group='value'),
|
|
||||||
video_id)
|
|
||||||
|
|
||||||
player = options['plugins']['sabaPlayerPlugin']
|
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
for sources in player['multiSRC']:
|
for sources in (options.get('multiSRC') or []):
|
||||||
for item in sources:
|
for item in sources:
|
||||||
if not isinstance(item, dict):
|
if not isinstance(item, dict):
|
||||||
continue
|
continue
|
||||||
@@ -85,11 +78,12 @@ class AparatIE(InfoExtractor):
|
|||||||
info = self._search_json_ld(webpage, video_id, default={})
|
info = self._search_json_ld(webpage, video_id, default={})
|
||||||
|
|
||||||
if not info.get('title'):
|
if not info.get('title'):
|
||||||
info['title'] = player['title']
|
info['title'] = get_element_by_id('videoTitle', webpage) or \
|
||||||
|
self._html_search_meta(['og:title', 'twitter:title', 'DC.Title', 'title'], webpage, fatal=True)
|
||||||
|
|
||||||
return merge_dicts(info, {
|
return merge_dicts(info, {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'thumbnail': url_or_none(options.get('poster')),
|
'thumbnail': url_or_none(options.get('poster')),
|
||||||
'duration': int_or_none(player.get('duration')),
|
'duration': int_or_none(options.get('duration')),
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class AppleConnectIE(InfoExtractor):
|
class AppleConnectIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://itunes\.apple\.com/\w{0,2}/?post/idsa\.(?P<id>[\w-]+)'
|
_VALID_URL = r'https?://itunes\.apple\.com/\w{0,2}/?post/(?:id)?sa\.(?P<id>[\w-]+)'
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'https://itunes.apple.com/us/post/idsa.4ab17a39-2720-11e5-96c5-a5b38f6c42d3',
|
'url': 'https://itunes.apple.com/us/post/idsa.4ab17a39-2720-11e5-96c5-a5b38f6c42d3',
|
||||||
'md5': 'e7c38568a01ea45402570e6029206723',
|
'md5': 'c1d41f72c8bcaf222e089434619316e4',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '4ab17a39-2720-11e5-96c5-a5b38f6c42d3',
|
'id': '4ab17a39-2720-11e5-96c5-a5b38f6c42d3',
|
||||||
'ext': 'm4v',
|
'ext': 'm4v',
|
||||||
@@ -22,7 +22,10 @@ class AppleConnectIE(InfoExtractor):
|
|||||||
'upload_date': '20150710',
|
'upload_date': '20150710',
|
||||||
'timestamp': 1436545535,
|
'timestamp': 1436545535,
|
||||||
},
|
},
|
||||||
}
|
}, {
|
||||||
|
'url': 'https://itunes.apple.com/us/post/sa.0fe0229f-2457-11e5-9f40-1bb645f2d5d9',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
@@ -36,7 +39,7 @@ class AppleConnectIE(InfoExtractor):
|
|||||||
|
|
||||||
video_data = self._parse_json(video_json, video_id)
|
video_data = self._parse_json(video_json, video_id)
|
||||||
timestamp = str_to_int(self._html_search_regex(r'data-timestamp="(\d+)"', webpage, 'timestamp'))
|
timestamp = str_to_int(self._html_search_regex(r'data-timestamp="(\d+)"', webpage, 'timestamp'))
|
||||||
like_count = str_to_int(self._html_search_regex(r'(\d+) Loves', webpage, 'like count'))
|
like_count = str_to_int(self._html_search_regex(r'(\d+) Loves', webpage, 'like count', default=None))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
|
|||||||
93
youtube_dl/extractor/applepodcasts.py
Normal file
93
youtube_dl/extractor/applepodcasts.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
clean_html,
|
||||||
|
clean_podcast_url,
|
||||||
|
get_element_by_class,
|
||||||
|
int_or_none,
|
||||||
|
parse_codecs,
|
||||||
|
parse_iso8601,
|
||||||
|
try_get,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ApplePodcastsIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://podcasts\.apple\.com/(?:[^/]+/)?podcast(?:/[^/]+){1,2}.*?\bi=(?P<id>\d+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://podcasts.apple.com/us/podcast/207-whitney-webb-returns/id1135137367?i=1000482637777',
|
||||||
|
'md5': '41dc31cd650143e530d9423b6b5a344f',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1000482637777',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'title': '207 - Whitney Webb Returns',
|
||||||
|
'description': 'md5:75ef4316031df7b41ced4e7b987f79c6',
|
||||||
|
'upload_date': '20200705',
|
||||||
|
'timestamp': 1593932400,
|
||||||
|
'duration': 6454,
|
||||||
|
'series': 'The Tim Dillon Show',
|
||||||
|
'thumbnail': 're:.+[.](png|jpe?g|webp)',
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'https://podcasts.apple.com/podcast/207-whitney-webb-returns/id1135137367?i=1000482637777',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://podcasts.apple.com/podcast/207-whitney-webb-returns?i=1000482637777',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://podcasts.apple.com/podcast/id1135137367?i=1000482637777',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
episode_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, episode_id)
|
||||||
|
episode_data = {}
|
||||||
|
ember_data = {}
|
||||||
|
# new page type 2021-11
|
||||||
|
amp_data = self._parse_json(self._search_regex(
|
||||||
|
r'(?s)id="shoebox-media-api-cache-amp-podcasts"[^>]*>\s*({.+?})\s*<',
|
||||||
|
webpage, 'AMP data', default='{}'), episode_id, fatal=False) or {}
|
||||||
|
amp_data = try_get(amp_data,
|
||||||
|
lambda a: self._parse_json(
|
||||||
|
next(a[x] for x in iter(a) if episode_id in x),
|
||||||
|
episode_id),
|
||||||
|
dict) or {}
|
||||||
|
amp_data = amp_data.get('d') or []
|
||||||
|
episode_data = try_get(
|
||||||
|
amp_data,
|
||||||
|
lambda a: next(x for x in a
|
||||||
|
if x['type'] == 'podcast-episodes' and x['id'] == episode_id),
|
||||||
|
dict)
|
||||||
|
if not episode_data:
|
||||||
|
# try pre 2021-11 page type: TODO: consider deleting if no longer used
|
||||||
|
ember_data = self._parse_json(self._search_regex(
|
||||||
|
r'(?s)id="shoebox-ember-data-store"[^>]*>\s*({.+?})\s*<',
|
||||||
|
webpage, 'ember data'), episode_id) or {}
|
||||||
|
ember_data = ember_data.get(episode_id) or ember_data
|
||||||
|
episode_data = try_get(ember_data, lambda x: x['data'], dict)
|
||||||
|
episode = episode_data['attributes']
|
||||||
|
description = episode.get('description') or {}
|
||||||
|
|
||||||
|
series = None
|
||||||
|
for inc in (amp_data or ember_data.get('included') or []):
|
||||||
|
if inc.get('type') == 'media/podcast':
|
||||||
|
series = try_get(inc, lambda x: x['attributes']['name'])
|
||||||
|
series = series or clean_html(get_element_by_class('podcast-header__identity', webpage))
|
||||||
|
|
||||||
|
info = [{
|
||||||
|
'id': episode_id,
|
||||||
|
'title': episode['name'],
|
||||||
|
'url': clean_podcast_url(episode['assetUrl']),
|
||||||
|
'description': description.get('standard') or description.get('short'),
|
||||||
|
'timestamp': parse_iso8601(episode.get('releaseDateTime')),
|
||||||
|
'duration': int_or_none(episode.get('durationInMilliseconds'), 1000),
|
||||||
|
'series': series,
|
||||||
|
'thumbnail': self._og_search_thumbnail(webpage),
|
||||||
|
}]
|
||||||
|
self._sort_formats(info)
|
||||||
|
info = info[0]
|
||||||
|
codecs = parse_codecs(info.get('ext', 'mp3'))
|
||||||
|
info.update(codecs)
|
||||||
|
return info
|
||||||
@@ -2,15 +2,17 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
unified_strdate,
|
|
||||||
clean_html,
|
clean_html,
|
||||||
|
extract_attributes,
|
||||||
|
unified_strdate,
|
||||||
|
unified_timestamp,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ArchiveOrgIE(InfoExtractor):
|
class ArchiveOrgIE(InfoExtractor):
|
||||||
IE_NAME = 'archive.org'
|
IE_NAME = 'archive.org'
|
||||||
IE_DESC = 'archive.org videos'
|
IE_DESC = 'archive.org videos'
|
||||||
_VALID_URL = r'https?://(?:www\.)?archive\.org/(?:details|embed)/(?P<id>[^/?#]+)(?:[?].*)?$'
|
_VALID_URL = r'https?://(?:www\.)?archive\.org/(?:details|embed)/(?P<id>[^/?#&]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://archive.org/details/XD300-23_68HighlightsAResearchCntAugHumanIntellect',
|
'url': 'http://archive.org/details/XD300-23_68HighlightsAResearchCntAugHumanIntellect',
|
||||||
'md5': '8af1d4cf447933ed3c7f4871162602db',
|
'md5': '8af1d4cf447933ed3c7f4871162602db',
|
||||||
@@ -19,8 +21,11 @@ class ArchiveOrgIE(InfoExtractor):
|
|||||||
'ext': 'ogg',
|
'ext': 'ogg',
|
||||||
'title': '1968 Demo - FJCC Conference Presentation Reel #1',
|
'title': '1968 Demo - FJCC Conference Presentation Reel #1',
|
||||||
'description': 'md5:da45c349df039f1cc8075268eb1b5c25',
|
'description': 'md5:da45c349df039f1cc8075268eb1b5c25',
|
||||||
'upload_date': '19681210',
|
'creator': 'SRI International',
|
||||||
'uploader': 'SRI International'
|
'release_date': '19681210',
|
||||||
|
'uploader': 'SRI International',
|
||||||
|
'timestamp': 1268695290,
|
||||||
|
'upload_date': '20100315',
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://archive.org/details/Cops1922',
|
'url': 'https://archive.org/details/Cops1922',
|
||||||
@@ -29,22 +34,43 @@ class ArchiveOrgIE(InfoExtractor):
|
|||||||
'id': 'Cops1922',
|
'id': 'Cops1922',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Buster Keaton\'s "Cops" (1922)',
|
'title': 'Buster Keaton\'s "Cops" (1922)',
|
||||||
'description': 'md5:89e7c77bf5d965dd5c0372cfb49470f6',
|
'description': 'md5:43a603fd6c5b4b90d12a96b921212b9c',
|
||||||
|
'timestamp': 1387699629,
|
||||||
|
'upload_date': '20131222',
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://archive.org/embed/XD300-23_68HighlightsAResearchCntAugHumanIntellect',
|
'url': 'http://archive.org/embed/XD300-23_68HighlightsAResearchCntAugHumanIntellect',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://archive.org/details/MSNBCW_20131125_040000_To_Catch_a_Predator/',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(
|
webpage = self._download_webpage(
|
||||||
'http://archive.org/embed/' + video_id, video_id)
|
'http://archive.org/embed/' + video_id, video_id)
|
||||||
jwplayer_playlist = self._parse_json(self._search_regex(
|
|
||||||
|
playlist = None
|
||||||
|
play8 = self._search_regex(
|
||||||
|
r'(<[^>]+\bclass=["\']js-play8-playlist[^>]+>)', webpage,
|
||||||
|
'playlist', default=None)
|
||||||
|
if play8:
|
||||||
|
attrs = extract_attributes(play8)
|
||||||
|
playlist = attrs.get('value')
|
||||||
|
if not playlist:
|
||||||
|
# Old jwplayer fallback
|
||||||
|
playlist = self._search_regex(
|
||||||
r"(?s)Play\('[^']+'\s*,\s*(\[.+\])\s*,\s*{.*?}\)",
|
r"(?s)Play\('[^']+'\s*,\s*(\[.+\])\s*,\s*{.*?}\)",
|
||||||
webpage, 'jwplayer playlist'), video_id)
|
webpage, 'jwplayer playlist', default='[]')
|
||||||
|
jwplayer_playlist = self._parse_json(playlist, video_id, fatal=False)
|
||||||
|
if jwplayer_playlist:
|
||||||
info = self._parse_jwplayer_data(
|
info = self._parse_jwplayer_data(
|
||||||
{'playlist': jwplayer_playlist}, video_id, base_url=url)
|
{'playlist': jwplayer_playlist}, video_id, base_url=url)
|
||||||
|
else:
|
||||||
|
# HTML5 media fallback
|
||||||
|
info = self._parse_html5_media_entries(url, webpage, video_id)[0]
|
||||||
|
info['id'] = video_id
|
||||||
|
|
||||||
def get_optional(metadata, field):
|
def get_optional(metadata, field):
|
||||||
return metadata.get(field, [None])[0]
|
return metadata.get(field, [None])[0]
|
||||||
@@ -58,8 +84,12 @@ class ArchiveOrgIE(InfoExtractor):
|
|||||||
'description': clean_html(get_optional(metadata, 'description')),
|
'description': clean_html(get_optional(metadata, 'description')),
|
||||||
})
|
})
|
||||||
if info.get('_type') != 'playlist':
|
if info.get('_type') != 'playlist':
|
||||||
|
creator = get_optional(metadata, 'creator')
|
||||||
info.update({
|
info.update({
|
||||||
'uploader': get_optional(metadata, 'creator'),
|
'creator': creator,
|
||||||
'upload_date': unified_strdate(get_optional(metadata, 'date')),
|
'release_date': unified_strdate(get_optional(metadata, 'date')),
|
||||||
|
'uploader': get_optional(metadata, 'publisher') or creator,
|
||||||
|
'timestamp': unified_timestamp(get_optional(metadata, 'publicdate')),
|
||||||
|
'language': get_optional(metadata, 'language'),
|
||||||
})
|
})
|
||||||
return info
|
return info
|
||||||
|
|||||||
174
youtube_dl/extractor/arcpublishing.py
Normal file
174
youtube_dl/extractor/arcpublishing.py
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
extract_attributes,
|
||||||
|
int_or_none,
|
||||||
|
parse_iso8601,
|
||||||
|
try_get,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ArcPublishingIE(InfoExtractor):
|
||||||
|
_UUID_REGEX = r'[\da-f]{8}-(?:[\da-f]{4}-){3}[\da-f]{12}'
|
||||||
|
_VALID_URL = r'arcpublishing:(?P<org>[a-z]+):(?P<id>%s)' % _UUID_REGEX
|
||||||
|
_TESTS = [{
|
||||||
|
# https://www.adn.com/politics/2020/11/02/video-senate-candidates-campaign-in-anchorage-on-eve-of-election-day/
|
||||||
|
'url': 'arcpublishing:adn:8c99cb6e-b29c-4bc9-9173-7bf9979225ab',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# https://www.bostonglobe.com/video/2020/12/30/metro/footage-released-showing-officer-talking-about-striking-protesters-with-car/
|
||||||
|
'url': 'arcpublishing:bostonglobe:232b7ae6-7d73-432d-bc0a-85dbf0119ab1',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# https://www.actionnewsjax.com/video/live-stream/
|
||||||
|
'url': 'arcpublishing:cmg:cfb1cf1b-3ab5-4d1b-86c5-a5515d311f2a',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# https://elcomercio.pe/videos/deportes/deporte-total-futbol-peruano-seleccion-peruana-la-valorizacion-de-los-peruanos-en-el-exterior-tras-un-2020-atipico-nnav-vr-video-noticia/
|
||||||
|
'url': 'arcpublishing:elcomercio:27a7e1f8-2ec7-4177-874f-a4feed2885b3',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# https://www.clickondetroit.com/video/community/2020/05/15/events-surrounding-woodward-dream-cruise-being-canceled/
|
||||||
|
'url': 'arcpublishing:gmg:c8793fb2-8d44-4242-881e-2db31da2d9fe',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# https://www.wabi.tv/video/2020/12/30/trenton-company-making-equipment-pfizer-covid-vaccine/
|
||||||
|
'url': 'arcpublishing:gray:0b0ba30e-032a-4598-8810-901d70e6033e',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# https://www.lateja.cr/el-mundo/video-china-aprueba-con-condiciones-su-primera/dfcbfa57-527f-45ff-a69b-35fe71054143/video/
|
||||||
|
'url': 'arcpublishing:gruponacion:dfcbfa57-527f-45ff-a69b-35fe71054143',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# https://www.fifthdomain.com/video/2018/03/09/is-america-vulnerable-to-a-cyber-attack/
|
||||||
|
'url': 'arcpublishing:mco:aa0ca6fe-1127-46d4-b32c-be0d6fdb8055',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# https://www.vl.no/kultur/2020/12/09/en-melding-fra-en-lytter-endret-julelista-til-lewi-bergrud/
|
||||||
|
'url': 'arcpublishing:mentormedier:47a12084-650b-4011-bfd0-3699b6947b2d',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# https://www.14news.com/2020/12/30/whiskey-theft-caught-camera-henderson-liquor-store/
|
||||||
|
'url': 'arcpublishing:raycom:b89f61f8-79fa-4c09-8255-e64237119bf7',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# https://www.theglobeandmail.com/world/video-ethiopian-woman-who-became-symbol-of-integration-in-italy-killed-on/
|
||||||
|
'url': 'arcpublishing:tgam:411b34c1-8701-4036-9831-26964711664b',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# https://www.pilotonline.com/460f2931-8130-4719-8ea1-ffcb2d7cb685-132.html
|
||||||
|
'url': 'arcpublishing:tronc:460f2931-8130-4719-8ea1-ffcb2d7cb685',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
_POWA_DEFAULTS = [
|
||||||
|
(['cmg', 'prisa'], '%s-config-prod.api.cdn.arcpublishing.com/video'),
|
||||||
|
([
|
||||||
|
'adn', 'advancelocal', 'answers', 'bonnier', 'bostonglobe', 'demo',
|
||||||
|
'gmg', 'gruponacion', 'infobae', 'mco', 'nzme', 'pmn', 'raycom',
|
||||||
|
'spectator', 'tbt', 'tgam', 'tronc', 'wapo', 'wweek',
|
||||||
|
], 'video-api-cdn.%s.arcpublishing.com/api'),
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_urls(webpage):
|
||||||
|
entries = []
|
||||||
|
# https://arcpublishing.atlassian.net/wiki/spaces/POWA/overview
|
||||||
|
for powa_el in re.findall(r'(<div[^>]+class="[^"]*\bpowa\b[^"]*"[^>]+data-uuid="%s"[^>]*>)' % ArcPublishingIE._UUID_REGEX, webpage):
|
||||||
|
powa = extract_attributes(powa_el) or {}
|
||||||
|
org = powa.get('data-org')
|
||||||
|
uuid = powa.get('data-uuid')
|
||||||
|
if org and uuid:
|
||||||
|
entries.append('arcpublishing:%s:%s' % (org, uuid))
|
||||||
|
return entries
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
org, uuid = re.match(self._VALID_URL, url).groups()
|
||||||
|
for orgs, tmpl in self._POWA_DEFAULTS:
|
||||||
|
if org in orgs:
|
||||||
|
base_api_tmpl = tmpl
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
base_api_tmpl = '%s-prod-cdn.video-api.arcpublishing.com/api'
|
||||||
|
if org == 'wapo':
|
||||||
|
org = 'washpost'
|
||||||
|
video = self._download_json(
|
||||||
|
'https://%s/v1/ansvideos/findByUuid' % (base_api_tmpl % org),
|
||||||
|
uuid, query={'uuid': uuid})[0]
|
||||||
|
title = video['headlines']['basic']
|
||||||
|
is_live = video.get('status') == 'live'
|
||||||
|
|
||||||
|
urls = []
|
||||||
|
formats = []
|
||||||
|
for s in video.get('streams', []):
|
||||||
|
s_url = s.get('url')
|
||||||
|
if not s_url or s_url in urls:
|
||||||
|
continue
|
||||||
|
urls.append(s_url)
|
||||||
|
stream_type = s.get('stream_type')
|
||||||
|
if stream_type == 'smil':
|
||||||
|
smil_formats = self._extract_smil_formats(
|
||||||
|
s_url, uuid, fatal=False)
|
||||||
|
for f in smil_formats:
|
||||||
|
if f['url'].endswith('/cfx/st'):
|
||||||
|
f['app'] = 'cfx/st'
|
||||||
|
if not f['play_path'].startswith('mp4:'):
|
||||||
|
f['play_path'] = 'mp4:' + f['play_path']
|
||||||
|
if isinstance(f['tbr'], float):
|
||||||
|
f['vbr'] = f['tbr'] * 1000
|
||||||
|
del f['tbr']
|
||||||
|
f['format_id'] = 'rtmp-%d' % f['vbr']
|
||||||
|
formats.extend(smil_formats)
|
||||||
|
elif stream_type in ('ts', 'hls'):
|
||||||
|
m3u8_formats = self._extract_m3u8_formats(
|
||||||
|
s_url, uuid, 'mp4', 'm3u8' if is_live else 'm3u8_native',
|
||||||
|
m3u8_id='hls', fatal=False)
|
||||||
|
if all([f.get('acodec') == 'none' for f in m3u8_formats]):
|
||||||
|
continue
|
||||||
|
for f in m3u8_formats:
|
||||||
|
if f.get('acodec') == 'none':
|
||||||
|
f['preference'] = -40
|
||||||
|
elif f.get('vcodec') == 'none':
|
||||||
|
f['preference'] = -50
|
||||||
|
height = f.get('height')
|
||||||
|
if not height:
|
||||||
|
continue
|
||||||
|
vbr = self._search_regex(
|
||||||
|
r'[_x]%d[_-](\d+)' % height, f['url'], 'vbr', default=None)
|
||||||
|
if vbr:
|
||||||
|
f['vbr'] = int(vbr)
|
||||||
|
formats.extend(m3u8_formats)
|
||||||
|
else:
|
||||||
|
vbr = int_or_none(s.get('bitrate'))
|
||||||
|
formats.append({
|
||||||
|
'format_id': '%s-%d' % (stream_type, vbr) if vbr else stream_type,
|
||||||
|
'vbr': vbr,
|
||||||
|
'width': int_or_none(s.get('width')),
|
||||||
|
'height': int_or_none(s.get('height')),
|
||||||
|
'filesize': int_or_none(s.get('filesize')),
|
||||||
|
'url': s_url,
|
||||||
|
'preference': -1,
|
||||||
|
})
|
||||||
|
self._sort_formats(
|
||||||
|
formats, ('preference', 'width', 'height', 'vbr', 'filesize', 'tbr', 'ext', 'format_id'))
|
||||||
|
|
||||||
|
subtitles = {}
|
||||||
|
for subtitle in (try_get(video, lambda x: x['subtitles']['urls'], list) or []):
|
||||||
|
subtitle_url = subtitle.get('url')
|
||||||
|
if subtitle_url:
|
||||||
|
subtitles.setdefault('en', []).append({'url': subtitle_url})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': uuid,
|
||||||
|
'title': self._live_title(title) if is_live else title,
|
||||||
|
'thumbnail': try_get(video, lambda x: x['promo_image']['url']),
|
||||||
|
'description': try_get(video, lambda x: x['subheadlines']['basic']),
|
||||||
|
'formats': formats,
|
||||||
|
'duration': int_or_none(video.get('duration'), 100),
|
||||||
|
'timestamp': parse_iso8601(video.get('created_date')),
|
||||||
|
'subtitles': subtitles,
|
||||||
|
'is_live': is_live,
|
||||||
|
}
|
||||||
@@ -187,13 +187,13 @@ class ARDMediathekIE(ARDMediathekBaseIE):
|
|||||||
if doc.tag == 'rss':
|
if doc.tag == 'rss':
|
||||||
return GenericIE()._extract_rss(url, video_id, doc)
|
return GenericIE()._extract_rss(url, video_id, doc)
|
||||||
|
|
||||||
title = self._html_search_regex(
|
title = self._og_search_title(webpage, default=None) or self._html_search_regex(
|
||||||
[r'<h1(?:\s+class="boxTopHeadline")?>(.*?)</h1>',
|
[r'<h1(?:\s+class="boxTopHeadline")?>(.*?)</h1>',
|
||||||
r'<meta name="dcterms\.title" content="(.*?)"/>',
|
r'<meta name="dcterms\.title" content="(.*?)"/>',
|
||||||
r'<h4 class="headline">(.*?)</h4>',
|
r'<h4 class="headline">(.*?)</h4>',
|
||||||
r'<title[^>]*>(.*?)</title>'],
|
r'<title[^>]*>(.*?)</title>'],
|
||||||
webpage, 'title')
|
webpage, 'title')
|
||||||
description = self._html_search_meta(
|
description = self._og_search_description(webpage, default=None) or self._html_search_meta(
|
||||||
'dcterms.abstract', webpage, 'description', default=None)
|
'dcterms.abstract', webpage, 'description', default=None)
|
||||||
if description is None:
|
if description is None:
|
||||||
description = self._html_search_meta(
|
description = self._html_search_meta(
|
||||||
@@ -249,28 +249,40 @@ class ARDMediathekIE(ARDMediathekBaseIE):
|
|||||||
|
|
||||||
|
|
||||||
class ARDIE(InfoExtractor):
|
class ARDIE(InfoExtractor):
|
||||||
_VALID_URL = r'(?P<mainurl>https?://(www\.)?daserste\.de/[^?#]+/videos/(?P<display_id>[^/?#]+)-(?P<id>[0-9]+))\.html'
|
_VALID_URL = r'(?P<mainurl>https?://(?:www\.)?daserste\.de/(?:[^/?#&]+/)+(?P<id>[^/?#&]+))\.html'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
# available till 14.02.2019
|
# available till 7.01.2022
|
||||||
'url': 'http://www.daserste.de/information/talk/maischberger/videos/das-groko-drama-zerlegen-sich-die-volksparteien-video-102.html',
|
'url': 'https://www.daserste.de/information/talk/maischberger/videos/maischberger-die-woche-video100.html',
|
||||||
'md5': '8e4ec85f31be7c7fc08a26cdbc5a1f49',
|
'md5': '867d8aa39eeaf6d76407c5ad1bb0d4c1',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'display_id': 'das-groko-drama-zerlegen-sich-die-volksparteien-video',
|
'id': 'maischberger-die-woche-video100',
|
||||||
'id': '102',
|
'display_id': 'maischberger-die-woche-video100',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'duration': 4435.0,
|
'duration': 3687.0,
|
||||||
'title': 'Das GroKo-Drama: Zerlegen sich die Volksparteien?',
|
'title': 'maischberger. die woche vom 7. Januar 2021',
|
||||||
'upload_date': '20180214',
|
'upload_date': '20210107',
|
||||||
'thumbnail': r're:^https?://.*\.jpg$',
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.daserste.de/information/politik-weltgeschehen/morgenmagazin/videosextern/dominik-kahun-aus-der-nhl-direkt-zur-weltmeisterschaft-100.html',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.daserste.de/information/nachrichten-wetter/tagesthemen/videosextern/tagesthemen-17736.html',
|
||||||
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.daserste.de/information/reportage-dokumentation/dokus/videos/die-story-im-ersten-mission-unter-falscher-flagge-100.html',
|
'url': 'http://www.daserste.de/information/reportage-dokumentation/dokus/videos/die-story-im-ersten-mission-unter-falscher-flagge-100.html',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.daserste.de/unterhaltung/serie/in-aller-freundschaft-die-jungen-aerzte/Drehpause-100.html',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.daserste.de/unterhaltung/film/filmmittwoch-im-ersten/videos/making-ofwendezeit-video-100.html',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
display_id = mobj.group('display_id')
|
display_id = mobj.group('id')
|
||||||
|
|
||||||
player_url = mobj.group('mainurl') + '~playerXml.xml'
|
player_url = mobj.group('mainurl') + '~playerXml.xml'
|
||||||
doc = self._download_xml(player_url, display_id)
|
doc = self._download_xml(player_url, display_id)
|
||||||
@@ -281,26 +293,63 @@ class ARDIE(InfoExtractor):
|
|||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
for a in video_node.findall('.//asset'):
|
for a in video_node.findall('.//asset'):
|
||||||
|
file_name = xpath_text(a, './fileName', default=None)
|
||||||
|
if not file_name:
|
||||||
|
continue
|
||||||
|
format_type = a.attrib.get('type')
|
||||||
|
format_url = url_or_none(file_name)
|
||||||
|
if format_url:
|
||||||
|
ext = determine_ext(file_name)
|
||||||
|
if ext == 'm3u8':
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
format_url, display_id, 'mp4', entry_protocol='m3u8_native',
|
||||||
|
m3u8_id=format_type or 'hls', fatal=False))
|
||||||
|
continue
|
||||||
|
elif ext == 'f4m':
|
||||||
|
formats.extend(self._extract_f4m_formats(
|
||||||
|
update_url_query(format_url, {'hdcore': '3.7.0'}),
|
||||||
|
display_id, f4m_id=format_type or 'hds', fatal=False))
|
||||||
|
continue
|
||||||
f = {
|
f = {
|
||||||
'format_id': a.attrib['type'],
|
'format_id': format_type,
|
||||||
'width': int_or_none(a.find('./frameWidth').text),
|
'width': int_or_none(xpath_text(a, './frameWidth')),
|
||||||
'height': int_or_none(a.find('./frameHeight').text),
|
'height': int_or_none(xpath_text(a, './frameHeight')),
|
||||||
'vbr': int_or_none(a.find('./bitrateVideo').text),
|
'vbr': int_or_none(xpath_text(a, './bitrateVideo')),
|
||||||
'abr': int_or_none(a.find('./bitrateAudio').text),
|
'abr': int_or_none(xpath_text(a, './bitrateAudio')),
|
||||||
'vcodec': a.find('./codecVideo').text,
|
'vcodec': xpath_text(a, './codecVideo'),
|
||||||
'tbr': int_or_none(a.find('./totalBitrate').text),
|
'tbr': int_or_none(xpath_text(a, './totalBitrate')),
|
||||||
}
|
}
|
||||||
if a.find('./serverPrefix').text:
|
server_prefix = xpath_text(a, './serverPrefix', default=None)
|
||||||
f['url'] = a.find('./serverPrefix').text
|
if server_prefix:
|
||||||
f['playpath'] = a.find('./fileName').text
|
f.update({
|
||||||
|
'url': server_prefix,
|
||||||
|
'playpath': file_name,
|
||||||
|
})
|
||||||
else:
|
else:
|
||||||
f['url'] = a.find('./fileName').text
|
if not format_url:
|
||||||
|
continue
|
||||||
|
f['url'] = format_url
|
||||||
formats.append(f)
|
formats.append(f)
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
_SUB_FORMATS = (
|
||||||
|
('./dataTimedText', 'ttml'),
|
||||||
|
('./dataTimedTextNoOffset', 'ttml'),
|
||||||
|
('./dataTimedTextVtt', 'vtt'),
|
||||||
|
)
|
||||||
|
|
||||||
|
subtitles = {}
|
||||||
|
for subsel, subext in _SUB_FORMATS:
|
||||||
|
for node in video_node.findall(subsel):
|
||||||
|
subtitles.setdefault('de', []).append({
|
||||||
|
'url': node.attrib['url'],
|
||||||
|
'ext': subext,
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': mobj.group('id'),
|
'id': xpath_text(video_node, './videoId', default=display_id),
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
'display_id': display_id,
|
'display_id': display_id,
|
||||||
'title': video_node.find('./title').text,
|
'title': video_node.find('./title').text,
|
||||||
'duration': parse_duration(video_node.find('./duration').text),
|
'duration': parse_duration(video_node.find('./duration').text),
|
||||||
@@ -310,39 +359,52 @@ class ARDIE(InfoExtractor):
|
|||||||
|
|
||||||
|
|
||||||
class ARDBetaMediathekIE(ARDMediathekBaseIE):
|
class ARDBetaMediathekIE(ARDMediathekBaseIE):
|
||||||
_VALID_URL = r'https://(?:beta|www)\.ardmediathek\.de/(?P<client>[^/]+)/(?:player|live)/(?P<video_id>[a-zA-Z0-9]+)(?:/(?P<display_id>[^/?#]+))?'
|
_VALID_URL = r'https://(?:(?:beta|www)\.)?ardmediathek\.de/(?:[^/]+/)?(?:player|live|video)/(?:[^/]+/)*(?P<id>Y3JpZDovL[a-zA-Z0-9]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://beta.ardmediathek.de/ard/player/Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhdG9ydC9mYmM4NGM1NC0xNzU4LTRmZGYtYWFhZS0wYzcyZTIxNGEyMDE/die-robuste-roswita',
|
'url': 'https://www.ardmediathek.de/mdr/video/die-robuste-roswita/Y3JpZDovL21kci5kZS9iZWl0cmFnL2Ntcy84MWMxN2MzZC0wMjkxLTRmMzUtODk4ZS0wYzhlOWQxODE2NGI/',
|
||||||
'md5': 'dfdc87d2e7e09d073d5a80770a9ce88f',
|
'md5': 'a1dc75a39c61601b980648f7c9f9f71d',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'display_id': 'die-robuste-roswita',
|
'display_id': 'die-robuste-roswita',
|
||||||
'id': '70153354',
|
'id': '78566716',
|
||||||
'title': 'Die robuste Roswita',
|
'title': 'Die robuste Roswita',
|
||||||
'description': r're:^Der Mord.*trüber ist als die Ilm.',
|
'description': r're:^Der Mord.*totgeglaubte Ehefrau Roswita',
|
||||||
'duration': 5316,
|
'duration': 5316,
|
||||||
'thumbnail': 'https://img.ardmediathek.de/standard/00/70/15/33/90/-1852531467/16x9/960?mandant=ard',
|
'thumbnail': 'https://img.ardmediathek.de/standard/00/78/56/67/84/575672121/16x9/960?mandant=ard',
|
||||||
'timestamp': 1577047500,
|
'timestamp': 1596658200,
|
||||||
'upload_date': '20191222',
|
'upload_date': '20200805',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://beta.ardmediathek.de/ard/video/Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhdG9ydC9mYmM4NGM1NC0xNzU4LTRmZGYtYWFhZS0wYzcyZTIxNGEyMDE',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://ardmediathek.de/ard/video/saartalk/saartalk-gesellschaftsgift-haltung-gegen-hass/sr-fernsehen/Y3JpZDovL3NyLW9ubGluZS5kZS9TVF84MTY4MA/',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.ardmediathek.de/ard/video/trailer/private-eyes-s01-e01/one/Y3JpZDovL3dkci5kZS9CZWl0cmFnLTE1MTgwYzczLWNiMTEtNGNkMS1iMjUyLTg5MGYzOWQxZmQ1YQ/',
|
||||||
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.ardmediathek.de/ard/player/Y3JpZDovL3N3ci5kZS9hZXgvbzEwNzE5MTU/',
|
'url': 'https://www.ardmediathek.de/ard/player/Y3JpZDovL3N3ci5kZS9hZXgvbzEwNzE5MTU/',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.ardmediathek.de/swr/live/Y3JpZDovL3N3ci5kZS8xMzQ4MTA0Mg',
|
'url': 'https://www.ardmediathek.de/swr/live/Y3JpZDovL3N3ci5kZS8xMzQ4MTA0Mg',
|
||||||
'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 _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
video_id = self._match_id(url)
|
||||||
video_id = mobj.group('video_id')
|
|
||||||
display_id = mobj.group('display_id') or video_id
|
|
||||||
|
|
||||||
player_page = self._download_json(
|
player_page = self._download_json(
|
||||||
'https://api.ardmediathek.de/public-gateway',
|
'https://api.ardmediathek.de/public-gateway',
|
||||||
display_id, data=json.dumps({
|
video_id, data=json.dumps({
|
||||||
'query': '''{
|
'query': '''{
|
||||||
playerPage(client:"%s", clipId: "%s") {
|
playerPage(client: "ard", clipId: "%s") {
|
||||||
blockedByFsk
|
blockedByFsk
|
||||||
broadcastedOn
|
broadcastedOn
|
||||||
maturityContentRating
|
maturityContentRating
|
||||||
@@ -372,7 +434,7 @@ class ARDBetaMediathekIE(ARDMediathekBaseIE):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}''' % (mobj.group('client'), video_id),
|
}''' % video_id,
|
||||||
}).encode(), headers={
|
}).encode(), headers={
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
})['data']['playerPage']
|
})['data']['playerPage']
|
||||||
@@ -397,7 +459,6 @@ class ARDBetaMediathekIE(ARDMediathekBaseIE):
|
|||||||
r'\(FSK\s*(\d+)\)\s*$', description, 'age limit', default=None))
|
r'\(FSK\s*(\d+)\)\s*$', description, 'age limit', default=None))
|
||||||
info.update({
|
info.update({
|
||||||
'age_limit': age_limit,
|
'age_limit': age_limit,
|
||||||
'display_id': display_id,
|
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': description,
|
'description': description,
|
||||||
'timestamp': unified_timestamp(player_page.get('broadcastedOn')),
|
'timestamp': unified_timestamp(player_page.get('broadcastedOn')),
|
||||||
|
|||||||
@@ -6,13 +6,11 @@ import re
|
|||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import compat_urlparse
|
from ..compat import compat_urlparse
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
determine_ext,
|
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
mimetype2ext,
|
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
strip_jsonp,
|
try_get,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -20,22 +18,27 @@ class ArkenaIE(InfoExtractor):
|
|||||||
_VALID_URL = r'''(?x)
|
_VALID_URL = r'''(?x)
|
||||||
https?://
|
https?://
|
||||||
(?:
|
(?:
|
||||||
video\.arkena\.com/play2/embed/player\?|
|
video\.(?:arkena|qbrick)\.com/play2/embed/player\?|
|
||||||
play\.arkena\.com/(?:config|embed)/avp/v\d/player/media/(?P<id>[^/]+)/[^/]+/(?P<account_id>\d+)
|
play\.arkena\.com/(?:config|embed)/avp/v\d/player/media/(?P<id>[^/]+)/[^/]+/(?P<account_id>\d+)
|
||||||
)
|
)
|
||||||
'''
|
'''
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://play.arkena.com/embed/avp/v2/player/media/b41dda37-d8e7-4d3f-b1b5-9a9db578bdfe/1/129411',
|
'url': 'https://video.qbrick.com/play2/embed/player?accountId=1034090&mediaId=d8ab4607-00090107-aab86310',
|
||||||
'md5': 'b96f2f71b359a8ecd05ce4e1daa72365',
|
'md5': '97f117754e5f3c020f5f26da4a44ebaf',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'b41dda37-d8e7-4d3f-b1b5-9a9db578bdfe',
|
'id': 'd8ab4607-00090107-aab86310',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Big Buck Bunny',
|
'title': 'EM_HT20_117_roslund_v2.mp4',
|
||||||
'description': 'Royalty free test video',
|
'timestamp': 1608285912,
|
||||||
'timestamp': 1432816365,
|
'upload_date': '20201218',
|
||||||
'upload_date': '20150528',
|
'duration': 1429.162667,
|
||||||
'is_live': False,
|
'subtitles': {
|
||||||
|
'sv': 'count:3',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://play.arkena.com/embed/avp/v2/player/media/b41dda37-d8e7-4d3f-b1b5-9a9db578bdfe/1/129411',
|
||||||
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://play.arkena.com/config/avp/v2/player/media/b41dda37-d8e7-4d3f-b1b5-9a9db578bdfe/1/129411/?callbackMethod=jQuery1111023664739129262213_1469227693893',
|
'url': 'https://play.arkena.com/config/avp/v2/player/media/b41dda37-d8e7-4d3f-b1b5-9a9db578bdfe/1/129411/?callbackMethod=jQuery1111023664739129262213_1469227693893',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
@@ -72,62 +75,89 @@ class ArkenaIE(InfoExtractor):
|
|||||||
if not video_id or not account_id:
|
if not video_id or not account_id:
|
||||||
raise ExtractorError('Invalid URL', expected=True)
|
raise ExtractorError('Invalid URL', expected=True)
|
||||||
|
|
||||||
playlist = self._download_json(
|
media = self._download_json(
|
||||||
'https://play.arkena.com/config/avp/v2/player/media/%s/0/%s/?callbackMethod=_'
|
'https://video.qbrick.com/api/v1/public/accounts/%s/medias/%s' % (account_id, video_id),
|
||||||
% (video_id, account_id),
|
video_id, query={
|
||||||
video_id, transform_source=strip_jsonp)['Playlist'][0]
|
# https://video.qbrick.com/docs/api/examples/library-api.html
|
||||||
|
'fields': 'asset/resources/*/renditions/*(height,id,language,links/*(href,mimeType),type,size,videos/*(audios/*(codec,sampleRate),bitrate,codec,duration,height,width),width),created,metadata/*(title,description),tags',
|
||||||
media_info = playlist['MediaInfo']
|
|
||||||
title = media_info['Title']
|
|
||||||
media_files = playlist['MediaFiles']
|
|
||||||
|
|
||||||
is_live = False
|
|
||||||
formats = []
|
|
||||||
for kind_case, kind_formats in media_files.items():
|
|
||||||
kind = kind_case.lower()
|
|
||||||
for f in kind_formats:
|
|
||||||
f_url = f.get('Url')
|
|
||||||
if not f_url:
|
|
||||||
continue
|
|
||||||
is_live = f.get('Live') == 'true'
|
|
||||||
exts = (mimetype2ext(f.get('Type')), determine_ext(f_url, None))
|
|
||||||
if kind == 'm3u8' or 'm3u8' in exts:
|
|
||||||
formats.extend(self._extract_m3u8_formats(
|
|
||||||
f_url, video_id, 'mp4', 'm3u8_native',
|
|
||||||
m3u8_id=kind, fatal=False, live=is_live))
|
|
||||||
elif kind == 'flash' or 'f4m' in exts:
|
|
||||||
formats.extend(self._extract_f4m_formats(
|
|
||||||
f_url, video_id, f4m_id=kind, fatal=False))
|
|
||||||
elif kind == 'dash' or 'mpd' in exts:
|
|
||||||
formats.extend(self._extract_mpd_formats(
|
|
||||||
f_url, video_id, mpd_id=kind, fatal=False))
|
|
||||||
elif kind == 'silverlight':
|
|
||||||
# TODO: process when ism is supported (see
|
|
||||||
# https://github.com/ytdl-org/youtube-dl/issues/8118)
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
tbr = float_or_none(f.get('Bitrate'), 1000)
|
|
||||||
formats.append({
|
|
||||||
'url': f_url,
|
|
||||||
'format_id': '%s-%d' % (kind, tbr) if tbr else kind,
|
|
||||||
'tbr': tbr,
|
|
||||||
})
|
})
|
||||||
self._sort_formats(formats)
|
metadata = media.get('metadata') or {}
|
||||||
|
title = metadata['title']
|
||||||
|
|
||||||
description = media_info.get('Description')
|
duration = None
|
||||||
video_id = media_info.get('VideoId') or video_id
|
formats = []
|
||||||
timestamp = parse_iso8601(media_info.get('PublishDate'))
|
thumbnails = []
|
||||||
thumbnails = [{
|
subtitles = {}
|
||||||
'url': thumbnail['Url'],
|
for resource in media['asset']['resources']:
|
||||||
'width': int_or_none(thumbnail.get('Size')),
|
for rendition in (resource.get('renditions') or []):
|
||||||
} for thumbnail in (media_info.get('Poster') or []) if thumbnail.get('Url')]
|
rendition_type = rendition.get('type')
|
||||||
|
for i, link in enumerate(rendition.get('links') or []):
|
||||||
|
href = link.get('href')
|
||||||
|
if not href:
|
||||||
|
continue
|
||||||
|
if rendition_type == 'image':
|
||||||
|
thumbnails.append({
|
||||||
|
'filesize': int_or_none(rendition.get('size')),
|
||||||
|
'height': int_or_none(rendition.get('height')),
|
||||||
|
'id': rendition.get('id'),
|
||||||
|
'url': href,
|
||||||
|
'width': int_or_none(rendition.get('width')),
|
||||||
|
})
|
||||||
|
elif rendition_type == 'subtitle':
|
||||||
|
subtitles.setdefault(rendition.get('language') or 'en', []).append({
|
||||||
|
'url': href,
|
||||||
|
})
|
||||||
|
elif rendition_type == 'video':
|
||||||
|
f = {
|
||||||
|
'filesize': int_or_none(rendition.get('size')),
|
||||||
|
'format_id': rendition.get('id'),
|
||||||
|
'url': href,
|
||||||
|
}
|
||||||
|
video = try_get(rendition, lambda x: x['videos'][i], dict)
|
||||||
|
if video:
|
||||||
|
if not duration:
|
||||||
|
duration = float_or_none(video.get('duration'))
|
||||||
|
f.update({
|
||||||
|
'height': int_or_none(video.get('height')),
|
||||||
|
'tbr': int_or_none(video.get('bitrate'), 1000),
|
||||||
|
'vcodec': video.get('codec'),
|
||||||
|
'width': int_or_none(video.get('width')),
|
||||||
|
})
|
||||||
|
audio = try_get(video, lambda x: x['audios'][0], dict)
|
||||||
|
if audio:
|
||||||
|
f.update({
|
||||||
|
'acodec': audio.get('codec'),
|
||||||
|
'asr': int_or_none(audio.get('sampleRate')),
|
||||||
|
})
|
||||||
|
formats.append(f)
|
||||||
|
elif rendition_type == 'index':
|
||||||
|
mime_type = link.get('mimeType')
|
||||||
|
if mime_type == 'application/smil+xml':
|
||||||
|
formats.extend(self._extract_smil_formats(
|
||||||
|
href, video_id, fatal=False))
|
||||||
|
elif mime_type == 'application/x-mpegURL':
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
href, video_id, 'mp4', 'm3u8_native',
|
||||||
|
m3u8_id='hls', fatal=False))
|
||||||
|
elif mime_type == 'application/hds+xml':
|
||||||
|
formats.extend(self._extract_f4m_formats(
|
||||||
|
href, video_id, f4m_id='hds', fatal=False))
|
||||||
|
elif mime_type == 'application/dash+xml':
|
||||||
|
formats.extend(self._extract_f4m_formats(
|
||||||
|
href, video_id, f4m_id='hds', fatal=False))
|
||||||
|
elif mime_type == 'application/vnd.ms-sstr+xml':
|
||||||
|
formats.extend(self._extract_ism_formats(
|
||||||
|
href, video_id, ism_id='mss', fatal=False))
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': description,
|
'description': metadata.get('description'),
|
||||||
'timestamp': timestamp,
|
'timestamp': parse_iso8601(media.get('created')),
|
||||||
'is_live': is_live,
|
|
||||||
'thumbnails': thumbnails,
|
'thumbnails': thumbnails,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
'duration': duration,
|
||||||
|
'tags': media.get('tags'),
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
}
|
}
|
||||||
|
|||||||
101
youtube_dl/extractor/arnes.py
Normal file
101
youtube_dl/extractor/arnes.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..compat import (
|
||||||
|
compat_parse_qs,
|
||||||
|
compat_urllib_parse_urlparse,
|
||||||
|
)
|
||||||
|
from ..utils import (
|
||||||
|
float_or_none,
|
||||||
|
int_or_none,
|
||||||
|
parse_iso8601,
|
||||||
|
remove_start,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ArnesIE(InfoExtractor):
|
||||||
|
IE_NAME = 'video.arnes.si'
|
||||||
|
IE_DESC = 'Arnes Video'
|
||||||
|
_VALID_URL = r'https?://video\.arnes\.si/(?:[a-z]{2}/)?(?:watch|embed|api/(?:asset|public/video))/(?P<id>[0-9a-zA-Z]{12})'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://video.arnes.si/watch/a1qrWTOQfVoU?t=10',
|
||||||
|
'md5': '4d0f4d0a03571b33e1efac25fd4a065d',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'a1qrWTOQfVoU',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Linearna neodvisnost, definicija',
|
||||||
|
'description': 'Linearna neodvisnost, definicija',
|
||||||
|
'license': 'PRIVATE',
|
||||||
|
'creator': 'Polona Oblak',
|
||||||
|
'timestamp': 1585063725,
|
||||||
|
'upload_date': '20200324',
|
||||||
|
'channel': 'Polona Oblak',
|
||||||
|
'channel_id': 'q6pc04hw24cj',
|
||||||
|
'channel_url': 'https://video.arnes.si/?channel=q6pc04hw24cj',
|
||||||
|
'duration': 596.75,
|
||||||
|
'view_count': int,
|
||||||
|
'tags': ['linearna_algebra'],
|
||||||
|
'start_time': 10,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'https://video.arnes.si/api/asset/s1YjnV7hadlC/play.mp4',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://video.arnes.si/embed/s1YjnV7hadlC',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://video.arnes.si/en/watch/s1YjnV7hadlC',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://video.arnes.si/embed/s1YjnV7hadlC?t=123&hideRelated=1',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://video.arnes.si/api/public/video/s1YjnV7hadlC',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
_BASE_URL = 'https://video.arnes.si'
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
video = self._download_json(
|
||||||
|
self._BASE_URL + '/api/public/video/' + video_id, video_id)['data']
|
||||||
|
title = video['title']
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
for media in (video.get('media') or []):
|
||||||
|
media_url = media.get('url')
|
||||||
|
if not media_url:
|
||||||
|
continue
|
||||||
|
formats.append({
|
||||||
|
'url': self._BASE_URL + media_url,
|
||||||
|
'format_id': remove_start(media.get('format'), 'FORMAT_'),
|
||||||
|
'format_note': media.get('formatTranslation'),
|
||||||
|
'width': int_or_none(media.get('width')),
|
||||||
|
'height': int_or_none(media.get('height')),
|
||||||
|
})
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
channel = video.get('channel') or {}
|
||||||
|
channel_id = channel.get('url')
|
||||||
|
thumbnail = video.get('thumbnailUrl')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'formats': formats,
|
||||||
|
'thumbnail': self._BASE_URL + thumbnail,
|
||||||
|
'description': video.get('description'),
|
||||||
|
'license': video.get('license'),
|
||||||
|
'creator': video.get('author'),
|
||||||
|
'timestamp': parse_iso8601(video.get('creationTime')),
|
||||||
|
'channel': channel.get('name'),
|
||||||
|
'channel_id': channel_id,
|
||||||
|
'channel_url': self._BASE_URL + '/?channel=' + channel_id if channel_id else None,
|
||||||
|
'duration': float_or_none(video.get('duration'), 1000),
|
||||||
|
'view_count': int_or_none(video.get('views')),
|
||||||
|
'tags': video.get('hashtags'),
|
||||||
|
'start_time': int_or_none(compat_parse_qs(
|
||||||
|
compat_urllib_parse_urlparse(url).query).get('t', [None])[0]),
|
||||||
|
}
|
||||||
@@ -4,23 +4,58 @@ from __future__ import unicode_literals
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import compat_str
|
from ..compat import (
|
||||||
|
compat_str,
|
||||||
|
compat_urlparse,
|
||||||
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
qualities,
|
qualities,
|
||||||
|
strip_or_none,
|
||||||
try_get,
|
try_get,
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
# There are different sources of video in arte.tv, the extraction process
|
|
||||||
# is different for each one. The videos usually expire in 7 days, so we can't
|
|
||||||
# add tests.
|
|
||||||
|
|
||||||
|
|
||||||
class ArteTVBaseIE(InfoExtractor):
|
class ArteTVBaseIE(InfoExtractor):
|
||||||
def _extract_from_json_url(self, json_url, video_id, lang, title=None):
|
_ARTE_LANGUAGES = 'fr|de|en|es|it|pl'
|
||||||
info = self._download_json(json_url, video_id)
|
_API_BASE = 'https://api.arte.tv/api/player/v1'
|
||||||
|
|
||||||
|
|
||||||
|
class ArteTVIE(ArteTVBaseIE):
|
||||||
|
_VALID_URL = r'''(?x)
|
||||||
|
https?://
|
||||||
|
(?:
|
||||||
|
(?:www\.)?arte\.tv/(?P<lang>%(langs)s)/videos|
|
||||||
|
api\.arte\.tv/api/player/v\d+/config/(?P<lang_2>%(langs)s)
|
||||||
|
)
|
||||||
|
/(?P<id>\d{6}-\d{3}-[AF])
|
||||||
|
''' % {'langs': ArteTVBaseIE._ARTE_LANGUAGES}
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.arte.tv/en/videos/088501-000-A/mexico-stealing-petrol-to-survive/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '088501-000-A',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Mexico: Stealing Petrol to Survive',
|
||||||
|
'upload_date': '20190628',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.arte.tv/pl/videos/100103-000-A/usa-dyskryminacja-na-porodowce/',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://api.arte.tv/api/player/v2/config/de/100605-013-A',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
mobj = re.match(self._VALID_URL, url)
|
||||||
|
video_id = mobj.group('id')
|
||||||
|
lang = mobj.group('lang') or mobj.group('lang_2')
|
||||||
|
|
||||||
|
info = self._download_json(
|
||||||
|
'%s/config/%s/%s' % (self._API_BASE, lang, video_id), video_id)
|
||||||
player_info = info['videoJsonPlayer']
|
player_info = info['videoJsonPlayer']
|
||||||
|
|
||||||
vsr = try_get(player_info, lambda x: x['VSR'], dict)
|
vsr = try_get(player_info, lambda x: x['VSR'], dict)
|
||||||
@@ -37,18 +72,11 @@ class ArteTVBaseIE(InfoExtractor):
|
|||||||
if not upload_date_str:
|
if not upload_date_str:
|
||||||
upload_date_str = (player_info.get('VRA') or player_info.get('VDA') or '').split(' ')[0]
|
upload_date_str = (player_info.get('VRA') or player_info.get('VDA') or '').split(' ')[0]
|
||||||
|
|
||||||
title = (player_info.get('VTI') or title or player_info['VID']).strip()
|
title = (player_info.get('VTI') or player_info['VID']).strip()
|
||||||
subtitle = player_info.get('VSU', '').strip()
|
subtitle = player_info.get('VSU', '').strip()
|
||||||
if subtitle:
|
if subtitle:
|
||||||
title += ' - %s' % subtitle
|
title += ' - %s' % subtitle
|
||||||
|
|
||||||
info_dict = {
|
|
||||||
'id': player_info['VID'],
|
|
||||||
'title': title,
|
|
||||||
'description': player_info.get('VDE'),
|
|
||||||
'upload_date': unified_strdate(upload_date_str),
|
|
||||||
'thumbnail': player_info.get('programImage') or player_info.get('VTU', {}).get('IUR'),
|
|
||||||
}
|
|
||||||
qfunc = qualities(['MQ', 'HQ', 'EQ', 'SQ'])
|
qfunc = qualities(['MQ', 'HQ', 'EQ', 'SQ'])
|
||||||
|
|
||||||
LANGS = {
|
LANGS = {
|
||||||
@@ -65,6 +93,10 @@ class ArteTVBaseIE(InfoExtractor):
|
|||||||
formats = []
|
formats = []
|
||||||
for format_id, format_dict in vsr.items():
|
for format_id, format_dict in vsr.items():
|
||||||
f = dict(format_dict)
|
f = dict(format_dict)
|
||||||
|
format_url = url_or_none(f.get('url'))
|
||||||
|
streamer = f.get('streamer')
|
||||||
|
if not format_url and not streamer:
|
||||||
|
continue
|
||||||
versionCode = f.get('versionCode')
|
versionCode = f.get('versionCode')
|
||||||
l = re.escape(langcode)
|
l = re.escape(langcode)
|
||||||
|
|
||||||
@@ -107,6 +139,16 @@ class ArteTVBaseIE(InfoExtractor):
|
|||||||
else:
|
else:
|
||||||
lang_pref = -1
|
lang_pref = -1
|
||||||
|
|
||||||
|
media_type = f.get('mediaType')
|
||||||
|
if media_type == 'hls':
|
||||||
|
m3u8_formats = self._extract_m3u8_formats(
|
||||||
|
format_url, video_id, 'mp4', entry_protocol='m3u8_native',
|
||||||
|
m3u8_id=format_id, fatal=False)
|
||||||
|
for m3u8_format in m3u8_formats:
|
||||||
|
m3u8_format['language_preference'] = lang_pref
|
||||||
|
formats.extend(m3u8_formats)
|
||||||
|
continue
|
||||||
|
|
||||||
format = {
|
format = {
|
||||||
'format_id': format_id,
|
'format_id': format_id,
|
||||||
'preference': -10 if f.get('videoFormat') == 'M3U8' else None,
|
'preference': -10 if f.get('videoFormat') == 'M3U8' else None,
|
||||||
@@ -118,7 +160,7 @@ class ArteTVBaseIE(InfoExtractor):
|
|||||||
'quality': qfunc(f.get('quality')),
|
'quality': qfunc(f.get('quality')),
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.get('mediaType') == 'rtmp':
|
if media_type == 'rtmp':
|
||||||
format['url'] = f['streamer']
|
format['url'] = f['streamer']
|
||||||
format['play_path'] = 'mp4:' + f['url']
|
format['play_path'] = 'mp4:' + f['url']
|
||||||
format['ext'] = 'flv'
|
format['ext'] = 'flv'
|
||||||
@@ -127,56 +169,50 @@ class ArteTVBaseIE(InfoExtractor):
|
|||||||
|
|
||||||
formats.append(format)
|
formats.append(format)
|
||||||
|
|
||||||
self._check_formats(formats, video_id)
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
info_dict['formats'] = formats
|
return {
|
||||||
return info_dict
|
'id': player_info.get('VID') or video_id,
|
||||||
|
'title': title,
|
||||||
|
'description': player_info.get('VDE'),
|
||||||
|
'upload_date': unified_strdate(upload_date_str),
|
||||||
|
'thumbnail': player_info.get('programImage') or player_info.get('VTU', {}).get('IUR'),
|
||||||
|
'formats': formats,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ArteTVPlus7IE(ArteTVBaseIE):
|
class ArteTVEmbedIE(InfoExtractor):
|
||||||
IE_NAME = 'arte.tv:+7'
|
_VALID_URL = r'https?://(?:www\.)?arte\.tv/player/v\d+/index\.php\?.*?\bjson_url=.+'
|
||||||
_VALID_URL = r'https?://(?:www\.)?arte\.tv/(?P<lang>fr|de|en|es|it|pl)/videos/(?P<id>\d{6}-\d{3}-[AF])'
|
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.arte.tv/en/videos/088501-000-A/mexico-stealing-petrol-to-survive/',
|
'url': 'https://www.arte.tv/player/v5/index.php?json_url=https%3A%2F%2Fapi.arte.tv%2Fapi%2Fplayer%2Fv2%2Fconfig%2Fde%2F100605-013-A&lang=de&autoplay=true&mute=0100605-013-A',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '088501-000-A',
|
'id': '100605-013-A',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Mexico: Stealing Petrol to Survive',
|
'title': 'United we Stream November Lockdown Edition #13',
|
||||||
'upload_date': '20190628',
|
'description': 'md5:be40b667f45189632b78c1425c7c2ce1',
|
||||||
|
'upload_date': '20201116',
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.arte.tv/player/v3/index.php?json_url=https://api.arte.tv/api/player/v2/config/de/100605-013-A',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
@staticmethod
|
||||||
lang, video_id = re.match(self._VALID_URL, url).groups()
|
def _extract_urls(webpage):
|
||||||
return self._extract_from_json_url(
|
return [url for _, url in re.findall(
|
||||||
'https://api.arte.tv/api/player/v1/config/%s/%s' % (lang, video_id),
|
r'<(?:iframe|script)[^>]+src=(["\'])(?P<url>(?:https?:)?//(?:www\.)?arte\.tv/player/v\d+/index\.php\?.*?\bjson_url=.+?)\1',
|
||||||
video_id, lang)
|
webpage)]
|
||||||
|
|
||||||
|
|
||||||
class ArteTVEmbedIE(ArteTVPlus7IE):
|
|
||||||
IE_NAME = 'arte.tv:embed'
|
|
||||||
_VALID_URL = r'''(?x)
|
|
||||||
https://www\.arte\.tv
|
|
||||||
/player/v3/index\.php\?json_url=
|
|
||||||
(?P<json_url>
|
|
||||||
https?://api\.arte\.tv/api/player/v1/config/
|
|
||||||
(?P<lang>[^/]+)/(?P<id>\d{6}-\d{3}-[AF])
|
|
||||||
)
|
|
||||||
'''
|
|
||||||
|
|
||||||
_TESTS = []
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
json_url, lang, video_id = re.match(self._VALID_URL, url).groups()
|
qs = compat_urlparse.parse_qs(compat_urlparse.urlparse(url).query)
|
||||||
return self._extract_from_json_url(json_url, video_id, lang)
|
json_url = qs['json_url'][0]
|
||||||
|
video_id = ArteTVIE._match_id(json_url)
|
||||||
|
return self.url_result(
|
||||||
|
json_url, ie=ArteTVIE.ie_key(), video_id=video_id)
|
||||||
|
|
||||||
|
|
||||||
class ArteTVPlaylistIE(ArteTVBaseIE):
|
class ArteTVPlaylistIE(ArteTVBaseIE):
|
||||||
IE_NAME = 'arte.tv:playlist'
|
_VALID_URL = r'https?://(?:www\.)?arte\.tv/(?P<lang>%s)/videos/(?P<id>RC-\d{6})' % ArteTVBaseIE._ARTE_LANGUAGES
|
||||||
_VALID_URL = r'https?://(?:www\.)?arte\.tv/(?P<lang>fr|de|en|es|it|pl)/videos/(?P<id>RC-\d{6})'
|
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.arte.tv/en/videos/RC-016954/earn-a-living/',
|
'url': 'https://www.arte.tv/en/videos/RC-016954/earn-a-living/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -185,17 +221,81 @@ class ArteTVPlaylistIE(ArteTVBaseIE):
|
|||||||
'description': 'md5:d322c55011514b3a7241f7fb80d494c2',
|
'description': 'md5:d322c55011514b3a7241f7fb80d494c2',
|
||||||
},
|
},
|
||||||
'playlist_mincount': 6,
|
'playlist_mincount': 6,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.arte.tv/pl/videos/RC-014123/arte-reportage/',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
lang, playlist_id = re.match(self._VALID_URL, url).groups()
|
lang, playlist_id = re.match(self._VALID_URL, url).groups()
|
||||||
collection = self._download_json(
|
collection = self._download_json(
|
||||||
'https://api.arte.tv/api/player/v1/collectionData/%s/%s?source=videos'
|
'%s/collectionData/%s/%s?source=videos'
|
||||||
% (lang, playlist_id), playlist_id)
|
% (self._API_BASE, lang, playlist_id), playlist_id)
|
||||||
|
entries = []
|
||||||
|
for video in collection['videos']:
|
||||||
|
if not isinstance(video, dict):
|
||||||
|
continue
|
||||||
|
video_url = url_or_none(video.get('url')) or url_or_none(video.get('jsonUrl'))
|
||||||
|
if not video_url:
|
||||||
|
continue
|
||||||
|
video_id = video.get('programId')
|
||||||
|
entries.append({
|
||||||
|
'_type': 'url_transparent',
|
||||||
|
'url': video_url,
|
||||||
|
'id': video_id,
|
||||||
|
'title': video.get('title'),
|
||||||
|
'alt_title': video.get('subtitle'),
|
||||||
|
'thumbnail': url_or_none(try_get(video, lambda x: x['mainImage']['url'], compat_str)),
|
||||||
|
'duration': int_or_none(video.get('durationSeconds')),
|
||||||
|
'view_count': int_or_none(video.get('views')),
|
||||||
|
'ie_key': ArteTVIE.ie_key(),
|
||||||
|
})
|
||||||
title = collection.get('title')
|
title = collection.get('title')
|
||||||
description = collection.get('shortDescription') or collection.get('teaserText')
|
description = collection.get('shortDescription') or collection.get('teaserText')
|
||||||
entries = [
|
|
||||||
self._extract_from_json_url(
|
|
||||||
video['jsonUrl'], video.get('programId') or playlist_id, lang)
|
|
||||||
for video in collection['videos'] if video.get('jsonUrl')]
|
|
||||||
return self.playlist_result(entries, playlist_id, title, description)
|
return self.playlist_result(entries, playlist_id, title, description)
|
||||||
|
|
||||||
|
|
||||||
|
class ArteTVCategoryIE(ArteTVBaseIE):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?arte\.tv/(?P<lang>%s)/videos/(?P<id>[\w-]+(?:/[\w-]+)*)/?\s*$' % ArteTVBaseIE._ARTE_LANGUAGES
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.arte.tv/en/videos/politics-and-society/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'politics-and-society',
|
||||||
|
'title': 'Politics and society',
|
||||||
|
'description': 'Investigative documentary series, geopolitical analysis, and international commentary',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 13,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def suitable(cls, url):
|
||||||
|
return (
|
||||||
|
not any(ie.suitable(url) for ie in (ArteTVIE, ArteTVPlaylistIE, ))
|
||||||
|
and super(ArteTVCategoryIE, cls).suitable(url))
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
lang, playlist_id = re.match(self._VALID_URL, url).groups()
|
||||||
|
webpage = self._download_webpage(url, playlist_id)
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for video in re.finditer(
|
||||||
|
r'<a\b[^>]*?href\s*=\s*(?P<q>"|\'|\b)(?P<url>https?://www\.arte\.tv/%s/videos/[\w/-]+)(?P=q)' % lang,
|
||||||
|
webpage):
|
||||||
|
video = video.group('url')
|
||||||
|
if video == url:
|
||||||
|
continue
|
||||||
|
if any(ie.suitable(video) for ie in (ArteTVIE, ArteTVPlaylistIE, )):
|
||||||
|
items.append(video)
|
||||||
|
|
||||||
|
if items:
|
||||||
|
title = (self._og_search_title(webpage, default=None)
|
||||||
|
or self._html_search_regex(r'<title\b[^>]*>([^<]+)</title>', default=None))
|
||||||
|
title = strip_or_none(title.rsplit('|', 1)[0]) or self._generic_title(url)
|
||||||
|
|
||||||
|
result = self.playlist_from_matches(items, playlist_id=playlist_id, playlist_title=title)
|
||||||
|
if result:
|
||||||
|
description = self._og_search_description(webpage, default=None)
|
||||||
|
if description:
|
||||||
|
result['description'] = description
|
||||||
|
return result
|
||||||
|
|||||||
@@ -1,27 +1,91 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import functools
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from .kaltura import KalturaIE
|
from .kaltura import KalturaIE
|
||||||
from ..utils import extract_attributes
|
from ..utils import (
|
||||||
|
extract_attributes,
|
||||||
|
int_or_none,
|
||||||
|
OnDemandPagedList,
|
||||||
|
parse_age_limit,
|
||||||
|
strip_or_none,
|
||||||
|
try_get,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AsianCrushIE(InfoExtractor):
|
class AsianCrushBaseIE(InfoExtractor):
|
||||||
_VALID_URL_BASE = r'https?://(?:www\.)?(?P<host>(?:(?:asiancrush|yuyutv|midnightpulp)\.com|cocoro\.tv))'
|
_VALID_URL_BASE = r'https?://(?:www\.)?(?P<host>(?:(?:asiancrush|yuyutv|midnightpulp)\.com|(?:cocoro|retrocrush)\.tv))'
|
||||||
_VALID_URL = r'%s/video/(?:[^/]+/)?0+(?P<id>\d+)v\b' % _VALID_URL_BASE
|
_KALTURA_KEYS = [
|
||||||
|
'video_url', 'progressive_url', 'download_url', 'thumbnail_url',
|
||||||
|
'widescreen_thumbnail_url', 'screencap_widescreen',
|
||||||
|
]
|
||||||
|
_API_SUFFIX = {'retrocrush.tv': '-ott'}
|
||||||
|
|
||||||
|
def _call_api(self, host, endpoint, video_id, query, resource):
|
||||||
|
return self._download_json(
|
||||||
|
'https://api%s.%s/%s' % (self._API_SUFFIX.get(host, ''), host, endpoint), video_id,
|
||||||
|
'Downloading %s JSON metadata' % resource, query=query,
|
||||||
|
headers=self.geo_verification_headers())['objects']
|
||||||
|
|
||||||
|
def _download_object_data(self, host, object_id, resource):
|
||||||
|
return self._call_api(
|
||||||
|
host, 'search', object_id, {'id': object_id}, resource)[0]
|
||||||
|
|
||||||
|
def _get_object_description(self, obj):
|
||||||
|
return strip_or_none(obj.get('long_description') or obj.get('short_description'))
|
||||||
|
|
||||||
|
def _parse_video_data(self, video):
|
||||||
|
title = video['name']
|
||||||
|
|
||||||
|
entry_id, partner_id = [None] * 2
|
||||||
|
for k in self._KALTURA_KEYS:
|
||||||
|
k_url = video.get(k)
|
||||||
|
if k_url:
|
||||||
|
mobj = re.search(r'/p/(\d+)/.+?/entryId/([^/]+)/', k_url)
|
||||||
|
if mobj:
|
||||||
|
partner_id, entry_id = mobj.groups()
|
||||||
|
break
|
||||||
|
|
||||||
|
meta_categories = try_get(video, lambda x: x['meta']['categories'], list) or []
|
||||||
|
categories = list(filter(None, [c.get('name') for c in meta_categories]))
|
||||||
|
|
||||||
|
show_info = video.get('show_info') or {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'_type': 'url_transparent',
|
||||||
|
'url': 'kaltura:%s:%s' % (partner_id, entry_id),
|
||||||
|
'ie_key': KalturaIE.ie_key(),
|
||||||
|
'id': entry_id,
|
||||||
|
'title': title,
|
||||||
|
'description': self._get_object_description(video),
|
||||||
|
'age_limit': parse_age_limit(video.get('mpaa_rating') or video.get('tv_rating')),
|
||||||
|
'categories': categories,
|
||||||
|
'series': show_info.get('show_name'),
|
||||||
|
'season_number': int_or_none(show_info.get('season_num')),
|
||||||
|
'season_id': show_info.get('season_id'),
|
||||||
|
'episode_number': int_or_none(show_info.get('episode_num')),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AsianCrushIE(AsianCrushBaseIE):
|
||||||
|
_VALID_URL = r'%s/video/(?:[^/]+/)?0+(?P<id>\d+)v\b' % AsianCrushBaseIE._VALID_URL_BASE
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.asiancrush.com/video/012869v/women-who-flirt/',
|
'url': 'https://www.asiancrush.com/video/004289v/women-who-flirt',
|
||||||
'md5': 'c3b740e48d0ba002a42c0b72857beae6',
|
'md5': 'c3b740e48d0ba002a42c0b72857beae6',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '1_y4tmjm5r',
|
'id': '1_y4tmjm5r',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Women Who Flirt',
|
'title': 'Women Who Flirt',
|
||||||
'description': 'md5:7e986615808bcfb11756eb503a751487',
|
'description': 'md5:b65c7e0ae03a85585476a62a186f924c',
|
||||||
'timestamp': 1496936429,
|
'timestamp': 1496936429,
|
||||||
'upload_date': '20170608',
|
'upload_date': '20170608',
|
||||||
'uploader_id': 'craig@crifkin.com',
|
'uploader_id': 'craig@crifkin.com',
|
||||||
|
'age_limit': 13,
|
||||||
|
'categories': 'count:5',
|
||||||
|
'duration': 5812,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.asiancrush.com/video/she-was-pretty/011886v-pretty-episode-3/',
|
'url': 'https://www.asiancrush.com/video/she-was-pretty/011886v-pretty-episode-3/',
|
||||||
@@ -41,67 +105,35 @@ class AsianCrushIE(InfoExtractor):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'https://www.cocoro.tv/video/the-wonderful-wizard-of-oz/008878v-the-wonderful-wizard-of-oz-ep01/',
|
'url': 'https://www.cocoro.tv/video/the-wonderful-wizard-of-oz/008878v-the-wonderful-wizard-of-oz-ep01/',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.retrocrush.tv/video/true-tears/012328v-i...gave-away-my-tears',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
host, video_id = re.match(self._VALID_URL, url).groups()
|
||||||
host = mobj.group('host')
|
|
||||||
video_id = mobj.group('id')
|
|
||||||
|
|
||||||
|
if host == 'cocoro.tv':
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
embed_vars = self._parse_json(self._search_regex(
|
||||||
entry_id, partner_id, title = [None] * 3
|
|
||||||
|
|
||||||
vars = self._parse_json(
|
|
||||||
self._search_regex(
|
|
||||||
r'iEmbedVars\s*=\s*({.+?})', webpage, 'embed vars',
|
r'iEmbedVars\s*=\s*({.+?})', webpage, 'embed vars',
|
||||||
default='{}'), video_id, fatal=False)
|
default='{}'), video_id, fatal=False) or {}
|
||||||
if vars:
|
video_id = embed_vars.get('entry_id') or video_id
|
||||||
entry_id = vars.get('entry_id')
|
|
||||||
partner_id = vars.get('partner_id')
|
|
||||||
title = vars.get('vid_label')
|
|
||||||
|
|
||||||
if not entry_id:
|
video = self._download_object_data(host, video_id, 'video')
|
||||||
entry_id = self._search_regex(
|
return self._parse_video_data(video)
|
||||||
r'\bentry_id["\']\s*:\s*["\'](\d+)', webpage, 'entry id')
|
|
||||||
|
|
||||||
player = self._download_webpage(
|
|
||||||
'https://api.%s/embeddedVideoPlayer' % host, video_id,
|
|
||||||
query={'id': entry_id})
|
|
||||||
|
|
||||||
kaltura_id = self._search_regex(
|
|
||||||
r'entry_id["\']\s*:\s*(["\'])(?P<id>(?:(?!\1).)+)\1', player,
|
|
||||||
'kaltura id', group='id')
|
|
||||||
|
|
||||||
if not partner_id:
|
|
||||||
partner_id = self._search_regex(
|
|
||||||
r'/p(?:artner_id)?/(\d+)', player, 'partner id',
|
|
||||||
default='513551')
|
|
||||||
|
|
||||||
description = self._html_search_regex(
|
|
||||||
r'(?s)<div[^>]+\bclass=["\']description["\'][^>]*>(.+?)</div>',
|
|
||||||
webpage, 'description', fatal=False)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'_type': 'url_transparent',
|
|
||||||
'url': 'kaltura:%s:%s' % (partner_id, kaltura_id),
|
|
||||||
'ie_key': KalturaIE.ie_key(),
|
|
||||||
'id': video_id,
|
|
||||||
'title': title,
|
|
||||||
'description': description,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class AsianCrushPlaylistIE(InfoExtractor):
|
class AsianCrushPlaylistIE(AsianCrushBaseIE):
|
||||||
_VALID_URL = r'%s/series/0+(?P<id>\d+)s\b' % AsianCrushIE._VALID_URL_BASE
|
_VALID_URL = r'%s/series/0+(?P<id>\d+)s\b' % AsianCrushBaseIE._VALID_URL_BASE
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.asiancrush.com/series/012481s/scholar-walks-night/',
|
'url': 'https://www.asiancrush.com/series/006447s/fruity-samurai',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '12481',
|
'id': '6447',
|
||||||
'title': 'Scholar Who Walks the Night',
|
'title': 'Fruity Samurai',
|
||||||
'description': 'md5:7addd7c5132a09fd4741152d96cce886',
|
'description': 'md5:7535174487e4a202d3872a7fc8f2f154',
|
||||||
},
|
},
|
||||||
'playlist_count': 20,
|
'playlist_count': 13,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.yuyutv.com/series/013920s/peep-show/',
|
'url': 'https://www.yuyutv.com/series/013920s/peep-show/',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
@@ -111,11 +143,27 @@ class AsianCrushPlaylistIE(InfoExtractor):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'https://www.cocoro.tv/series/008549s/the-wonderful-wizard-of-oz/',
|
'url': 'https://www.cocoro.tv/series/008549s/the-wonderful-wizard-of-oz/',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.retrocrush.tv/series/012355s/true-tears',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
_PAGE_SIZE = 1000000000
|
||||||
|
|
||||||
|
def _fetch_page(self, domain, parent_id, page):
|
||||||
|
videos = self._call_api(
|
||||||
|
domain, 'getreferencedobjects', parent_id, {
|
||||||
|
'max': self._PAGE_SIZE,
|
||||||
|
'object_type': 'video',
|
||||||
|
'parent_id': parent_id,
|
||||||
|
'start': page * self._PAGE_SIZE,
|
||||||
|
}, 'page %d' % (page + 1))
|
||||||
|
for video in videos:
|
||||||
|
yield self._parse_video_data(video)
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
playlist_id = self._match_id(url)
|
host, playlist_id = re.match(self._VALID_URL, url).groups()
|
||||||
|
|
||||||
|
if host == 'cocoro.tv':
|
||||||
webpage = self._download_webpage(url, playlist_id)
|
webpage = self._download_webpage(url, playlist_id)
|
||||||
|
|
||||||
entries = []
|
entries = []
|
||||||
@@ -141,5 +189,12 @@ class AsianCrushPlaylistIE(InfoExtractor):
|
|||||||
description = self._og_search_description(
|
description = self._og_search_description(
|
||||||
webpage, default=None) or self._html_search_meta(
|
webpage, default=None) or self._html_search_meta(
|
||||||
'twitter:description', webpage, 'description', fatal=False)
|
'twitter:description', webpage, 'description', fatal=False)
|
||||||
|
else:
|
||||||
|
show = self._download_object_data(host, playlist_id, 'show')
|
||||||
|
title = show.get('name')
|
||||||
|
description = self._get_object_description(show)
|
||||||
|
entries = OnDemandPagedList(
|
||||||
|
functools.partial(self._fetch_page, host, playlist_id),
|
||||||
|
self._PAGE_SIZE)
|
||||||
|
|
||||||
return self.playlist_result(entries, playlist_id, title, description)
|
return self.playlist_result(entries, playlist_id, title, description)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -29,25 +29,27 @@ class AudiomackIE(InfoExtractor):
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
# audiomack wrapper around soundcloud song
|
# audiomack wrapper around soundcloud song
|
||||||
|
# Needs new test URL.
|
||||||
{
|
{
|
||||||
'add_ie': ['Soundcloud'],
|
'add_ie': ['Soundcloud'],
|
||||||
'url': 'http://www.audiomack.com/song/hip-hop-daily/black-mamba-freestyle',
|
'url': 'http://www.audiomack.com/song/hip-hop-daily/black-mamba-freestyle',
|
||||||
'info_dict': {
|
'only_matching': True,
|
||||||
'id': '258901379',
|
# 'info_dict': {
|
||||||
'ext': 'mp3',
|
# 'id': '258901379',
|
||||||
'description': 'mamba day freestyle for the legend Kobe Bryant ',
|
# 'ext': 'mp3',
|
||||||
'title': 'Black Mamba Freestyle [Prod. By Danny Wolf]',
|
# 'description': 'mamba day freestyle for the legend Kobe Bryant ',
|
||||||
'uploader': 'ILOVEMAKONNEN',
|
# 'title': 'Black Mamba Freestyle [Prod. By Danny Wolf]',
|
||||||
'upload_date': '20160414',
|
# 'uploader': 'ILOVEMAKONNEN',
|
||||||
}
|
# 'upload_date': '20160414',
|
||||||
|
# }
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
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 +75,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 +97,24 @@ 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) - 10. 4 Minutes Of Hell Part 4 (prod by DY OF 808 MAFIA)',
|
||||||
'id': '837577',
|
'id': '837580',
|
||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'uploader': 'Lil Herb a.k.a. G Herbo',
|
'uploader': 'Lil Herb a.k.a. G Herbo',
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
'params': {
|
'params': {
|
||||||
'playliststart': 9,
|
'playliststart': 2,
|
||||||
'playlistend': 9,
|
'playlistend': 2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
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 +136,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)),
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class AWAANBaseIE(InfoExtractor):
|
|||||||
'duration': int_or_none(video_data.get('duration')),
|
'duration': int_or_none(video_data.get('duration')),
|
||||||
'timestamp': parse_iso8601(video_data.get('create_time'), ' '),
|
'timestamp': parse_iso8601(video_data.get('create_time'), ' '),
|
||||||
'is_live': is_live,
|
'is_live': is_live,
|
||||||
|
'uploader_id': video_data.get('user_id'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -107,6 +108,7 @@ class AWAANLiveIE(AWAANBaseIE):
|
|||||||
'title': 're:Dubai Al Oula [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
|
'title': 're:Dubai Al Oula [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
|
||||||
'upload_date': '20150107',
|
'upload_date': '20150107',
|
||||||
'timestamp': 1420588800,
|
'timestamp': 1420588800,
|
||||||
|
'uploader_id': '71',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
# m3u8 download
|
# m3u8 download
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class AZMedienIE(InfoExtractor):
|
|||||||
'url': 'https://www.telebaern.tv/telebaern-news/montag-1-oktober-2018-ganze-sendung-133531189#video=0_7xjo9lf1',
|
'url': 'https://www.telebaern.tv/telebaern-news/montag-1-oktober-2018-ganze-sendung-133531189#video=0_7xjo9lf1',
|
||||||
'only_matching': True
|
'only_matching': True
|
||||||
}]
|
}]
|
||||||
_API_TEMPL = 'https://www.%s/api/pub/gql/%s/NewsArticleTeaser/cb9f2f81ed22e9b47f4ca64ea3cc5a5d13e88d1d'
|
_API_TEMPL = 'https://www.%s/api/pub/gql/%s/NewsArticleTeaser/a4016f65fe62b81dc6664dd9f4910e4ab40383be'
|
||||||
_PARTNER_ID = '1719221'
|
_PARTNER_ID = '1719221'
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
|||||||
37
youtube_dl/extractor/bandaichannel.py
Normal file
37
youtube_dl/extractor/bandaichannel.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .brightcove import BrightcoveNewIE
|
||||||
|
from ..utils import extract_attributes
|
||||||
|
|
||||||
|
|
||||||
|
class BandaiChannelIE(BrightcoveNewIE):
|
||||||
|
IE_NAME = 'bandaichannel'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?b-ch\.com/titles/(?P<id>\d+/\d+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.b-ch.com/titles/514/001',
|
||||||
|
'md5': 'a0f2d787baa5729bed71108257f613a4',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '6128044564001',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'メタルファイターMIKU 第1話',
|
||||||
|
'timestamp': 1580354056,
|
||||||
|
'uploader_id': '5797077852001',
|
||||||
|
'upload_date': '20200130',
|
||||||
|
'duration': 1387.733,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'format': 'bestvideo',
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
attrs = extract_attributes(self._search_regex(
|
||||||
|
r'(<video-js[^>]+\bid="bcplayer"[^>]*>)', webpage, 'player'))
|
||||||
|
bc = self._download_json(
|
||||||
|
'https://pbifcd.b-ch.com/v1/playbackinfo/ST/70/' + attrs['data-info'],
|
||||||
|
video_id, headers={'X-API-KEY': attrs['data-auth'].strip()})['bc']
|
||||||
|
return self._parse_brightcove_metadata(bc, bc['id'])
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import random
|
import random
|
||||||
@@ -5,10 +6,7 @@ import re
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import (
|
from ..compat import compat_str
|
||||||
compat_str,
|
|
||||||
compat_urlparse,
|
|
||||||
)
|
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
@@ -17,30 +15,32 @@ from ..utils import (
|
|||||||
parse_filesize,
|
parse_filesize,
|
||||||
str_or_none,
|
str_or_none,
|
||||||
try_get,
|
try_get,
|
||||||
unescapeHTML,
|
|
||||||
update_url_query,
|
update_url_query,
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
unified_timestamp,
|
unified_timestamp,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
|
urljoin,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BandcampIE(InfoExtractor):
|
class BandcampIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://[^/]+\.bandcamp\.com/track/(?P<title>[^/?#&]+)'
|
_VALID_URL = r'https?://[^/]+\.bandcamp\.com/track/(?P<id>[^/?#&]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://youtube-dl.bandcamp.com/track/youtube-dl-test-song',
|
'url': 'http://youtube-dl.bandcamp.com/track/youtube-dl-test-song',
|
||||||
'md5': 'c557841d5e50261777a6585648adf439',
|
'md5': 'c557841d5e50261777a6585648adf439',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '1812978515',
|
'id': '1812978515',
|
||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'title': "youtube-dl \"'/\\\u00e4\u21ad - youtube-dl test song \"'/\\\u00e4\u21ad",
|
'title': "youtube-dl \"'/\\ä↭ - youtube-dl \"'/\\ä↭ - youtube-dl test song \"'/\\ä↭",
|
||||||
'duration': 9.8485,
|
'duration': 9.8485,
|
||||||
|
'uploader': 'youtube-dl "\'/\\ä↭',
|
||||||
|
'upload_date': '20121129',
|
||||||
|
'timestamp': 1354224127,
|
||||||
},
|
},
|
||||||
'_skip': 'There is a limit of 200 free downloads / month for the test song'
|
'_skip': 'There is a limit of 200 free downloads / month for the test song'
|
||||||
}, {
|
}, {
|
||||||
# free download
|
# free download
|
||||||
'url': 'http://benprunty.bandcamp.com/track/lanius-battle',
|
'url': 'http://benprunty.bandcamp.com/track/lanius-battle',
|
||||||
'md5': '853e35bf34aa1d6fe2615ae612564b36',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '2650410135',
|
'id': '2650410135',
|
||||||
'ext': 'aiff',
|
'ext': 'aiff',
|
||||||
@@ -49,6 +49,7 @@ class BandcampIE(InfoExtractor):
|
|||||||
'uploader': 'Ben Prunty',
|
'uploader': 'Ben Prunty',
|
||||||
'timestamp': 1396508491,
|
'timestamp': 1396508491,
|
||||||
'upload_date': '20140403',
|
'upload_date': '20140403',
|
||||||
|
'release_timestamp': 1396483200,
|
||||||
'release_date': '20140403',
|
'release_date': '20140403',
|
||||||
'duration': 260.877,
|
'duration': 260.877,
|
||||||
'track': 'Lanius (Battle)',
|
'track': 'Lanius (Battle)',
|
||||||
@@ -69,6 +70,7 @@ class BandcampIE(InfoExtractor):
|
|||||||
'uploader': 'Mastodon',
|
'uploader': 'Mastodon',
|
||||||
'timestamp': 1322005399,
|
'timestamp': 1322005399,
|
||||||
'upload_date': '20111122',
|
'upload_date': '20111122',
|
||||||
|
'release_timestamp': 1076112000,
|
||||||
'release_date': '20040207',
|
'release_date': '20040207',
|
||||||
'duration': 120.79,
|
'duration': 120.79,
|
||||||
'track': 'Hail to Fire',
|
'track': 'Hail to Fire',
|
||||||
@@ -79,11 +81,16 @@ class BandcampIE(InfoExtractor):
|
|||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
def _extract_data_attr(self, webpage, video_id, attr='tralbum', fatal=True):
|
||||||
|
return self._parse_json(self._html_search_regex(
|
||||||
|
r'data-%s=(["\'])({.+?})\1' % attr, webpage,
|
||||||
|
attr + ' data', group=2), video_id, fatal=fatal)
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
title = self._match_id(url)
|
||||||
title = mobj.group('title')
|
|
||||||
webpage = self._download_webpage(url, title)
|
webpage = self._download_webpage(url, title)
|
||||||
thumbnail = self._html_search_meta('og:image', webpage, default=None)
|
tralbum = self._extract_data_attr(webpage, title)
|
||||||
|
thumbnail = self._og_search_thumbnail(webpage)
|
||||||
|
|
||||||
track_id = None
|
track_id = None
|
||||||
track = None
|
track = None
|
||||||
@@ -91,10 +98,7 @@ class BandcampIE(InfoExtractor):
|
|||||||
duration = None
|
duration = None
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
track_info = self._parse_json(
|
track_info = try_get(tralbum, lambda x: x['trackinfo'][0], dict)
|
||||||
self._search_regex(
|
|
||||||
r'trackinfo\s*:\s*\[\s*({.+?})\s*\]\s*,\s*?\n',
|
|
||||||
webpage, 'track info', default='{}'), title)
|
|
||||||
if track_info:
|
if track_info:
|
||||||
file_ = track_info.get('file')
|
file_ = track_info.get('file')
|
||||||
if isinstance(file_, dict):
|
if isinstance(file_, dict):
|
||||||
@@ -111,37 +115,25 @@ class BandcampIE(InfoExtractor):
|
|||||||
'abr': int_or_none(abr_str),
|
'abr': int_or_none(abr_str),
|
||||||
})
|
})
|
||||||
track = track_info.get('title')
|
track = track_info.get('title')
|
||||||
track_id = str_or_none(track_info.get('track_id') or track_info.get('id'))
|
track_id = str_or_none(
|
||||||
|
track_info.get('track_id') or track_info.get('id'))
|
||||||
track_number = int_or_none(track_info.get('track_num'))
|
track_number = int_or_none(track_info.get('track_num'))
|
||||||
duration = float_or_none(track_info.get('duration'))
|
duration = float_or_none(track_info.get('duration'))
|
||||||
|
|
||||||
def extract(key):
|
embed = self._extract_data_attr(webpage, title, 'embed', False)
|
||||||
return self._search_regex(
|
current = tralbum.get('current') or {}
|
||||||
r'\b%s\s*["\']?\s*:\s*(["\'])(?P<value>(?:(?!\1).)+)\1' % key,
|
artist = embed.get('artist') or current.get('artist') or tralbum.get('artist')
|
||||||
webpage, key, default=None, group='value')
|
|
||||||
|
|
||||||
artist = extract('artist')
|
|
||||||
album = extract('album_title')
|
|
||||||
timestamp = unified_timestamp(
|
timestamp = unified_timestamp(
|
||||||
extract('publish_date') or extract('album_publish_date'))
|
current.get('publish_date') or tralbum.get('album_publish_date'))
|
||||||
release_date = unified_strdate(extract('album_release_date'))
|
|
||||||
|
|
||||||
download_link = self._search_regex(
|
download_link = tralbum.get('freeDownloadPage')
|
||||||
r'freeDownloadPage\s*:\s*(["\'])(?P<url>(?:(?!\1).)+)\1', webpage,
|
|
||||||
'download link', default=None, group='url')
|
|
||||||
if download_link:
|
if download_link:
|
||||||
track_id = self._search_regex(
|
track_id = compat_str(tralbum['id'])
|
||||||
r'(?ms)var TralbumData = .*?[{,]\s*id: (?P<id>\d+),?$',
|
|
||||||
webpage, 'track id')
|
|
||||||
|
|
||||||
download_webpage = self._download_webpage(
|
download_webpage = self._download_webpage(
|
||||||
download_link, track_id, 'Downloading free downloads page')
|
download_link, track_id, 'Downloading free downloads page')
|
||||||
|
|
||||||
blob = self._parse_json(
|
blob = self._extract_data_attr(download_webpage, track_id, 'blob')
|
||||||
self._search_regex(
|
|
||||||
r'data-blob=(["\'])(?P<blob>{.+?})\1', download_webpage,
|
|
||||||
'blob', group='blob'),
|
|
||||||
track_id, transform_source=unescapeHTML)
|
|
||||||
|
|
||||||
info = try_get(
|
info = try_get(
|
||||||
blob, (lambda x: x['digital_items'][0],
|
blob, (lambda x: x['digital_items'][0],
|
||||||
@@ -207,20 +199,20 @@ class BandcampIE(InfoExtractor):
|
|||||||
'thumbnail': thumbnail,
|
'thumbnail': thumbnail,
|
||||||
'uploader': artist,
|
'uploader': artist,
|
||||||
'timestamp': timestamp,
|
'timestamp': timestamp,
|
||||||
'release_date': release_date,
|
'release_timestamp': unified_timestamp(tralbum.get('album_release_date')),
|
||||||
'duration': duration,
|
'duration': duration,
|
||||||
'track': track,
|
'track': track,
|
||||||
'track_number': track_number,
|
'track_number': track_number,
|
||||||
'track_id': track_id,
|
'track_id': track_id,
|
||||||
'artist': artist,
|
'artist': artist,
|
||||||
'album': album,
|
'album': embed.get('album_title'),
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class BandcampAlbumIE(InfoExtractor):
|
class BandcampAlbumIE(BandcampIE):
|
||||||
IE_NAME = 'Bandcamp:album'
|
IE_NAME = 'Bandcamp:album'
|
||||||
_VALID_URL = r'https?://(?:(?P<subdomain>[^.]+)\.)?bandcamp\.com(?:/album/(?P<album_id>[^/?#&]+))?'
|
_VALID_URL = r'https?://(?:(?P<subdomain>[^.]+)\.)?bandcamp\.com(?:/album/(?P<id>[^/?#&]+))?'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://blazo.bandcamp.com/album/jazz-format-mixtape-vol-1',
|
'url': 'http://blazo.bandcamp.com/album/jazz-format-mixtape-vol-1',
|
||||||
@@ -230,7 +222,10 @@ class BandcampAlbumIE(InfoExtractor):
|
|||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '1353101989',
|
'id': '1353101989',
|
||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'title': 'Intro',
|
'title': 'Blazo - Intro',
|
||||||
|
'timestamp': 1311756226,
|
||||||
|
'upload_date': '20110727',
|
||||||
|
'uploader': 'Blazo',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -238,7 +233,10 @@ class BandcampAlbumIE(InfoExtractor):
|
|||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '38097443',
|
'id': '38097443',
|
||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'title': 'Kero One - Keep It Alive (Blazo remix)',
|
'title': 'Blazo - Kero One - Keep It Alive (Blazo remix)',
|
||||||
|
'timestamp': 1311757238,
|
||||||
|
'upload_date': '20110727',
|
||||||
|
'uploader': 'Blazo',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -274,6 +272,7 @@ class BandcampAlbumIE(InfoExtractor):
|
|||||||
'title': '"Entropy" EP',
|
'title': '"Entropy" EP',
|
||||||
'uploader_id': 'jstrecords',
|
'uploader_id': 'jstrecords',
|
||||||
'id': 'entropy-ep',
|
'id': 'entropy-ep',
|
||||||
|
'description': 'md5:0ff22959c943622972596062f2f366a5',
|
||||||
},
|
},
|
||||||
'playlist_mincount': 3,
|
'playlist_mincount': 3,
|
||||||
}, {
|
}, {
|
||||||
@@ -283,6 +282,7 @@ class BandcampAlbumIE(InfoExtractor):
|
|||||||
'id': 'we-are-the-plague',
|
'id': 'we-are-the-plague',
|
||||||
'title': 'WE ARE THE PLAGUE',
|
'title': 'WE ARE THE PLAGUE',
|
||||||
'uploader_id': 'insulters',
|
'uploader_id': 'insulters',
|
||||||
|
'description': 'md5:b3cf845ee41b2b1141dc7bde9237255f',
|
||||||
},
|
},
|
||||||
'playlist_count': 2,
|
'playlist_count': 2,
|
||||||
}]
|
}]
|
||||||
@@ -294,41 +294,34 @@ class BandcampAlbumIE(InfoExtractor):
|
|||||||
else super(BandcampAlbumIE, cls).suitable(url))
|
else super(BandcampAlbumIE, cls).suitable(url))
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
uploader_id, album_id = re.match(self._VALID_URL, url).groups()
|
||||||
uploader_id = mobj.group('subdomain')
|
|
||||||
album_id = mobj.group('album_id')
|
|
||||||
playlist_id = album_id or uploader_id
|
playlist_id = album_id or uploader_id
|
||||||
webpage = self._download_webpage(url, playlist_id)
|
webpage = self._download_webpage(url, playlist_id)
|
||||||
track_elements = re.findall(
|
tralbum = self._extract_data_attr(webpage, playlist_id)
|
||||||
r'(?s)<div[^>]*>(.*?<a[^>]+href="([^"]+?)"[^>]+itemprop="url"[^>]*>.*?)</div>', webpage)
|
track_info = tralbum.get('trackinfo')
|
||||||
if not track_elements:
|
if not track_info:
|
||||||
raise ExtractorError('The page doesn\'t contain any tracks')
|
raise ExtractorError('The page doesn\'t contain any tracks')
|
||||||
# Only tracks with duration info have songs
|
# Only tracks with duration info have songs
|
||||||
entries = [
|
entries = [
|
||||||
self.url_result(
|
self.url_result(
|
||||||
compat_urlparse.urljoin(url, t_path),
|
urljoin(url, t['title_link']), BandcampIE.ie_key(),
|
||||||
ie=BandcampIE.ie_key(),
|
str_or_none(t.get('track_id') or t.get('id')), t.get('title'))
|
||||||
video_title=self._search_regex(
|
for t in track_info
|
||||||
r'<span\b[^>]+\bitemprop=["\']name["\'][^>]*>([^<]+)',
|
if t.get('duration')]
|
||||||
elem_content, 'track title', fatal=False))
|
|
||||||
for elem_content, t_path in track_elements
|
current = tralbum.get('current') or {}
|
||||||
if self._html_search_meta('duration', elem_content, default=None)]
|
|
||||||
|
|
||||||
title = self._html_search_regex(
|
|
||||||
r'album_title\s*:\s*"((?:\\.|[^"\\])+?)"',
|
|
||||||
webpage, 'title', fatal=False)
|
|
||||||
if title:
|
|
||||||
title = title.replace(r'\"', '"')
|
|
||||||
return {
|
return {
|
||||||
'_type': 'playlist',
|
'_type': 'playlist',
|
||||||
'uploader_id': uploader_id,
|
'uploader_id': uploader_id,
|
||||||
'id': playlist_id,
|
'id': playlist_id,
|
||||||
'title': title,
|
'title': current.get('title'),
|
||||||
|
'description': current.get('about'),
|
||||||
'entries': entries,
|
'entries': entries,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class BandcampWeeklyIE(InfoExtractor):
|
class BandcampWeeklyIE(BandcampIE):
|
||||||
IE_NAME = 'Bandcamp:weekly'
|
IE_NAME = 'Bandcamp:weekly'
|
||||||
_VALID_URL = r'https?://(?:www\.)?bandcamp\.com/?\?(?:.*?&)?show=(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:www\.)?bandcamp\.com/?\?(?:.*?&)?show=(?P<id>\d+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
@@ -343,29 +336,23 @@ class BandcampWeeklyIE(InfoExtractor):
|
|||||||
'release_date': '20170404',
|
'release_date': '20170404',
|
||||||
'series': 'Bandcamp Weekly',
|
'series': 'Bandcamp Weekly',
|
||||||
'episode': 'Magic Moments',
|
'episode': 'Magic Moments',
|
||||||
'episode_number': 208,
|
|
||||||
'episode_id': '224',
|
'episode_id': '224',
|
||||||
}
|
},
|
||||||
|
'params': {
|
||||||
|
'format': 'opus-lo',
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://bandcamp.com/?blah/blah@&show=228',
|
'url': 'https://bandcamp.com/?blah/blah@&show=228',
|
||||||
'only_matching': True
|
'only_matching': True
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
show_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, show_id)
|
||||||
|
|
||||||
blob = self._parse_json(
|
blob = self._extract_data_attr(webpage, show_id, 'blob')
|
||||||
self._search_regex(
|
|
||||||
r'data-blob=(["\'])(?P<blob>{.+?})\1', webpage,
|
|
||||||
'blob', group='blob'),
|
|
||||||
video_id, transform_source=unescapeHTML)
|
|
||||||
|
|
||||||
show = blob['bcw_show']
|
show = blob['bcw_data'][show_id]
|
||||||
|
|
||||||
# This is desired because any invalid show id redirects to `bandcamp.com`
|
|
||||||
# which happens to expose the latest Bandcamp Weekly episode.
|
|
||||||
show_id = int_or_none(show.get('show_id')) or int_or_none(video_id)
|
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
for format_id, format_url in show['audio_stream'].items():
|
for format_id, format_url in show['audio_stream'].items():
|
||||||
@@ -390,20 +377,8 @@ class BandcampWeeklyIE(InfoExtractor):
|
|||||||
if subtitle:
|
if subtitle:
|
||||||
title += ' - %s' % subtitle
|
title += ' - %s' % subtitle
|
||||||
|
|
||||||
episode_number = None
|
|
||||||
seq = blob.get('bcw_seq')
|
|
||||||
|
|
||||||
if seq and isinstance(seq, list):
|
|
||||||
try:
|
|
||||||
episode_number = next(
|
|
||||||
int_or_none(e.get('episode_number'))
|
|
||||||
for e in seq
|
|
||||||
if isinstance(e, dict) and int_or_none(e.get('id')) == show_id)
|
|
||||||
except StopIteration:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': show_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': show.get('desc') or show.get('short_desc'),
|
'description': show.get('desc') or show.get('short_desc'),
|
||||||
'duration': float_or_none(show.get('audio_duration')),
|
'duration': float_or_none(show.get('audio_duration')),
|
||||||
@@ -411,7 +386,6 @@ class BandcampWeeklyIE(InfoExtractor):
|
|||||||
'release_date': unified_strdate(show.get('published_date')),
|
'release_date': unified_strdate(show.get('published_date')),
|
||||||
'series': 'Bandcamp Weekly',
|
'series': 'Bandcamp Weekly',
|
||||||
'episode': show.get('subtitle'),
|
'episode': show.get('subtitle'),
|
||||||
'episode_number': episode_number,
|
'episode_id': show_id,
|
||||||
'episode_id': compat_str(video_id),
|
|
||||||
'formats': formats
|
'formats': formats
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,46 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import functools
|
||||||
import itertools
|
import itertools
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
from ..compat import (
|
||||||
|
compat_etree_Element,
|
||||||
|
compat_HTTPError,
|
||||||
|
compat_parse_qs,
|
||||||
|
compat_str,
|
||||||
|
compat_urllib_error,
|
||||||
|
compat_urllib_parse_urlparse,
|
||||||
|
compat_urlparse,
|
||||||
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
OnDemandPagedList,
|
||||||
clean_html,
|
clean_html,
|
||||||
dict_get,
|
dict_get,
|
||||||
ExtractorError,
|
|
||||||
float_or_none,
|
float_or_none,
|
||||||
get_element_by_class,
|
get_element_by_class,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
js_to_json,
|
js_to_json,
|
||||||
parse_duration,
|
parse_duration,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
|
strip_or_none,
|
||||||
try_get,
|
try_get,
|
||||||
unescapeHTML,
|
unescapeHTML,
|
||||||
|
unified_timestamp,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
urlencode_postdata,
|
urlencode_postdata,
|
||||||
urljoin,
|
urljoin,
|
||||||
)
|
)
|
||||||
from ..compat import (
|
|
||||||
compat_etree_Element,
|
|
||||||
compat_HTTPError,
|
|
||||||
compat_urlparse,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BBCCoUkIE(InfoExtractor):
|
class BBCCoUkIE(InfoExtractor):
|
||||||
IE_NAME = 'bbc.co.uk'
|
IE_NAME = 'bbc.co.uk'
|
||||||
IE_DESC = 'BBC iPlayer'
|
IE_DESC = 'BBC iPlayer'
|
||||||
_ID_REGEX = r'(?:[pbm][\da-z]{7}|w[\da-z]{7,14})'
|
_ID_REGEX = r'(?:[pbml][\da-z]{7}|w[\da-z]{7,14})'
|
||||||
_VALID_URL = r'''(?x)
|
_VALID_URL = r'''(?x)
|
||||||
https?://
|
https?://
|
||||||
(?:www\.)?bbc\.co\.uk/
|
(?:www\.)?bbc\.co\.uk/
|
||||||
@@ -49,22 +58,17 @@ class BBCCoUkIE(InfoExtractor):
|
|||||||
_LOGIN_URL = 'https://account.bbc.com/signin'
|
_LOGIN_URL = 'https://account.bbc.com/signin'
|
||||||
_NETRC_MACHINE = 'bbc'
|
_NETRC_MACHINE = 'bbc'
|
||||||
|
|
||||||
_MEDIASELECTOR_URLS = [
|
_MEDIA_SELECTOR_URL_TEMPL = 'https://open.live.bbc.co.uk/mediaselector/6/select/version/2.0/mediaset/%s/vpid/%s'
|
||||||
|
_MEDIA_SETS = [
|
||||||
# Provides HQ HLS streams with even better quality that pc mediaset but fails
|
# Provides HQ HLS streams with even better quality that pc mediaset but fails
|
||||||
# with geolocation in some cases when it's even not geo restricted at all (e.g.
|
# with geolocation in some cases when it's even not geo restricted at all (e.g.
|
||||||
# http://www.bbc.co.uk/programmes/b06bp7lf). Also may fail with selectionunavailable.
|
# http://www.bbc.co.uk/programmes/b06bp7lf). Also may fail with selectionunavailable.
|
||||||
'http://open.live.bbc.co.uk/mediaselector/5/select/version/2.0/mediaset/iptv-all/vpid/%s',
|
'iptv-all',
|
||||||
'http://open.live.bbc.co.uk/mediaselector/5/select/version/2.0/mediaset/pc/vpid/%s',
|
'pc',
|
||||||
]
|
]
|
||||||
|
|
||||||
_MEDIASELECTION_NS = 'http://bbc.co.uk/2008/mp/mediaselection'
|
|
||||||
_EMP_PLAYLIST_NS = 'http://bbc.co.uk/2008/emp/playlist'
|
_EMP_PLAYLIST_NS = 'http://bbc.co.uk/2008/emp/playlist'
|
||||||
|
|
||||||
_NAMESPACES = (
|
|
||||||
_MEDIASELECTION_NS,
|
|
||||||
_EMP_PLAYLIST_NS,
|
|
||||||
)
|
|
||||||
|
|
||||||
_TESTS = [
|
_TESTS = [
|
||||||
{
|
{
|
||||||
'url': 'http://www.bbc.co.uk/programmes/b039g8p7',
|
'url': 'http://www.bbc.co.uk/programmes/b039g8p7',
|
||||||
@@ -261,8 +265,6 @@ class BBCCoUkIE(InfoExtractor):
|
|||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
_USP_RE = r'/([^/]+?)\.ism(?:\.hlsv2\.ism)?/[^/]+\.m3u8'
|
|
||||||
|
|
||||||
def _login(self):
|
def _login(self):
|
||||||
username, password = self._get_login_info()
|
username, password = self._get_login_info()
|
||||||
if username is None:
|
if username is None:
|
||||||
@@ -307,22 +309,14 @@ class BBCCoUkIE(InfoExtractor):
|
|||||||
def _extract_items(self, playlist):
|
def _extract_items(self, playlist):
|
||||||
return playlist.findall('./{%s}item' % self._EMP_PLAYLIST_NS)
|
return playlist.findall('./{%s}item' % self._EMP_PLAYLIST_NS)
|
||||||
|
|
||||||
def _findall_ns(self, element, xpath):
|
|
||||||
elements = []
|
|
||||||
for ns in self._NAMESPACES:
|
|
||||||
elements.extend(element.findall(xpath % ns))
|
|
||||||
return elements
|
|
||||||
|
|
||||||
def _extract_medias(self, media_selection):
|
def _extract_medias(self, media_selection):
|
||||||
error = media_selection.find('./{%s}error' % self._MEDIASELECTION_NS)
|
error = media_selection.get('result')
|
||||||
if error is None:
|
if error:
|
||||||
media_selection.find('./{%s}error' % self._EMP_PLAYLIST_NS)
|
raise BBCCoUkIE.MediaSelectionError(error)
|
||||||
if error is not None:
|
return media_selection.get('media') or []
|
||||||
raise BBCCoUkIE.MediaSelectionError(error.get('id'))
|
|
||||||
return self._findall_ns(media_selection, './{%s}media')
|
|
||||||
|
|
||||||
def _extract_connections(self, media):
|
def _extract_connections(self, media):
|
||||||
return self._findall_ns(media, './{%s}connection')
|
return media.get('connection') or []
|
||||||
|
|
||||||
def _get_subtitles(self, media, programme_id):
|
def _get_subtitles(self, media, programme_id):
|
||||||
subtitles = {}
|
subtitles = {}
|
||||||
@@ -334,13 +328,13 @@ class BBCCoUkIE(InfoExtractor):
|
|||||||
cc_url, programme_id, 'Downloading captions', fatal=False)
|
cc_url, programme_id, 'Downloading captions', fatal=False)
|
||||||
if not isinstance(captions, compat_etree_Element):
|
if not isinstance(captions, compat_etree_Element):
|
||||||
continue
|
continue
|
||||||
lang = captions.get('{http://www.w3.org/XML/1998/namespace}lang', 'en')
|
subtitles['en'] = [
|
||||||
subtitles[lang] = [
|
|
||||||
{
|
{
|
||||||
'url': connection.get('href'),
|
'url': connection.get('href'),
|
||||||
'ext': 'ttml',
|
'ext': 'ttml',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
break
|
||||||
return subtitles
|
return subtitles
|
||||||
|
|
||||||
def _raise_extractor_error(self, media_selection_error):
|
def _raise_extractor_error(self, media_selection_error):
|
||||||
@@ -350,10 +344,10 @@ class BBCCoUkIE(InfoExtractor):
|
|||||||
|
|
||||||
def _download_media_selector(self, programme_id):
|
def _download_media_selector(self, programme_id):
|
||||||
last_exception = None
|
last_exception = None
|
||||||
for mediaselector_url in self._MEDIASELECTOR_URLS:
|
for media_set in self._MEDIA_SETS:
|
||||||
try:
|
try:
|
||||||
return self._download_media_selector_url(
|
return self._download_media_selector_url(
|
||||||
mediaselector_url % programme_id, programme_id)
|
self._MEDIA_SELECTOR_URL_TEMPL % (media_set, programme_id), programme_id)
|
||||||
except BBCCoUkIE.MediaSelectionError as e:
|
except BBCCoUkIE.MediaSelectionError as e:
|
||||||
if e.id in ('notukerror', 'geolocation', 'selectionunavailable'):
|
if e.id in ('notukerror', 'geolocation', 'selectionunavailable'):
|
||||||
last_exception = e
|
last_exception = e
|
||||||
@@ -362,8 +356,8 @@ class BBCCoUkIE(InfoExtractor):
|
|||||||
self._raise_extractor_error(last_exception)
|
self._raise_extractor_error(last_exception)
|
||||||
|
|
||||||
def _download_media_selector_url(self, url, programme_id=None):
|
def _download_media_selector_url(self, url, programme_id=None):
|
||||||
media_selection = self._download_xml(
|
media_selection = self._download_json(
|
||||||
url, programme_id, 'Downloading media selection XML',
|
url, programme_id, 'Downloading media selection JSON',
|
||||||
expected_status=(403, 404))
|
expected_status=(403, 404))
|
||||||
return self._process_media_selector(media_selection, programme_id)
|
return self._process_media_selector(media_selection, programme_id)
|
||||||
|
|
||||||
@@ -377,7 +371,6 @@ class BBCCoUkIE(InfoExtractor):
|
|||||||
if kind in ('video', 'audio'):
|
if kind in ('video', 'audio'):
|
||||||
bitrate = int_or_none(media.get('bitrate'))
|
bitrate = int_or_none(media.get('bitrate'))
|
||||||
encoding = media.get('encoding')
|
encoding = media.get('encoding')
|
||||||
service = media.get('service')
|
|
||||||
width = int_or_none(media.get('width'))
|
width = int_or_none(media.get('width'))
|
||||||
height = int_or_none(media.get('height'))
|
height = int_or_none(media.get('height'))
|
||||||
file_size = int_or_none(media.get('media_file_size'))
|
file_size = int_or_none(media.get('media_file_size'))
|
||||||
@@ -392,8 +385,6 @@ class BBCCoUkIE(InfoExtractor):
|
|||||||
supplier = connection.get('supplier')
|
supplier = connection.get('supplier')
|
||||||
transfer_format = connection.get('transferFormat')
|
transfer_format = connection.get('transferFormat')
|
||||||
format_id = supplier or conn_kind or protocol
|
format_id = supplier or conn_kind or protocol
|
||||||
if service:
|
|
||||||
format_id = '%s_%s' % (service, format_id)
|
|
||||||
# ASX playlist
|
# ASX playlist
|
||||||
if supplier == 'asx':
|
if supplier == 'asx':
|
||||||
for i, ref in enumerate(self._extract_asx_playlist(connection, programme_id)):
|
for i, ref in enumerate(self._extract_asx_playlist(connection, programme_id)):
|
||||||
@@ -405,23 +396,22 @@ class BBCCoUkIE(InfoExtractor):
|
|||||||
formats.extend(self._extract_mpd_formats(
|
formats.extend(self._extract_mpd_formats(
|
||||||
href, programme_id, mpd_id=format_id, fatal=False))
|
href, programme_id, mpd_id=format_id, fatal=False))
|
||||||
elif transfer_format == 'hls':
|
elif transfer_format == 'hls':
|
||||||
formats.extend(self._extract_m3u8_formats(
|
# TODO: let expected_status be passed into _extract_xxx_formats() instead
|
||||||
|
try:
|
||||||
|
fmts = self._extract_m3u8_formats(
|
||||||
href, programme_id, ext='mp4', entry_protocol='m3u8_native',
|
href, programme_id, ext='mp4', entry_protocol='m3u8_native',
|
||||||
m3u8_id=format_id, fatal=False))
|
|
||||||
if re.search(self._USP_RE, href):
|
|
||||||
usp_formats = self._extract_m3u8_formats(
|
|
||||||
re.sub(self._USP_RE, r'/\1.ism/\1.m3u8', href),
|
|
||||||
programme_id, ext='mp4', entry_protocol='m3u8_native',
|
|
||||||
m3u8_id=format_id, fatal=False)
|
m3u8_id=format_id, fatal=False)
|
||||||
for f in usp_formats:
|
except ExtractorError as e:
|
||||||
if f.get('height') and f['height'] > 720:
|
if not (isinstance(e.exc_info[1], compat_urllib_error.HTTPError)
|
||||||
continue
|
and e.exc_info[1].code in (403, 404)):
|
||||||
formats.append(f)
|
raise
|
||||||
|
fmts = []
|
||||||
|
formats.extend(fmts)
|
||||||
elif transfer_format == 'hds':
|
elif transfer_format == 'hds':
|
||||||
formats.extend(self._extract_f4m_formats(
|
formats.extend(self._extract_f4m_formats(
|
||||||
href, programme_id, f4m_id=format_id, fatal=False))
|
href, programme_id, f4m_id=format_id, fatal=False))
|
||||||
else:
|
else:
|
||||||
if not service and not supplier and bitrate:
|
if not supplier and bitrate:
|
||||||
format_id += '-%d' % bitrate
|
format_id += '-%d' % bitrate
|
||||||
fmt = {
|
fmt = {
|
||||||
'format_id': format_id,
|
'format_id': format_id,
|
||||||
@@ -528,7 +518,7 @@ class BBCCoUkIE(InfoExtractor):
|
|||||||
|
|
||||||
def get_programme_id(item):
|
def get_programme_id(item):
|
||||||
def get_from_attributes(item):
|
def get_from_attributes(item):
|
||||||
for p in('identifier', 'group'):
|
for p in ('identifier', 'group'):
|
||||||
value = item.get(p)
|
value = item.get(p)
|
||||||
if value and re.match(r'^[pb][\da-z]{7}$', value):
|
if value and re.match(r'^[pb][\da-z]{7}$', value):
|
||||||
return value
|
return value
|
||||||
@@ -554,7 +544,7 @@ class BBCCoUkIE(InfoExtractor):
|
|||||||
webpage = self._download_webpage(url, group_id, 'Downloading video page')
|
webpage = self._download_webpage(url, group_id, 'Downloading video page')
|
||||||
|
|
||||||
error = self._search_regex(
|
error = self._search_regex(
|
||||||
r'<div\b[^>]+\bclass=["\']smp__message delta["\'][^>]*>([^<]+)<',
|
r'<div\b[^>]+\bclass=["\'](?:smp|playout)__message delta["\'][^>]*>\s*([^<]+?)\s*<',
|
||||||
webpage, 'error', default=None)
|
webpage, 'error', default=None)
|
||||||
if error:
|
if error:
|
||||||
raise ExtractorError(error, expected=True)
|
raise ExtractorError(error, expected=True)
|
||||||
@@ -607,16 +597,9 @@ class BBCIE(BBCCoUkIE):
|
|||||||
IE_DESC = 'BBC'
|
IE_DESC = 'BBC'
|
||||||
_VALID_URL = r'https?://(?:www\.)?bbc\.(?:com|co\.uk)/(?:[^/]+/)+(?P<id>[^/#?]+)'
|
_VALID_URL = r'https?://(?:www\.)?bbc\.(?:com|co\.uk)/(?:[^/]+/)+(?P<id>[^/#?]+)'
|
||||||
|
|
||||||
_MEDIASELECTOR_URLS = [
|
_MEDIA_SETS = [
|
||||||
# Provides HQ HLS streams but fails with geolocation in some cases when it's
|
'mobile-tablet-main',
|
||||||
# even not geo restricted at all
|
'pc',
|
||||||
'http://open.live.bbc.co.uk/mediaselector/5/select/version/2.0/mediaset/iptv-all/vpid/%s',
|
|
||||||
# Provides more formats, namely direct mp4 links, but fails on some videos with
|
|
||||||
# notukerror for non UK (?) users (e.g.
|
|
||||||
# http://www.bbc.com/travel/story/20150625-sri-lankas-spicy-secret)
|
|
||||||
'http://open.live.bbc.co.uk/mediaselector/4/mtis/stream/%s',
|
|
||||||
# Provides fewer formats, but works everywhere for everybody (hopefully)
|
|
||||||
'http://open.live.bbc.co.uk/mediaselector/5/select/version/2.0/mediaset/journalism-pc/vpid/%s',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
@@ -790,23 +773,44 @@ class BBCIE(BBCCoUkIE):
|
|||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
# custom redirection to www.bbc.com
|
# custom redirection to www.bbc.com
|
||||||
|
# also, video with window.__INITIAL_DATA__
|
||||||
'url': 'http://www.bbc.co.uk/news/science-environment-33661876',
|
'url': 'http://www.bbc.co.uk/news/science-environment-33661876',
|
||||||
'only_matching': True,
|
'info_dict': {
|
||||||
|
'id': 'p02xzws1',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': "Pluto may have 'nitrogen glaciers'",
|
||||||
|
'description': 'md5:6a95b593f528d7a5f2605221bc56912f',
|
||||||
|
'thumbnail': r're:https?://.+/.+\.jpg',
|
||||||
|
'timestamp': 1437785037,
|
||||||
|
'upload_date': '20150725',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# video with window.__INITIAL_DATA__ and value as JSON string
|
||||||
|
'url': 'https://www.bbc.com/news/av/world-europe-59468682',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'p0b71qth',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Why France is making this woman a national hero',
|
||||||
|
'description': 'md5:7affdfab80e9c3a1f976230a1ff4d5e4',
|
||||||
|
'thumbnail': r're:https?://.+/.+\.jpg',
|
||||||
|
'timestamp': 1638230731,
|
||||||
|
'upload_date': '20211130',
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
# single video article embedded with data-media-vpid
|
# single video article embedded with data-media-vpid
|
||||||
'url': 'http://www.bbc.co.uk/sport/rowing/35908187',
|
'url': 'http://www.bbc.co.uk/sport/rowing/35908187',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
|
# bbcthreeConfig
|
||||||
'url': 'https://www.bbc.co.uk/bbcthree/clip/73d0bbd0-abc3-4cea-b3c0-cdae21905eb1',
|
'url': 'https://www.bbc.co.uk/bbcthree/clip/73d0bbd0-abc3-4cea-b3c0-cdae21905eb1',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'p06556y7',
|
'id': 'p06556y7',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Transfers: Cristiano Ronaldo to Man Utd, Arsenal to spend?',
|
'title': 'Things Not To Say to people that live on council estates',
|
||||||
'description': 'md5:4b7dfd063d5a789a1512e99662be3ddd',
|
'description': "From being labelled a 'chav', to the presumption that they're 'scroungers', people who live on council estates encounter all kinds of prejudices and false assumptions about themselves, their families, and their lifestyles. Here, eight people discuss the common statements, misconceptions, and clichés that they're tired of hearing.",
|
||||||
|
'duration': 360,
|
||||||
|
'thumbnail': r're:https?://.+/.+\.jpg',
|
||||||
},
|
},
|
||||||
'params': {
|
|
||||||
'skip_download': True,
|
|
||||||
}
|
|
||||||
}, {
|
}, {
|
||||||
# window.__PRELOADED_STATE__
|
# window.__PRELOADED_STATE__
|
||||||
'url': 'https://www.bbc.co.uk/radio/play/b0b9z4yl',
|
'url': 'https://www.bbc.co.uk/radio/play/b0b9z4yl',
|
||||||
@@ -827,11 +831,25 @@ class BBCIE(BBCCoUkIE):
|
|||||||
'description': 'Learn English words and phrases from this story',
|
'description': 'Learn English words and phrases from this story',
|
||||||
},
|
},
|
||||||
'add_ie': [BBCCoUkIE.ie_key()],
|
'add_ie': [BBCCoUkIE.ie_key()],
|
||||||
|
}, {
|
||||||
|
# BBC Reel
|
||||||
|
'url': 'https://www.bbc.com/reel/video/p07c6sb6/how-positive-thinking-is-harming-your-happiness',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'p07c6sb9',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'How positive thinking is harming your happiness',
|
||||||
|
'alt_title': 'The downsides of positive thinking',
|
||||||
|
'description': 'md5:fad74b31da60d83b8265954ee42d85b4',
|
||||||
|
'duration': 235,
|
||||||
|
'thumbnail': r're:https?://.+/p07c9dsr.jpg',
|
||||||
|
'upload_date': '20190604',
|
||||||
|
'categories': ['Psychology'],
|
||||||
|
},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def suitable(cls, url):
|
def suitable(cls, url):
|
||||||
EXCLUDE_IE = (BBCCoUkIE, BBCCoUkArticleIE, BBCCoUkIPlayerPlaylistIE, BBCCoUkPlaylistIE)
|
EXCLUDE_IE = (BBCCoUkIE, BBCCoUkArticleIE, BBCCoUkIPlayerEpisodesIE, BBCCoUkIPlayerGroupIE, BBCCoUkPlaylistIE)
|
||||||
return (False if any(ie.suitable(url) for ie in EXCLUDE_IE)
|
return (False if any(ie.suitable(url) for ie in EXCLUDE_IE)
|
||||||
else super(BBCIE, cls).suitable(url))
|
else super(BBCIE, cls).suitable(url))
|
||||||
|
|
||||||
@@ -963,7 +981,7 @@ class BBCIE(BBCCoUkIE):
|
|||||||
else:
|
else:
|
||||||
entry['title'] = info['title']
|
entry['title'] = info['title']
|
||||||
entry['formats'].extend(info['formats'])
|
entry['formats'].extend(info['formats'])
|
||||||
except Exception as e:
|
except ExtractorError as e:
|
||||||
# Some playlist URL may fail with 500, at the same time
|
# Some playlist URL may fail with 500, at the same time
|
||||||
# the other one may work fine (e.g.
|
# the other one may work fine (e.g.
|
||||||
# http://www.bbc.com/turkce/haberler/2015/06/150615_telabyad_kentin_cogu)
|
# http://www.bbc.com/turkce/haberler/2015/06/150615_telabyad_kentin_cogu)
|
||||||
@@ -981,7 +999,7 @@ class BBCIE(BBCCoUkIE):
|
|||||||
group_id = self._search_regex(
|
group_id = self._search_regex(
|
||||||
r'<div[^>]+\bclass=["\']video["\'][^>]+\bdata-pid=["\'](%s)' % self._ID_REGEX,
|
r'<div[^>]+\bclass=["\']video["\'][^>]+\bdata-pid=["\'](%s)' % self._ID_REGEX,
|
||||||
webpage, 'group id', default=None)
|
webpage, 'group id', default=None)
|
||||||
if playlist_id:
|
if group_id:
|
||||||
return self.url_result(
|
return self.url_result(
|
||||||
'https://www.bbc.co.uk/programmes/%s' % group_id,
|
'https://www.bbc.co.uk/programmes/%s' % group_id,
|
||||||
ie=BBCCoUkIE.ie_key())
|
ie=BBCCoUkIE.ie_key())
|
||||||
@@ -1014,6 +1032,37 @@ class BBCIE(BBCCoUkIE):
|
|||||||
'subtitles': subtitles,
|
'subtitles': subtitles,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# bbc reel (e.g. https://www.bbc.com/reel/video/p07c6sb6/how-positive-thinking-is-harming-your-happiness)
|
||||||
|
initial_data = self._parse_json(self._html_search_regex(
|
||||||
|
r'<script[^>]+id=(["\'])initial-data\1[^>]+data-json=(["\'])(?P<json>(?:(?!\2).)+)',
|
||||||
|
webpage, 'initial data', default='{}', group='json'), playlist_id, fatal=False)
|
||||||
|
if initial_data:
|
||||||
|
init_data = try_get(
|
||||||
|
initial_data, lambda x: x['initData']['items'][0], dict) or {}
|
||||||
|
smp_data = init_data.get('smpData') or {}
|
||||||
|
clip_data = try_get(smp_data, lambda x: x['items'][0], dict) or {}
|
||||||
|
version_id = clip_data.get('versionID')
|
||||||
|
if version_id:
|
||||||
|
title = smp_data['title']
|
||||||
|
formats, subtitles = self._download_media_selector(version_id)
|
||||||
|
self._sort_formats(formats)
|
||||||
|
image_url = smp_data.get('holdingImageURL')
|
||||||
|
display_date = init_data.get('displayDate')
|
||||||
|
topic_title = init_data.get('topicTitle')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': version_id,
|
||||||
|
'title': title,
|
||||||
|
'formats': formats,
|
||||||
|
'alt_title': init_data.get('shortTitle'),
|
||||||
|
'thumbnail': image_url.replace('$recipe', 'raw') if image_url else None,
|
||||||
|
'description': smp_data.get('summary') or init_data.get('shortSummary'),
|
||||||
|
'upload_date': display_date.replace('-', '') if display_date else None,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
'duration': int_or_none(clip_data.get('duration')),
|
||||||
|
'categories': [topic_title] if topic_title else None,
|
||||||
|
}
|
||||||
|
|
||||||
# Morph based embed (e.g. http://www.bbc.co.uk/sport/live/olympics/36895975)
|
# Morph based embed (e.g. http://www.bbc.co.uk/sport/live/olympics/36895975)
|
||||||
# There are several setPayload calls may be present but the video
|
# There are several setPayload calls may be present but the video
|
||||||
# seems to be always related to the first one
|
# seems to be always related to the first one
|
||||||
@@ -1075,7 +1124,7 @@ class BBCIE(BBCCoUkIE):
|
|||||||
thumbnail = None
|
thumbnail = None
|
||||||
image_url = current_programme.get('image_url')
|
image_url = current_programme.get('image_url')
|
||||||
if image_url:
|
if image_url:
|
||||||
thumbnail = image_url.replace('{recipe}', '1920x1920')
|
thumbnail = image_url.replace('{recipe}', 'raw')
|
||||||
return {
|
return {
|
||||||
'id': programme_id,
|
'id': programme_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
@@ -1092,10 +1141,26 @@ class BBCIE(BBCCoUkIE):
|
|||||||
self._search_regex(
|
self._search_regex(
|
||||||
r'(?s)bbcthreeConfig\s*=\s*({.+?})\s*;\s*<', webpage,
|
r'(?s)bbcthreeConfig\s*=\s*({.+?})\s*;\s*<', webpage,
|
||||||
'bbcthree config', default='{}'),
|
'bbcthree config', default='{}'),
|
||||||
playlist_id, transform_source=js_to_json, fatal=False)
|
playlist_id, transform_source=js_to_json, fatal=False) or {}
|
||||||
if bbc3_config:
|
payload = bbc3_config.get('payload') or {}
|
||||||
|
if payload:
|
||||||
|
clip = payload.get('currentClip') or {}
|
||||||
|
clip_vpid = clip.get('vpid')
|
||||||
|
clip_title = clip.get('title')
|
||||||
|
if clip_vpid and clip_title:
|
||||||
|
formats, subtitles = self._download_media_selector(clip_vpid)
|
||||||
|
self._sort_formats(formats)
|
||||||
|
return {
|
||||||
|
'id': clip_vpid,
|
||||||
|
'title': clip_title,
|
||||||
|
'thumbnail': dict_get(clip, ('poster', 'imageUrl')),
|
||||||
|
'description': clip.get('description'),
|
||||||
|
'duration': parse_duration(clip.get('duration')),
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
}
|
||||||
bbc3_playlist = try_get(
|
bbc3_playlist = try_get(
|
||||||
bbc3_config, lambda x: x['payload']['content']['bbcMedia']['playlist'],
|
payload, lambda x: x['content']['bbcMedia']['playlist'],
|
||||||
dict)
|
dict)
|
||||||
if bbc3_playlist:
|
if bbc3_playlist:
|
||||||
playlist_title = bbc3_playlist.get('title') or playlist_title
|
playlist_title = bbc3_playlist.get('title') or playlist_title
|
||||||
@@ -1118,6 +1183,66 @@ class BBCIE(BBCCoUkIE):
|
|||||||
return self.playlist_result(
|
return self.playlist_result(
|
||||||
entries, playlist_id, playlist_title, playlist_description)
|
entries, playlist_id, playlist_title, playlist_description)
|
||||||
|
|
||||||
|
initial_data = self._search_regex(
|
||||||
|
r'window\.__INITIAL_DATA__\s*=\s*("{.+?}")\s*;', webpage,
|
||||||
|
'quoted preload state', default=None)
|
||||||
|
if initial_data is None:
|
||||||
|
initial_data = self._search_regex(
|
||||||
|
r'window\.__INITIAL_DATA__\s*=\s*({.+?})\s*;', webpage,
|
||||||
|
'preload state', default={})
|
||||||
|
else:
|
||||||
|
initial_data = self._parse_json(initial_data or '"{}"', playlist_id, fatal=False)
|
||||||
|
initial_data = self._parse_json(initial_data, playlist_id, fatal=False)
|
||||||
|
if initial_data:
|
||||||
|
def parse_media(media):
|
||||||
|
if not media:
|
||||||
|
return
|
||||||
|
for item in (try_get(media, lambda x: x['media']['items'], list) or []):
|
||||||
|
item_id = item.get('id')
|
||||||
|
item_title = item.get('title')
|
||||||
|
if not (item_id and item_title):
|
||||||
|
continue
|
||||||
|
formats, subtitles = self._download_media_selector(item_id)
|
||||||
|
self._sort_formats(formats)
|
||||||
|
item_desc = None
|
||||||
|
blocks = try_get(media, lambda x: x['summary']['blocks'], list)
|
||||||
|
if blocks:
|
||||||
|
summary = []
|
||||||
|
for block in blocks:
|
||||||
|
text = try_get(block, lambda x: x['model']['text'], compat_str)
|
||||||
|
if text:
|
||||||
|
summary.append(text)
|
||||||
|
if summary:
|
||||||
|
item_desc = '\n\n'.join(summary)
|
||||||
|
item_time = None
|
||||||
|
for meta in try_get(media, lambda x: x['metadata']['items'], list) or []:
|
||||||
|
if try_get(meta, lambda x: x['label']) == 'Published':
|
||||||
|
item_time = unified_timestamp(meta.get('timestamp'))
|
||||||
|
break
|
||||||
|
entries.append({
|
||||||
|
'id': item_id,
|
||||||
|
'title': item_title,
|
||||||
|
'thumbnail': item.get('holdingImageUrl'),
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
'timestamp': item_time,
|
||||||
|
'description': strip_or_none(item_desc),
|
||||||
|
})
|
||||||
|
for resp in (initial_data.get('data') or {}).values():
|
||||||
|
name = resp.get('name')
|
||||||
|
if name == 'media-experience':
|
||||||
|
parse_media(try_get(resp, lambda x: x['data']['initialItem']['mediaItem'], dict))
|
||||||
|
elif name == 'article':
|
||||||
|
for block in (try_get(resp,
|
||||||
|
(lambda x: x['data']['blocks'],
|
||||||
|
lambda x: x['data']['content']['model']['blocks'],),
|
||||||
|
list) or []):
|
||||||
|
if block.get('type') != 'media':
|
||||||
|
continue
|
||||||
|
parse_media(block.get('model'))
|
||||||
|
return self.playlist_result(
|
||||||
|
entries, playlist_id, playlist_title, playlist_description)
|
||||||
|
|
||||||
def extract_all(pattern):
|
def extract_all(pattern):
|
||||||
return list(filter(None, map(
|
return list(filter(None, map(
|
||||||
lambda s: self._parse_json(s, playlist_id, fatal=False),
|
lambda s: self._parse_json(s, playlist_id, fatal=False),
|
||||||
@@ -1278,21 +1403,149 @@ class BBCCoUkPlaylistBaseIE(InfoExtractor):
|
|||||||
playlist_id, title, description)
|
playlist_id, title, description)
|
||||||
|
|
||||||
|
|
||||||
class BBCCoUkIPlayerPlaylistIE(BBCCoUkPlaylistBaseIE):
|
class BBCCoUkIPlayerPlaylistBaseIE(InfoExtractor):
|
||||||
IE_NAME = 'bbc.co.uk:iplayer:playlist'
|
_VALID_URL_TMPL = r'https?://(?:www\.)?bbc\.co\.uk/iplayer/%%s/(?P<id>%s)' % BBCCoUkIE._ID_REGEX
|
||||||
_VALID_URL = r'https?://(?:www\.)?bbc\.co\.uk/iplayer/(?:episodes|group)/(?P<id>%s)' % BBCCoUkIE._ID_REGEX
|
|
||||||
_URL_TEMPLATE = 'http://www.bbc.co.uk/iplayer/episode/%s'
|
@staticmethod
|
||||||
_VIDEO_ID_TEMPLATE = r'data-ip-id=["\'](%s)'
|
def _get_default(episode, key, default_key='default'):
|
||||||
|
return try_get(episode, lambda x: x[key][default_key])
|
||||||
|
|
||||||
|
def _get_description(self, data):
|
||||||
|
synopsis = data.get(self._DESCRIPTION_KEY) or {}
|
||||||
|
return dict_get(synopsis, ('large', 'medium', 'small'))
|
||||||
|
|
||||||
|
def _fetch_page(self, programme_id, per_page, series_id, page):
|
||||||
|
elements = self._get_elements(self._call_api(
|
||||||
|
programme_id, per_page, page + 1, series_id))
|
||||||
|
for element in elements:
|
||||||
|
episode = self._get_episode(element)
|
||||||
|
episode_id = episode.get('id')
|
||||||
|
if not episode_id:
|
||||||
|
continue
|
||||||
|
thumbnail = None
|
||||||
|
image = self._get_episode_image(episode)
|
||||||
|
if image:
|
||||||
|
thumbnail = image.replace('{recipe}', 'raw')
|
||||||
|
category = self._get_default(episode, 'labels', 'category')
|
||||||
|
yield {
|
||||||
|
'_type': 'url',
|
||||||
|
'id': episode_id,
|
||||||
|
'title': self._get_episode_field(episode, 'subtitle'),
|
||||||
|
'url': 'https://www.bbc.co.uk/iplayer/episode/' + episode_id,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
|
'description': self._get_description(episode),
|
||||||
|
'categories': [category] if category else None,
|
||||||
|
'series': self._get_episode_field(episode, 'title'),
|
||||||
|
'ie_key': BBCCoUkIE.ie_key(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
pid = self._match_id(url)
|
||||||
|
qs = compat_parse_qs(compat_urllib_parse_urlparse(url).query)
|
||||||
|
series_id = qs.get('seriesId', [None])[0]
|
||||||
|
page = qs.get('page', [None])[0]
|
||||||
|
per_page = 36 if page else self._PAGE_SIZE
|
||||||
|
fetch_page = functools.partial(self._fetch_page, pid, per_page, series_id)
|
||||||
|
entries = fetch_page(int(page) - 1) if page else OnDemandPagedList(fetch_page, self._PAGE_SIZE)
|
||||||
|
playlist_data = self._get_playlist_data(self._call_api(pid, 1))
|
||||||
|
return self.playlist_result(
|
||||||
|
entries, pid, self._get_playlist_title(playlist_data),
|
||||||
|
self._get_description(playlist_data))
|
||||||
|
|
||||||
|
|
||||||
|
class BBCCoUkIPlayerEpisodesIE(BBCCoUkIPlayerPlaylistBaseIE):
|
||||||
|
IE_NAME = 'bbc.co.uk:iplayer:episodes'
|
||||||
|
_VALID_URL = BBCCoUkIPlayerPlaylistBaseIE._VALID_URL_TMPL % 'episodes'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.bbc.co.uk/iplayer/episodes/b05rcz9v',
|
'url': 'http://www.bbc.co.uk/iplayer/episodes/b05rcz9v',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'b05rcz9v',
|
'id': 'b05rcz9v',
|
||||||
'title': 'The Disappearance',
|
'title': 'The Disappearance',
|
||||||
'description': 'French thriller serial about a missing teenager.',
|
'description': 'md5:58eb101aee3116bad4da05f91179c0cb',
|
||||||
},
|
},
|
||||||
'playlist_mincount': 6,
|
'playlist_mincount': 8,
|
||||||
'skip': 'This programme is not currently available on BBC iPlayer',
|
|
||||||
}, {
|
}, {
|
||||||
|
# all seasons
|
||||||
|
'url': 'https://www.bbc.co.uk/iplayer/episodes/b094m5t9/doctor-foster',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'b094m5t9',
|
||||||
|
'title': 'Doctor Foster',
|
||||||
|
'description': 'md5:5aa9195fad900e8e14b52acd765a9fd6',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 10,
|
||||||
|
}, {
|
||||||
|
# explicit season
|
||||||
|
'url': 'https://www.bbc.co.uk/iplayer/episodes/b094m5t9/doctor-foster?seriesId=b094m6nv',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'b094m5t9',
|
||||||
|
'title': 'Doctor Foster',
|
||||||
|
'description': 'md5:5aa9195fad900e8e14b52acd765a9fd6',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 5,
|
||||||
|
}, {
|
||||||
|
# all pages
|
||||||
|
'url': 'https://www.bbc.co.uk/iplayer/episodes/m0004c4v/beechgrove',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'm0004c4v',
|
||||||
|
'title': 'Beechgrove',
|
||||||
|
'description': 'Gardening show that celebrates Scottish horticulture and growing conditions.',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 37,
|
||||||
|
}, {
|
||||||
|
# explicit page
|
||||||
|
'url': 'https://www.bbc.co.uk/iplayer/episodes/m0004c4v/beechgrove?page=2',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'm0004c4v',
|
||||||
|
'title': 'Beechgrove',
|
||||||
|
'description': 'Gardening show that celebrates Scottish horticulture and growing conditions.',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 1,
|
||||||
|
}]
|
||||||
|
_PAGE_SIZE = 100
|
||||||
|
_DESCRIPTION_KEY = 'synopsis'
|
||||||
|
|
||||||
|
def _get_episode_image(self, episode):
|
||||||
|
return self._get_default(episode, 'image')
|
||||||
|
|
||||||
|
def _get_episode_field(self, episode, field):
|
||||||
|
return self._get_default(episode, field)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_elements(data):
|
||||||
|
return data['entities']['results']
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_episode(element):
|
||||||
|
return element.get('episode') or {}
|
||||||
|
|
||||||
|
def _call_api(self, pid, per_page, page=1, series_id=None):
|
||||||
|
variables = {
|
||||||
|
'id': pid,
|
||||||
|
'page': page,
|
||||||
|
'perPage': per_page,
|
||||||
|
}
|
||||||
|
if series_id:
|
||||||
|
variables['sliceId'] = series_id
|
||||||
|
return self._download_json(
|
||||||
|
'https://graph.ibl.api.bbc.co.uk/', pid, headers={
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}, data=json.dumps({
|
||||||
|
'id': '5692d93d5aac8d796a0305e895e61551',
|
||||||
|
'variables': variables,
|
||||||
|
}).encode('utf-8'))['data']['programme']
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_playlist_data(data):
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _get_playlist_title(self, data):
|
||||||
|
return self._get_default(data, 'title')
|
||||||
|
|
||||||
|
|
||||||
|
class BBCCoUkIPlayerGroupIE(BBCCoUkIPlayerPlaylistBaseIE):
|
||||||
|
IE_NAME = 'bbc.co.uk:iplayer:group'
|
||||||
|
_VALID_URL = BBCCoUkIPlayerPlaylistBaseIE._VALID_URL_TMPL % 'group'
|
||||||
|
_TESTS = [{
|
||||||
# Available for over a year unlike 30 days for most other programmes
|
# Available for over a year unlike 30 days for most other programmes
|
||||||
'url': 'http://www.bbc.co.uk/iplayer/group/p02tcc32',
|
'url': 'http://www.bbc.co.uk/iplayer/group/p02tcc32',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -1301,14 +1554,56 @@ class BBCCoUkIPlayerPlaylistIE(BBCCoUkPlaylistBaseIE):
|
|||||||
'description': 'md5:683e901041b2fe9ba596f2ab04c4dbe7',
|
'description': 'md5:683e901041b2fe9ba596f2ab04c4dbe7',
|
||||||
},
|
},
|
||||||
'playlist_mincount': 10,
|
'playlist_mincount': 10,
|
||||||
|
}, {
|
||||||
|
# all pages
|
||||||
|
'url': 'https://www.bbc.co.uk/iplayer/group/p081d7j7',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'p081d7j7',
|
||||||
|
'title': 'Music in Scotland',
|
||||||
|
'description': 'Perfomances in Scotland and programmes featuring Scottish acts.',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 47,
|
||||||
|
}, {
|
||||||
|
# explicit page
|
||||||
|
'url': 'https://www.bbc.co.uk/iplayer/group/p081d7j7?page=2',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'p081d7j7',
|
||||||
|
'title': 'Music in Scotland',
|
||||||
|
'description': 'Perfomances in Scotland and programmes featuring Scottish acts.',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 11,
|
||||||
}]
|
}]
|
||||||
|
_PAGE_SIZE = 200
|
||||||
|
_DESCRIPTION_KEY = 'synopses'
|
||||||
|
|
||||||
def _extract_title_and_description(self, webpage):
|
def _get_episode_image(self, episode):
|
||||||
title = self._search_regex(r'<h1>([^<]+)</h1>', webpage, 'title', fatal=False)
|
return self._get_default(episode, 'images', 'standard')
|
||||||
description = self._search_regex(
|
|
||||||
r'<p[^>]+class=(["\'])subtitle\1[^>]*>(?P<value>[^<]+)</p>',
|
def _get_episode_field(self, episode, field):
|
||||||
webpage, 'description', fatal=False, group='value')
|
return episode.get(field)
|
||||||
return title, description
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_elements(data):
|
||||||
|
return data['elements']
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_episode(element):
|
||||||
|
return element
|
||||||
|
|
||||||
|
def _call_api(self, pid, per_page, page=1, series_id=None):
|
||||||
|
return self._download_json(
|
||||||
|
'http://ibl.api.bbc.co.uk/ibl/v1/groups/%s/episodes' % pid,
|
||||||
|
pid, query={
|
||||||
|
'page': page,
|
||||||
|
'per_page': per_page,
|
||||||
|
})['group_episodes']
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_playlist_data(data):
|
||||||
|
return data['group']
|
||||||
|
|
||||||
|
def _get_playlist_title(self, data):
|
||||||
|
return data.get('title')
|
||||||
|
|
||||||
|
|
||||||
class BBCCoUkPlaylistIE(BBCCoUkPlaylistBaseIE):
|
class BBCCoUkPlaylistIE(BBCCoUkPlaylistBaseIE):
|
||||||
|
|||||||
@@ -1,194 +0,0 @@
|
|||||||
# coding: utf-8
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
|
||||||
from ..utils import (
|
|
||||||
ExtractorError,
|
|
||||||
clean_html,
|
|
||||||
compat_str,
|
|
||||||
float_or_none,
|
|
||||||
int_or_none,
|
|
||||||
parse_iso8601,
|
|
||||||
try_get,
|
|
||||||
urljoin,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BeamProBaseIE(InfoExtractor):
|
|
||||||
_API_BASE = 'https://mixer.com/api/v1'
|
|
||||||
_RATINGS = {'family': 0, 'teen': 13, '18+': 18}
|
|
||||||
|
|
||||||
def _extract_channel_info(self, chan):
|
|
||||||
user_id = chan.get('userId') or try_get(chan, lambda x: x['user']['id'])
|
|
||||||
return {
|
|
||||||
'uploader': chan.get('token') or try_get(
|
|
||||||
chan, lambda x: x['user']['username'], compat_str),
|
|
||||||
'uploader_id': compat_str(user_id) if user_id else None,
|
|
||||||
'age_limit': self._RATINGS.get(chan.get('audience')),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class BeamProLiveIE(BeamProBaseIE):
|
|
||||||
IE_NAME = 'Mixer:live'
|
|
||||||
_VALID_URL = r'https?://(?:\w+\.)?(?:beam\.pro|mixer\.com)/(?P<id>[^/?#&]+)'
|
|
||||||
_TEST = {
|
|
||||||
'url': 'http://mixer.com/niterhayven',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '261562',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Introducing The Witcher 3 // The Grind Starts Now!',
|
|
||||||
'description': 'md5:0b161ac080f15fe05d18a07adb44a74d',
|
|
||||||
'thumbnail': r're:https://.*\.jpg$',
|
|
||||||
'timestamp': 1483477281,
|
|
||||||
'upload_date': '20170103',
|
|
||||||
'uploader': 'niterhayven',
|
|
||||||
'uploader_id': '373396',
|
|
||||||
'age_limit': 18,
|
|
||||||
'is_live': True,
|
|
||||||
'view_count': int,
|
|
||||||
},
|
|
||||||
'skip': 'niterhayven is offline',
|
|
||||||
'params': {
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
_MANIFEST_URL_TEMPLATE = '%s/channels/%%s/manifest.%%s' % BeamProBaseIE._API_BASE
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def suitable(cls, url):
|
|
||||||
return False if BeamProVodIE.suitable(url) else super(BeamProLiveIE, cls).suitable(url)
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
channel_name = self._match_id(url)
|
|
||||||
|
|
||||||
chan = self._download_json(
|
|
||||||
'%s/channels/%s' % (self._API_BASE, channel_name), channel_name)
|
|
||||||
|
|
||||||
if chan.get('online') is False:
|
|
||||||
raise ExtractorError(
|
|
||||||
'{0} is offline'.format(channel_name), expected=True)
|
|
||||||
|
|
||||||
channel_id = chan['id']
|
|
||||||
|
|
||||||
def manifest_url(kind):
|
|
||||||
return self._MANIFEST_URL_TEMPLATE % (channel_id, kind)
|
|
||||||
|
|
||||||
formats = self._extract_m3u8_formats(
|
|
||||||
manifest_url('m3u8'), channel_name, ext='mp4', m3u8_id='hls',
|
|
||||||
fatal=False)
|
|
||||||
formats.extend(self._extract_smil_formats(
|
|
||||||
manifest_url('smil'), channel_name, fatal=False))
|
|
||||||
self._sort_formats(formats)
|
|
||||||
|
|
||||||
info = {
|
|
||||||
'id': compat_str(chan.get('id') or channel_name),
|
|
||||||
'title': self._live_title(chan.get('name') or channel_name),
|
|
||||||
'description': clean_html(chan.get('description')),
|
|
||||||
'thumbnail': try_get(
|
|
||||||
chan, lambda x: x['thumbnail']['url'], compat_str),
|
|
||||||
'timestamp': parse_iso8601(chan.get('updatedAt')),
|
|
||||||
'is_live': True,
|
|
||||||
'view_count': int_or_none(chan.get('viewersTotal')),
|
|
||||||
'formats': formats,
|
|
||||||
}
|
|
||||||
info.update(self._extract_channel_info(chan))
|
|
||||||
|
|
||||||
return info
|
|
||||||
|
|
||||||
|
|
||||||
class BeamProVodIE(BeamProBaseIE):
|
|
||||||
IE_NAME = 'Mixer:vod'
|
|
||||||
_VALID_URL = r'https?://(?:\w+\.)?(?:beam\.pro|mixer\.com)/[^/?#&]+\?.*?\bvod=(?P<id>[^?#&]+)'
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://mixer.com/willow8714?vod=2259830',
|
|
||||||
'md5': 'b2431e6e8347dc92ebafb565d368b76b',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '2259830',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'willow8714\'s Channel',
|
|
||||||
'duration': 6828.15,
|
|
||||||
'thumbnail': r're:https://.*source\.png$',
|
|
||||||
'timestamp': 1494046474,
|
|
||||||
'upload_date': '20170506',
|
|
||||||
'uploader': 'willow8714',
|
|
||||||
'uploader_id': '6085379',
|
|
||||||
'age_limit': 13,
|
|
||||||
'view_count': int,
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://mixer.com/streamer?vod=IxFno1rqC0S_XJ1a2yGgNw',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://mixer.com/streamer?vod=Rh3LY0VAqkGpEQUe2pN-ig',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _extract_format(vod, vod_type):
|
|
||||||
if not vod.get('baseUrl'):
|
|
||||||
return []
|
|
||||||
|
|
||||||
if vod_type == 'hls':
|
|
||||||
filename, protocol = 'manifest.m3u8', 'm3u8_native'
|
|
||||||
elif vod_type == 'raw':
|
|
||||||
filename, protocol = 'source.mp4', 'https'
|
|
||||||
else:
|
|
||||||
assert False
|
|
||||||
|
|
||||||
data = vod.get('data') if isinstance(vod.get('data'), dict) else {}
|
|
||||||
|
|
||||||
format_id = [vod_type]
|
|
||||||
if isinstance(data.get('Height'), compat_str):
|
|
||||||
format_id.append('%sp' % data['Height'])
|
|
||||||
|
|
||||||
return [{
|
|
||||||
'url': urljoin(vod['baseUrl'], filename),
|
|
||||||
'format_id': '-'.join(format_id),
|
|
||||||
'ext': 'mp4',
|
|
||||||
'protocol': protocol,
|
|
||||||
'width': int_or_none(data.get('Width')),
|
|
||||||
'height': int_or_none(data.get('Height')),
|
|
||||||
'fps': int_or_none(data.get('Fps')),
|
|
||||||
'tbr': int_or_none(data.get('Bitrate'), 1000),
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
vod_id = self._match_id(url)
|
|
||||||
|
|
||||||
vod_info = self._download_json(
|
|
||||||
'%s/recordings/%s' % (self._API_BASE, vod_id), vod_id)
|
|
||||||
|
|
||||||
state = vod_info.get('state')
|
|
||||||
if state != 'AVAILABLE':
|
|
||||||
raise ExtractorError(
|
|
||||||
'VOD %s is not available (state: %s)' % (vod_id, state),
|
|
||||||
expected=True)
|
|
||||||
|
|
||||||
formats = []
|
|
||||||
thumbnail_url = None
|
|
||||||
|
|
||||||
for vod in vod_info['vods']:
|
|
||||||
vod_type = vod.get('format')
|
|
||||||
if vod_type in ('hls', 'raw'):
|
|
||||||
formats.extend(self._extract_format(vod, vod_type))
|
|
||||||
elif vod_type == 'thumbnail':
|
|
||||||
thumbnail_url = urljoin(vod.get('baseUrl'), 'source.png')
|
|
||||||
|
|
||||||
self._sort_formats(formats)
|
|
||||||
|
|
||||||
info = {
|
|
||||||
'id': vod_id,
|
|
||||||
'title': vod_info.get('name') or vod_id,
|
|
||||||
'duration': float_or_none(vod_info.get('duration')),
|
|
||||||
'thumbnail': thumbnail_url,
|
|
||||||
'timestamp': parse_iso8601(vod_info.get('createdAt')),
|
|
||||||
'view_count': int_or_none(vod_info.get('viewsTotal')),
|
|
||||||
'formats': formats,
|
|
||||||
}
|
|
||||||
info.update(self._extract_channel_info(vod_info.get('channel') or {}))
|
|
||||||
|
|
||||||
return info
|
|
||||||
@@ -25,8 +25,8 @@ class BellMediaIE(InfoExtractor):
|
|||||||
etalk|
|
etalk|
|
||||||
marilyn
|
marilyn
|
||||||
)\.ca|
|
)\.ca|
|
||||||
much\.com
|
(?:much|cp24)\.com
|
||||||
)/.*?(?:\bvid(?:eoid)?=|-vid|~|%7E|/(?:episode)?)(?P<id>[0-9]{6,})'''
|
)/.*?(?:\b(?:vid(?:eoid)?|clipId)=|-vid|~|%7E|/(?:episode)?)(?P<id>[0-9]{6,})'''
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.bnnbloomberg.ca/video/david-cockfield-s-top-picks~1403070',
|
'url': 'https://www.bnnbloomberg.ca/video/david-cockfield-s-top-picks~1403070',
|
||||||
'md5': '36d3ef559cfe8af8efe15922cd3ce950',
|
'md5': '36d3ef559cfe8af8efe15922cd3ce950',
|
||||||
@@ -62,6 +62,9 @@ class BellMediaIE(InfoExtractor):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'http://www.etalk.ca/video?videoid=663455',
|
'url': 'http://www.etalk.ca/video?videoid=663455',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.cp24.com/video?clipId=1982548',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
_DOMAINS = {
|
_DOMAINS = {
|
||||||
'thecomedynetwork': 'comedy',
|
'thecomedynetwork': 'comedy',
|
||||||
|
|||||||
103
youtube_dl/extractor/bfmtv.py
Normal file
103
youtube_dl/extractor/bfmtv.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import extract_attributes
|
||||||
|
|
||||||
|
|
||||||
|
class BFMTVBaseIE(InfoExtractor):
|
||||||
|
_VALID_URL_BASE = r'https?://(?:www\.)?bfmtv\.com/'
|
||||||
|
_VALID_URL_TMPL = _VALID_URL_BASE + r'(?:[^/]+/)*[^/?&#]+_%s[A-Z]-(?P<id>\d{12})\.html'
|
||||||
|
_VIDEO_BLOCK_REGEX = r'(<div[^>]+class="video_block"[^>]*>)'
|
||||||
|
BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/%s/%s_default/index.html?videoId=%s'
|
||||||
|
|
||||||
|
def _brightcove_url_result(self, video_id, video_block):
|
||||||
|
account_id = video_block.get('accountid') or '876450612001'
|
||||||
|
player_id = video_block.get('playerid') or 'I2qBTln4u'
|
||||||
|
return self.url_result(
|
||||||
|
self.BRIGHTCOVE_URL_TEMPLATE % (account_id, player_id, video_id),
|
||||||
|
'BrightcoveNew', video_id)
|
||||||
|
|
||||||
|
|
||||||
|
class BFMTVIE(BFMTVBaseIE):
|
||||||
|
IE_NAME = 'bfmtv'
|
||||||
|
_VALID_URL = BFMTVBaseIE._VALID_URL_TMPL % 'V'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.bfmtv.com/politique/emmanuel-macron-l-islam-est-une-religion-qui-vit-une-crise-aujourd-hui-partout-dans-le-monde_VN-202010020146.html',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '6196747868001',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Emmanuel Macron: "L\'Islam est une religion qui vit une crise aujourd’hui, partout dans le monde"',
|
||||||
|
'description': 'Le Président s\'exprime sur la question du séparatisme depuis les Mureaux, dans les Yvelines.',
|
||||||
|
'uploader_id': '876450610001',
|
||||||
|
'upload_date': '20201002',
|
||||||
|
'timestamp': 1601629620,
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
bfmtv_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, bfmtv_id)
|
||||||
|
video_block = extract_attributes(self._search_regex(
|
||||||
|
self._VIDEO_BLOCK_REGEX, webpage, 'video block'))
|
||||||
|
return self._brightcove_url_result(video_block['videoid'], video_block)
|
||||||
|
|
||||||
|
|
||||||
|
class BFMTVLiveIE(BFMTVIE):
|
||||||
|
IE_NAME = 'bfmtv:live'
|
||||||
|
_VALID_URL = BFMTVBaseIE._VALID_URL_BASE + '(?P<id>(?:[^/]+/)?en-direct)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.bfmtv.com/en-direct/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '5615950982001',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': r're:^le direct BFMTV WEB \d{4}-\d{2}-\d{2} \d{2}:\d{2}$',
|
||||||
|
'uploader_id': '876450610001',
|
||||||
|
'upload_date': '20171018',
|
||||||
|
'timestamp': 1508329950,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.bfmtv.com/economie/en-direct/',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
class BFMTVArticleIE(BFMTVBaseIE):
|
||||||
|
IE_NAME = 'bfmtv:article'
|
||||||
|
_VALID_URL = BFMTVBaseIE._VALID_URL_TMPL % 'A'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.bfmtv.com/sante/covid-19-un-responsable-de-l-institut-pasteur-se-demande-quand-la-france-va-se-reconfiner_AV-202101060198.html',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '202101060198',
|
||||||
|
'title': 'Covid-19: un responsable de l\'Institut Pasteur se demande "quand la France va se reconfiner"',
|
||||||
|
'description': 'md5:947974089c303d3ac6196670ae262843',
|
||||||
|
},
|
||||||
|
'playlist_count': 2,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.bfmtv.com/international/pour-bolsonaro-le-bresil-est-en-faillite-mais-il-ne-peut-rien-faire_AD-202101060232.html',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.bfmtv.com/sante/covid-19-oui-le-vaccin-de-pfizer-distribue-en-france-a-bien-ete-teste-sur-des-personnes-agees_AN-202101060275.html',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
bfmtv_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, bfmtv_id)
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
for video_block_el in re.findall(self._VIDEO_BLOCK_REGEX, webpage):
|
||||||
|
video_block = extract_attributes(video_block_el)
|
||||||
|
video_id = video_block.get('videoid')
|
||||||
|
if not video_id:
|
||||||
|
continue
|
||||||
|
entries.append(self._brightcove_url_result(video_id, video_block))
|
||||||
|
|
||||||
|
return self.playlist_result(
|
||||||
|
entries, bfmtv_id, self._og_search_title(webpage, fatal=False),
|
||||||
|
self._html_search_meta(['og:description', 'description'], webpage))
|
||||||
30
youtube_dl/extractor/bibeltv.py
Normal file
30
youtube_dl/extractor/bibeltv.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
|
||||||
|
|
||||||
|
class BibelTVIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?bibeltv\.de/mediathek/videos/(?:crn/)?(?P<id>\d+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.bibeltv.de/mediathek/videos/329703-sprachkurs-in-malaiisch',
|
||||||
|
'md5': '252f908192d611de038b8504b08bf97f',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'ref:329703',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Sprachkurs in Malaiisch',
|
||||||
|
'description': 'md5:3e9f197d29ee164714e67351cf737dfe',
|
||||||
|
'timestamp': 1608316701,
|
||||||
|
'uploader_id': '5840105145001',
|
||||||
|
'upload_date': '20201218',
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.bibeltv.de/mediathek/videos/crn/326374',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/5840105145001/default_default/index.html?videoId=ref:%s'
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
crn_id = self._match_id(url)
|
||||||
|
return self.url_result(
|
||||||
|
self.BRIGHTCOVE_URL_TEMPLATE % crn_id, 'BrightcoveNew')
|
||||||
59
youtube_dl/extractor/bigo.py
Normal file
59
youtube_dl/extractor/bigo.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import ExtractorError, urlencode_postdata
|
||||||
|
|
||||||
|
|
||||||
|
class BigoIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?bigo\.tv/(?:[a-z]{2,}/)?(?P<id>[^/]+)'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.bigo.tv/ja/221338632',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '6576287577575737440',
|
||||||
|
'title': '土よ〜💁♂️ 休憩室/REST room',
|
||||||
|
'thumbnail': r're:https?://.+',
|
||||||
|
'uploader': '✨Shin💫',
|
||||||
|
'uploader_id': '221338632',
|
||||||
|
'is_live': True,
|
||||||
|
},
|
||||||
|
'skip': 'livestream',
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.bigo.tv/th/Tarlerm1304',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://bigo.tv/115976881',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
user_id = self._match_id(url)
|
||||||
|
|
||||||
|
info_raw = self._download_json(
|
||||||
|
'https://bigo.tv/studio/getInternalStudioInfo',
|
||||||
|
user_id, data=urlencode_postdata({'siteId': user_id}))
|
||||||
|
|
||||||
|
if not isinstance(info_raw, dict):
|
||||||
|
raise ExtractorError('Received invalid JSON data')
|
||||||
|
if info_raw.get('code'):
|
||||||
|
raise ExtractorError(
|
||||||
|
'Bigo says: %s (code %s)' % (info_raw.get('msg'), info_raw.get('code')), expected=True)
|
||||||
|
info = info_raw.get('data') or {}
|
||||||
|
|
||||||
|
if not info.get('alive'):
|
||||||
|
raise ExtractorError('This user is offline.', expected=True)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': info.get('roomId') or user_id,
|
||||||
|
'title': info.get('roomTopic') or info.get('nick_name') or user_id,
|
||||||
|
'formats': [{
|
||||||
|
'url': info.get('hls_src'),
|
||||||
|
'ext': 'mp4',
|
||||||
|
'protocol': 'm3u8',
|
||||||
|
}],
|
||||||
|
'thumbnail': info.get('snapshot'),
|
||||||
|
'uploader': info.get('nick_name'),
|
||||||
|
'uploader_id': user_id,
|
||||||
|
'is_live': True,
|
||||||
|
}
|
||||||
@@ -24,7 +24,18 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class BiliBiliIE(InfoExtractor):
|
class BiliBiliIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.|bangumi\.|)bilibili\.(?:tv|com)/(?:video/av|anime/(?P<anime_id>\d+)/play#)(?P<id>\d+)'
|
_VALID_URL = r'''(?x)
|
||||||
|
https?://
|
||||||
|
(?:(?:www|bangumi)\.)?
|
||||||
|
bilibili\.(?:tv|com)/
|
||||||
|
(?:
|
||||||
|
(?:
|
||||||
|
video/[aA][vV]|
|
||||||
|
anime/(?P<anime_id>\d+)/play\#
|
||||||
|
)(?P<id_bv>\d+)|
|
||||||
|
video/[bB][vV](?P<id>[^/?#&]+)
|
||||||
|
)
|
||||||
|
'''
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.bilibili.tv/video/av1074402/',
|
'url': 'http://www.bilibili.tv/video/av1074402/',
|
||||||
@@ -92,6 +103,10 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
'skip_download': True, # Test metadata only
|
'skip_download': True, # Test metadata only
|
||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
|
}, {
|
||||||
|
# new BV video id format
|
||||||
|
'url': 'https://www.bilibili.com/video/BV1JE411F741',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
_APP_KEY = 'iVGUTjsxvpLeuDCf'
|
_APP_KEY = 'iVGUTjsxvpLeuDCf'
|
||||||
@@ -109,7 +124,7 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
url, smuggled_data = unsmuggle_url(url, {})
|
url, smuggled_data = unsmuggle_url(url, {})
|
||||||
|
|
||||||
mobj = re.match(self._VALID_URL, url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
video_id = mobj.group('id')
|
video_id = mobj.group('id') or mobj.group('id_bv')
|
||||||
anime_id = mobj.group('anime_id')
|
anime_id = mobj.group('anime_id')
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
@@ -141,6 +156,7 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
cid = js['result']['cid']
|
cid = js['result']['cid']
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
|
'Accept': 'application/json',
|
||||||
'Referer': url
|
'Referer': url
|
||||||
}
|
}
|
||||||
headers.update(self.geo_verification_headers())
|
headers.update(self.geo_verification_headers())
|
||||||
@@ -217,7 +233,7 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
webpage)
|
webpage)
|
||||||
if uploader_mobj:
|
if uploader_mobj:
|
||||||
info.update({
|
info.update({
|
||||||
'uploader': uploader_mobj.group('name'),
|
'uploader': uploader_mobj.group('name').strip(),
|
||||||
'uploader_id': uploader_mobj.group('id'),
|
'uploader_id': uploader_mobj.group('id'),
|
||||||
})
|
})
|
||||||
if not info.get('uploader'):
|
if not info.get('uploader'):
|
||||||
@@ -353,6 +369,11 @@ class BilibiliAudioIE(BilibiliAudioBaseIE):
|
|||||||
'filesize': int_or_none(play_data.get('size')),
|
'filesize': int_or_none(play_data.get('size')),
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
for a_format in formats:
|
||||||
|
a_format.setdefault('http_headers', {}).update({
|
||||||
|
'Referer': url,
|
||||||
|
})
|
||||||
|
|
||||||
song = self._call_api('song/info', au_id)
|
song = self._call_api('song/info', au_id)
|
||||||
title = song['title']
|
title = song['title']
|
||||||
statistic = song.get('statistic') or {}
|
statistic = song.get('statistic') or {}
|
||||||
@@ -419,3 +440,17 @@ class BilibiliAudioAlbumIE(BilibiliAudioBaseIE):
|
|||||||
entries, am_id, album_title, album_data.get('intro'))
|
entries, am_id, album_title, album_data.get('intro'))
|
||||||
|
|
||||||
return self.playlist_result(entries, am_id)
|
return self.playlist_result(entries, am_id)
|
||||||
|
|
||||||
|
|
||||||
|
class BiliBiliPlayerIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://player\.bilibili\.com/player\.html\?.*?\baid=(?P<id>\d+)'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'http://player.bilibili.com/player.html?aid=92494333&cid=157926707&page=1',
|
||||||
|
'only_matching': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
return self.url_result(
|
||||||
|
'http://www.bilibili.tv/video/av%s/' % video_id,
|
||||||
|
ie=BiliBiliIE.ie_key(), video_id=video_id)
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from .vk import VKIE
|
from .vk import VKIE
|
||||||
from ..utils import (
|
from ..compat import (
|
||||||
HEADRequest,
|
compat_b64decode,
|
||||||
int_or_none,
|
compat_urllib_parse_unquote,
|
||||||
)
|
)
|
||||||
|
from ..utils import int_or_none
|
||||||
|
|
||||||
|
|
||||||
class BIQLEIE(InfoExtractor):
|
class BIQLEIE(InfoExtractor):
|
||||||
@@ -47,9 +48,16 @@ class BIQLEIE(InfoExtractor):
|
|||||||
if VKIE.suitable(embed_url):
|
if VKIE.suitable(embed_url):
|
||||||
return self.url_result(embed_url, VKIE.ie_key(), video_id)
|
return self.url_result(embed_url, VKIE.ie_key(), video_id)
|
||||||
|
|
||||||
self._request_webpage(
|
embed_page = self._download_webpage(
|
||||||
HEADRequest(embed_url), video_id, headers={'Referer': url})
|
embed_url, video_id, headers={'Referer': url})
|
||||||
video_id, sig, _, access_token = self._get_cookies(embed_url)['video_ext'].value.split('%3A')
|
video_ext = self._get_cookies(embed_url).get('video_ext')
|
||||||
|
if video_ext:
|
||||||
|
video_ext = compat_urllib_parse_unquote(video_ext.value)
|
||||||
|
if not video_ext:
|
||||||
|
video_ext = compat_b64decode(self._search_regex(
|
||||||
|
r'video_ext\s*:\s*[\'"]([A-Za-z0-9+/=]+)',
|
||||||
|
embed_page, 'video_ext')).decode()
|
||||||
|
video_id, sig, _, access_token = video_ext.split(':')
|
||||||
item = self._download_json(
|
item = self._download_json(
|
||||||
'https://api.vk.com/method/video.get', video_id,
|
'https://api.vk.com/method/video.get', video_id,
|
||||||
headers={'User-Agent': 'okhttp/3.4.1'}, query={
|
headers={'User-Agent': 'okhttp/3.4.1'}, query={
|
||||||
|
|||||||
@@ -90,13 +90,19 @@ class BleacherReportCMSIE(AMPIE):
|
|||||||
_VALID_URL = r'https?://(?:www\.)?bleacherreport\.com/video_embed\?id=(?P<id>[0-9a-f-]{36}|\d{5})'
|
_VALID_URL = r'https?://(?:www\.)?bleacherreport\.com/video_embed\?id=(?P<id>[0-9a-f-]{36}|\d{5})'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://bleacherreport.com/video_embed?id=8fd44c2f-3dc5-4821-9118-2c825a98c0e1&library=video-cms',
|
'url': 'http://bleacherreport.com/video_embed?id=8fd44c2f-3dc5-4821-9118-2c825a98c0e1&library=video-cms',
|
||||||
'md5': '2e4b0a997f9228ffa31fada5c53d1ed1',
|
'md5': '670b2d73f48549da032861130488c681',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '8fd44c2f-3dc5-4821-9118-2c825a98c0e1',
|
'id': '8fd44c2f-3dc5-4821-9118-2c825a98c0e1',
|
||||||
'ext': 'flv',
|
'ext': 'mp4',
|
||||||
'title': 'Cena vs. Rollins Would Expose the Heavyweight Division',
|
'title': 'Cena vs. Rollins Would Expose the Heavyweight Division',
|
||||||
'description': 'md5:984afb4ade2f9c0db35f3267ed88b36e',
|
'description': 'md5:984afb4ade2f9c0db35f3267ed88b36e',
|
||||||
|
'upload_date': '20150723',
|
||||||
|
'timestamp': 1437679032,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
'expected_warnings': [
|
||||||
|
'Unable to download f4m manifest'
|
||||||
|
]
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
|
||||||
from ..utils import (
|
|
||||||
remove_start,
|
|
||||||
int_or_none,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BlinkxIE(InfoExtractor):
|
|
||||||
_VALID_URL = r'(?:https?://(?:www\.)blinkx\.com/#?ce/|blinkx:)(?P<id>[^?]+)'
|
|
||||||
IE_NAME = 'blinkx'
|
|
||||||
|
|
||||||
_TEST = {
|
|
||||||
'url': 'http://www.blinkx.com/ce/Da0Gw3xc5ucpNduzLuDDlv4WC9PuI4fDi1-t6Y3LyfdY2SZS5Urbvn-UPJvrvbo8LTKTc67Wu2rPKSQDJyZeeORCR8bYkhs8lI7eqddznH2ofh5WEEdjYXnoRtj7ByQwt7atMErmXIeYKPsSDuMAAqJDlQZ-3Ff4HJVeH_s3Gh8oQ',
|
|
||||||
'md5': '337cf7a344663ec79bf93a526a2e06c7',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'Da0Gw3xc',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'No Daily Show for John Oliver; HBO Show Renewed - IGN News',
|
|
||||||
'uploader': 'IGN News',
|
|
||||||
'upload_date': '20150217',
|
|
||||||
'timestamp': 1424215740,
|
|
||||||
'description': 'HBO has renewed Last Week Tonight With John Oliver for two more seasons.',
|
|
||||||
'duration': 47.743333,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
video_id = self._match_id(url)
|
|
||||||
display_id = video_id[:8]
|
|
||||||
|
|
||||||
api_url = ('https://apib4.blinkx.com/api.php?action=play_video&'
|
|
||||||
+ 'video=%s' % video_id)
|
|
||||||
data_json = self._download_webpage(api_url, display_id)
|
|
||||||
data = json.loads(data_json)['api']['results'][0]
|
|
||||||
duration = None
|
|
||||||
thumbnails = []
|
|
||||||
formats = []
|
|
||||||
for m in data['media']:
|
|
||||||
if m['type'] == 'jpg':
|
|
||||||
thumbnails.append({
|
|
||||||
'url': m['link'],
|
|
||||||
'width': int(m['w']),
|
|
||||||
'height': int(m['h']),
|
|
||||||
})
|
|
||||||
elif m['type'] == 'original':
|
|
||||||
duration = float(m['d'])
|
|
||||||
elif m['type'] == 'youtube':
|
|
||||||
yt_id = m['link']
|
|
||||||
self.to_screen('Youtube video detected: %s' % yt_id)
|
|
||||||
return self.url_result(yt_id, 'Youtube', video_id=yt_id)
|
|
||||||
elif m['type'] in ('flv', 'mp4'):
|
|
||||||
vcodec = remove_start(m['vcodec'], 'ff')
|
|
||||||
acodec = remove_start(m['acodec'], 'ff')
|
|
||||||
vbr = int_or_none(m.get('vbr') or m.get('vbitrate'), 1000)
|
|
||||||
abr = int_or_none(m.get('abr') or m.get('abitrate'), 1000)
|
|
||||||
tbr = vbr + abr if vbr and abr else None
|
|
||||||
format_id = '%s-%sk-%s' % (vcodec, tbr, m['w'])
|
|
||||||
formats.append({
|
|
||||||
'format_id': format_id,
|
|
||||||
'url': m['link'],
|
|
||||||
'vcodec': vcodec,
|
|
||||||
'acodec': acodec,
|
|
||||||
'abr': abr,
|
|
||||||
'vbr': vbr,
|
|
||||||
'tbr': tbr,
|
|
||||||
'width': int_or_none(m.get('w')),
|
|
||||||
'height': int_or_none(m.get('h')),
|
|
||||||
})
|
|
||||||
|
|
||||||
self._sort_formats(formats)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': display_id,
|
|
||||||
'fullid': video_id,
|
|
||||||
'title': data['title'],
|
|
||||||
'formats': formats,
|
|
||||||
'uploader': data['channel_name'],
|
|
||||||
'timestamp': data['pubdate_epoch'],
|
|
||||||
'description': data.get('description'),
|
|
||||||
'thumbnails': thumbnails,
|
|
||||||
'duration': duration,
|
|
||||||
}
|
|
||||||
76
youtube_dl/extractor/bongacams.py
Normal file
76
youtube_dl/extractor/bongacams.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..compat import compat_str
|
||||||
|
from ..utils import (
|
||||||
|
int_or_none,
|
||||||
|
try_get,
|
||||||
|
urlencode_postdata,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BongaCamsIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?P<host>(?:[^/]+\.)?bongacams\d*\.(?:com|net))/(?P<id>[^/?&#]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://de.bongacams.com/azumi-8',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://cn.bongacams.com/azumi-8',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://de.bongacams.net/claireashton',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'claireashton',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': r're:ClaireAshton \d{4}-\d{2}-\d{2} \d{2}:\d{2}',
|
||||||
|
'age_limit': 18,
|
||||||
|
'uploader_id': 'ClaireAshton',
|
||||||
|
'uploader': 'ClaireAshton',
|
||||||
|
'like_count': int,
|
||||||
|
'is_live': True,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
mobj = re.match(self._VALID_URL, url)
|
||||||
|
host = mobj.group('host')
|
||||||
|
channel_id = mobj.group('id')
|
||||||
|
|
||||||
|
amf = self._download_json(
|
||||||
|
'https://%s/tools/amf.php' % host, channel_id,
|
||||||
|
data=urlencode_postdata((
|
||||||
|
('method', 'getRoomData'),
|
||||||
|
('args[]', channel_id),
|
||||||
|
('args[]', 'false'),
|
||||||
|
)), headers={'X-Requested-With': 'XMLHttpRequest'})
|
||||||
|
|
||||||
|
server_url = amf['localData']['videoServerUrl']
|
||||||
|
|
||||||
|
uploader_id = try_get(
|
||||||
|
amf, lambda x: x['performerData']['username'], compat_str) or channel_id
|
||||||
|
uploader = try_get(
|
||||||
|
amf, lambda x: x['performerData']['displayName'], compat_str)
|
||||||
|
like_count = int_or_none(try_get(
|
||||||
|
amf, lambda x: x['performerData']['loversCount']))
|
||||||
|
|
||||||
|
formats = self._extract_m3u8_formats(
|
||||||
|
'%s/hls/stream_%s/playlist.m3u8' % (server_url, uploader_id),
|
||||||
|
channel_id, 'mp4', m3u8_id='hls', live=True)
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': channel_id,
|
||||||
|
'title': self._live_title(uploader or uploader_id),
|
||||||
|
'uploader': uploader,
|
||||||
|
'uploader_id': uploader_id,
|
||||||
|
'like_count': like_count,
|
||||||
|
'age_limit': 18,
|
||||||
|
'is_live': True,
|
||||||
|
'formats': formats,
|
||||||
|
}
|
||||||
98
youtube_dl/extractor/box.py
Normal file
98
youtube_dl/extractor/box.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
determine_ext,
|
||||||
|
parse_iso8601,
|
||||||
|
# try_get,
|
||||||
|
update_url_query,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BoxIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:[^.]+\.)?app\.box\.com/s/(?P<shared_name>[^/]+)/file/(?P<id>\d+)'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'https://mlssoccer.app.box.com/s/0evd2o3e08l60lr4ygukepvnkord1o1x/file/510727257538',
|
||||||
|
'md5': '1f81b2fd3960f38a40a3b8823e5fcd43',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '510727257538',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Garber St. Louis will be 28th MLS team +scarving.mp4',
|
||||||
|
'uploader': 'MLS Video',
|
||||||
|
'timestamp': 1566320259,
|
||||||
|
'upload_date': '20190820',
|
||||||
|
'uploader_id': '235196876',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
shared_name, file_id = re.match(self._VALID_URL, url).groups()
|
||||||
|
webpage = self._download_webpage(url, file_id)
|
||||||
|
request_token = self._parse_json(self._search_regex(
|
||||||
|
r'Box\.config\s*=\s*({.+?});', webpage,
|
||||||
|
'Box config'), file_id)['requestToken']
|
||||||
|
access_token = self._download_json(
|
||||||
|
'https://app.box.com/app-api/enduserapp/elements/tokens', file_id,
|
||||||
|
'Downloading token JSON metadata',
|
||||||
|
data=json.dumps({'fileIDs': [file_id]}).encode(), headers={
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Request-Token': request_token,
|
||||||
|
'X-Box-EndUser-API': 'sharedName=' + shared_name,
|
||||||
|
})[file_id]['read']
|
||||||
|
shared_link = 'https://app.box.com/s/' + shared_name
|
||||||
|
f = self._download_json(
|
||||||
|
'https://api.box.com/2.0/files/' + file_id, file_id,
|
||||||
|
'Downloading file JSON metadata', headers={
|
||||||
|
'Authorization': 'Bearer ' + access_token,
|
||||||
|
'BoxApi': 'shared_link=' + shared_link,
|
||||||
|
'X-Rep-Hints': '[dash]', # TODO: extract `hls` formats
|
||||||
|
}, query={
|
||||||
|
'fields': 'authenticated_download_url,created_at,created_by,description,extension,is_download_available,name,representations,size'
|
||||||
|
})
|
||||||
|
title = f['name']
|
||||||
|
|
||||||
|
query = {
|
||||||
|
'access_token': access_token,
|
||||||
|
'shared_link': shared_link
|
||||||
|
}
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
|
||||||
|
# for entry in (try_get(f, lambda x: x['representations']['entries'], list) or []):
|
||||||
|
# entry_url_template = try_get(
|
||||||
|
# entry, lambda x: x['content']['url_template'])
|
||||||
|
# if not entry_url_template:
|
||||||
|
# continue
|
||||||
|
# representation = entry.get('representation')
|
||||||
|
# if representation == 'dash':
|
||||||
|
# TODO: append query to every fragment URL
|
||||||
|
# formats.extend(self._extract_mpd_formats(
|
||||||
|
# entry_url_template.replace('{+asset_path}', 'manifest.mpd'),
|
||||||
|
# file_id, query=query))
|
||||||
|
|
||||||
|
authenticated_download_url = f.get('authenticated_download_url')
|
||||||
|
if authenticated_download_url and f.get('is_download_available'):
|
||||||
|
formats.append({
|
||||||
|
'ext': f.get('extension') or determine_ext(title),
|
||||||
|
'filesize': f.get('size'),
|
||||||
|
'format_id': 'download',
|
||||||
|
'url': update_url_query(authenticated_download_url, query),
|
||||||
|
})
|
||||||
|
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
creator = f.get('created_by') or {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': file_id,
|
||||||
|
'title': title,
|
||||||
|
'formats': formats,
|
||||||
|
'description': f.get('description') or None,
|
||||||
|
'uploader': creator.get('name'),
|
||||||
|
'timestamp': parse_iso8601(f.get('created_at')),
|
||||||
|
'uploader_id': creator.get('id'),
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class BravoTVIE(AdobePassIE):
|
class BravoTVIE(AdobePassIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?bravotv\.com/(?:[^/]+/)+(?P<id>[^/?#]+)'
|
_VALID_URL = r'https?://(?:www\.)?(?P<req_id>bravotv|oxygen)\.com/(?:[^/]+/)+(?P<id>[^/?#]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.bravotv.com/top-chef/season-16/episode-15/videos/the-top-chef-season-16-winner-is',
|
'url': 'https://www.bravotv.com/top-chef/season-16/episode-15/videos/the-top-chef-season-16-winner-is',
|
||||||
'md5': 'e34684cfea2a96cd2ee1ef3a60909de9',
|
'md5': 'e34684cfea2a96cd2ee1ef3a60909de9',
|
||||||
@@ -28,10 +28,13 @@ class BravoTVIE(AdobePassIE):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'http://www.bravotv.com/below-deck/season-3/ep-14-reunion-part-1',
|
'url': 'http://www.bravotv.com/below-deck/season-3/ep-14-reunion-part-1',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.oxygen.com/in-ice-cold-blood/season-2/episode-16/videos/handling-the-horwitz-house-after-the-murder-season-2',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id = self._match_id(url)
|
site, display_id = re.match(self._VALID_URL, url).groups()
|
||||||
webpage = self._download_webpage(url, display_id)
|
webpage = self._download_webpage(url, display_id)
|
||||||
settings = self._parse_json(self._search_regex(
|
settings = self._parse_json(self._search_regex(
|
||||||
r'<script[^>]+data-drupal-selector="drupal-settings-json"[^>]*>({.+?})</script>', webpage, 'drupal settings'),
|
r'<script[^>]+data-drupal-selector="drupal-settings-json"[^>]*>({.+?})</script>', webpage, 'drupal settings'),
|
||||||
@@ -53,11 +56,14 @@ class BravoTVIE(AdobePassIE):
|
|||||||
tp_path = release_pid = tve['release_pid']
|
tp_path = release_pid = tve['release_pid']
|
||||||
if tve.get('entitlement') == 'auth':
|
if tve.get('entitlement') == 'auth':
|
||||||
adobe_pass = settings.get('tve_adobe_auth', {})
|
adobe_pass = settings.get('tve_adobe_auth', {})
|
||||||
|
if site == 'bravotv':
|
||||||
|
site = 'bravo'
|
||||||
resource = self._get_mvpd_resource(
|
resource = self._get_mvpd_resource(
|
||||||
adobe_pass.get('adobePassResourceId', 'bravo'),
|
adobe_pass.get('adobePassResourceId') or site,
|
||||||
tve['title'], release_pid, tve.get('rating'))
|
tve['title'], release_pid, tve.get('rating'))
|
||||||
query['auth'] = self._extract_mvpd_auth(
|
query['auth'] = self._extract_mvpd_auth(
|
||||||
url, release_pid, adobe_pass.get('adobePassRequestorId', 'bravo'), resource)
|
url, release_pid,
|
||||||
|
adobe_pass.get('adobePassRequestorId') or site, resource)
|
||||||
else:
|
else:
|
||||||
shared_playlist = settings['ls_playlist']
|
shared_playlist = settings['ls_playlist']
|
||||||
account_pid = shared_playlist['account_pid']
|
account_pid = shared_playlist['account_pid']
|
||||||
|
|||||||
@@ -5,32 +5,35 @@ import base64
|
|||||||
import re
|
import re
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
from .common import InfoExtractor
|
|
||||||
from .adobepass import AdobePassIE
|
from .adobepass import AdobePassIE
|
||||||
|
from .common import InfoExtractor
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
compat_etree_fromstring,
|
compat_etree_fromstring,
|
||||||
|
compat_HTTPError,
|
||||||
compat_parse_qs,
|
compat_parse_qs,
|
||||||
compat_urllib_parse_urlparse,
|
compat_urllib_parse_urlparse,
|
||||||
compat_urlparse,
|
compat_urlparse,
|
||||||
compat_xml_parse_error,
|
compat_xml_parse_error,
|
||||||
compat_HTTPError,
|
|
||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
clean_html,
|
||||||
extract_attributes,
|
extract_attributes,
|
||||||
|
ExtractorError,
|
||||||
find_xpath_attr,
|
find_xpath_attr,
|
||||||
fix_xml_ampersands,
|
fix_xml_ampersands,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
js_to_json,
|
|
||||||
int_or_none,
|
int_or_none,
|
||||||
|
js_to_json,
|
||||||
|
mimetype2ext,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
smuggle_url,
|
smuggle_url,
|
||||||
|
str_or_none,
|
||||||
|
try_get,
|
||||||
unescapeHTML,
|
unescapeHTML,
|
||||||
unsmuggle_url,
|
unsmuggle_url,
|
||||||
update_url_query,
|
|
||||||
clean_html,
|
|
||||||
mimetype2ext,
|
|
||||||
UnsupportedError,
|
UnsupportedError,
|
||||||
|
update_url_query,
|
||||||
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -145,7 +148,7 @@ class BrightcoveLegacyIE(InfoExtractor):
|
|||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _build_brighcove_url(cls, object_str):
|
def _build_brightcove_url(cls, object_str):
|
||||||
"""
|
"""
|
||||||
Build a Brightcove url from a xml string containing
|
Build a Brightcove url from a xml string containing
|
||||||
<object class="BrightcoveExperience">{params}</object>
|
<object class="BrightcoveExperience">{params}</object>
|
||||||
@@ -215,7 +218,7 @@ class BrightcoveLegacyIE(InfoExtractor):
|
|||||||
return cls._make_brightcove_url(params)
|
return cls._make_brightcove_url(params)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _build_brighcove_url_from_js(cls, object_js):
|
def _build_brightcove_url_from_js(cls, object_js):
|
||||||
# The layout of JS is as follows:
|
# The layout of JS is as follows:
|
||||||
# customBC.createVideo = function (width, height, playerID, playerKey, videoPlayer, VideoRandomID) {
|
# customBC.createVideo = function (width, height, playerID, playerKey, videoPlayer, VideoRandomID) {
|
||||||
# // build Brightcove <object /> XML
|
# // build Brightcove <object /> XML
|
||||||
@@ -270,12 +273,12 @@ class BrightcoveLegacyIE(InfoExtractor):
|
|||||||
).+?>\s*</object>''',
|
).+?>\s*</object>''',
|
||||||
webpage)
|
webpage)
|
||||||
if matches:
|
if matches:
|
||||||
return list(filter(None, [cls._build_brighcove_url(m) for m in matches]))
|
return list(filter(None, [cls._build_brightcove_url(m) for m in matches]))
|
||||||
|
|
||||||
matches = re.findall(r'(customBC\.createVideo\(.+?\);)', webpage)
|
matches = re.findall(r'(customBC\.createVideo\(.+?\);)', webpage)
|
||||||
if matches:
|
if matches:
|
||||||
return list(filter(None, [
|
return list(filter(None, [
|
||||||
cls._build_brighcove_url_from_js(custom_bc)
|
cls._build_brightcove_url_from_js(custom_bc)
|
||||||
for custom_bc in matches]))
|
for custom_bc in matches]))
|
||||||
return [src for _, src in re.findall(
|
return [src for _, src in re.findall(
|
||||||
r'<iframe[^>]+src=([\'"])((?:https?:)?//link\.brightcove\.com/services/player/(?!\1).+)\1', webpage)]
|
r'<iframe[^>]+src=([\'"])((?:https?:)?//link\.brightcove\.com/services/player/(?!\1).+)\1', webpage)]
|
||||||
@@ -424,7 +427,7 @@ class BrightcoveNewIE(AdobePassIE):
|
|||||||
# [2] looks like:
|
# [2] looks like:
|
||||||
for video, script_tag, account_id, player_id, embed in re.findall(
|
for video, script_tag, account_id, player_id, embed in re.findall(
|
||||||
r'''(?isx)
|
r'''(?isx)
|
||||||
(<video\s+[^>]*\bdata-video-id\s*=\s*['"]?[^>]+>)
|
(<video(?:-js)?\s+[^>]*\bdata-video-id\s*=\s*['"]?[^>]+>)
|
||||||
(?:.*?
|
(?:.*?
|
||||||
(<script[^>]+
|
(<script[^>]+
|
||||||
src=["\'](?:https?:)?//players\.brightcove\.net/
|
src=["\'](?:https?:)?//players\.brightcove\.net/
|
||||||
@@ -468,13 +471,18 @@ 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 = []
|
formats = []
|
||||||
for source in json_data.get('sources', []):
|
sources = json_data.get('sources') or []
|
||||||
|
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')
|
||||||
# https://support.brightcove.com/playback-api-video-fields-reference#key_systems_object
|
# https://support.brightcove.com/playback-api-video-fields-reference#key_systems_object
|
||||||
if ext == 'ism' or container == 'WVM' or source.get('key_systems'):
|
if container == 'WVM' or source.get('key_systems'):
|
||||||
|
num_drm_sources += 1
|
||||||
|
continue
|
||||||
|
elif ext == 'ism':
|
||||||
continue
|
continue
|
||||||
elif ext == 'm3u8' or container == 'M2TS':
|
elif ext == 'm3u8' or container == 'M2TS':
|
||||||
if not src:
|
if not src:
|
||||||
@@ -531,20 +539,15 @@ class BrightcoveNewIE(AdobePassIE):
|
|||||||
'format_id': build_format_id('rtmp'),
|
'format_id': build_format_id('rtmp'),
|
||||||
})
|
})
|
||||||
formats.append(f)
|
formats.append(f)
|
||||||
if not formats:
|
|
||||||
# for sonyliv.com DRM protected videos
|
|
||||||
s3_source_url = json_data.get('custom_fields', {}).get('s3sourceurl')
|
|
||||||
if s3_source_url:
|
|
||||||
formats.append({
|
|
||||||
'url': s3_source_url,
|
|
||||||
'format_id': 'source',
|
|
||||||
})
|
|
||||||
|
|
||||||
|
if not formats:
|
||||||
errors = json_data.get('errors')
|
errors = json_data.get('errors')
|
||||||
if not formats and errors:
|
if errors:
|
||||||
error = errors[0]
|
error = errors[0]
|
||||||
raise ExtractorError(
|
raise ExtractorError(
|
||||||
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)
|
||||||
|
if sources and num_drm_sources == len(sources):
|
||||||
|
raise ExtractorError('This video is DRM protected.', expected=True)
|
||||||
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
@@ -553,9 +556,15 @@ class BrightcoveNewIE(AdobePassIE):
|
|||||||
|
|
||||||
subtitles = {}
|
subtitles = {}
|
||||||
for text_track in json_data.get('text_tracks', []):
|
for text_track in json_data.get('text_tracks', []):
|
||||||
if text_track.get('src'):
|
if text_track.get('kind') != 'captions':
|
||||||
subtitles.setdefault(text_track.get('srclang'), []).append({
|
continue
|
||||||
'url': text_track['src'],
|
text_track_url = url_or_none(text_track.get('src'))
|
||||||
|
if not text_track_url:
|
||||||
|
continue
|
||||||
|
lang = (str_or_none(text_track.get('srclang'))
|
||||||
|
or str_or_none(text_track.get('label')) or 'en').lower()
|
||||||
|
subtitles.setdefault(lang, []).append({
|
||||||
|
'url': text_track_url,
|
||||||
})
|
})
|
||||||
|
|
||||||
is_live = False
|
is_live = False
|
||||||
@@ -592,11 +601,14 @@ class BrightcoveNewIE(AdobePassIE):
|
|||||||
store_pk = lambda x: self._downloader.cache.store('brightcove', policy_key_id, x)
|
store_pk = lambda x: self._downloader.cache.store('brightcove', policy_key_id, x)
|
||||||
|
|
||||||
def extract_policy_key():
|
def extract_policy_key():
|
||||||
|
base_url = 'http://players.brightcove.net/%s/%s_%s/' % (account_id, player_id, embed)
|
||||||
|
config = self._download_json(
|
||||||
|
base_url + 'config.json', video_id, fatal=False) or {}
|
||||||
|
policy_key = try_get(
|
||||||
|
config, lambda x: x['video_cloud']['policy_key'])
|
||||||
|
if not policy_key:
|
||||||
webpage = self._download_webpage(
|
webpage = self._download_webpage(
|
||||||
'http://players.brightcove.net/%s/%s_%s/index.min.js'
|
base_url + 'index.min.js', video_id)
|
||||||
% (account_id, player_id, embed), video_id)
|
|
||||||
|
|
||||||
policy_key = None
|
|
||||||
|
|
||||||
catalog = self._search_regex(
|
catalog = self._search_regex(
|
||||||
r'catalog\(({.+?})\);', webpage, 'catalog', default=None)
|
r'catalog\(({.+?})\);', webpage, 'catalog', default=None)
|
||||||
|
|||||||
@@ -8,18 +8,20 @@ from .gigya import GigyaBaseIE
|
|||||||
from ..compat import compat_HTTPError
|
from ..compat import compat_HTTPError
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
strip_or_none,
|
clean_html,
|
||||||
|
extract_attributes,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
|
get_element_by_class,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
merge_dicts,
|
merge_dicts,
|
||||||
parse_iso8601,
|
|
||||||
str_or_none,
|
str_or_none,
|
||||||
|
strip_or_none,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CanvasIE(InfoExtractor):
|
class CanvasIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://mediazone\.vrt\.be/api/v1/(?P<site_id>canvas|een|ketnet|vrt(?:video|nieuws)|sporza)/assets/(?P<id>[^/?#&]+)'
|
_VALID_URL = r'https?://mediazone\.vrt\.be/api/v1/(?P<site_id>canvas|een|ketnet|vrt(?:video|nieuws)|sporza|dako)/assets/(?P<id>[^/?#&]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://mediazone.vrt.be/api/v1/ketnet/assets/md-ast-4ac54990-ce66-4d00-a8ca-9eac86f4c475',
|
'url': 'https://mediazone.vrt.be/api/v1/ketnet/assets/md-ast-4ac54990-ce66-4d00-a8ca-9eac86f4c475',
|
||||||
'md5': '68993eda72ef62386a15ea2cf3c93107',
|
'md5': '68993eda72ef62386a15ea2cf3c93107',
|
||||||
@@ -37,6 +39,7 @@ class CanvasIE(InfoExtractor):
|
|||||||
'url': 'https://mediazone.vrt.be/api/v1/canvas/assets/mz-ast-5e5f90b6-2d72-4c40-82c2-e134f884e93e',
|
'url': 'https://mediazone.vrt.be/api/v1/canvas/assets/mz-ast-5e5f90b6-2d72-4c40-82c2-e134f884e93e',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
_GEO_BYPASS = False
|
||||||
_HLS_ENTRY_PROTOCOLS_MAP = {
|
_HLS_ENTRY_PROTOCOLS_MAP = {
|
||||||
'HLS': 'm3u8_native',
|
'HLS': 'm3u8_native',
|
||||||
'HLS_AES': 'm3u8',
|
'HLS_AES': 'm3u8',
|
||||||
@@ -47,6 +50,8 @@ class CanvasIE(InfoExtractor):
|
|||||||
mobj = re.match(self._VALID_URL, url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
site_id, video_id = mobj.group('site_id'), mobj.group('id')
|
site_id, video_id = mobj.group('site_id'), mobj.group('id')
|
||||||
|
|
||||||
|
data = None
|
||||||
|
if site_id != 'vrtvideo':
|
||||||
# Old API endpoint, serves more formats but may fail for some videos
|
# Old API endpoint, serves more formats but may fail for some videos
|
||||||
data = self._download_json(
|
data = self._download_json(
|
||||||
'https://mediazone.vrt.be/api/v1/%s/assets/%s'
|
'https://mediazone.vrt.be/api/v1/%s/assets/%s'
|
||||||
@@ -55,21 +60,24 @@ class CanvasIE(InfoExtractor):
|
|||||||
|
|
||||||
# New API endpoint
|
# New API endpoint
|
||||||
if not data:
|
if not data:
|
||||||
|
headers = self.geo_verification_headers()
|
||||||
|
headers.update({'Content-Type': 'application/json'})
|
||||||
token = self._download_json(
|
token = self._download_json(
|
||||||
'%s/tokens' % self._REST_API_BASE, video_id,
|
'%s/tokens' % self._REST_API_BASE, video_id,
|
||||||
'Downloading token', data=b'',
|
'Downloading token', data=b'', headers=headers)['vrtPlayerToken']
|
||||||
headers={'Content-Type': 'application/json'})['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', fatal=False, query={
|
video_id, 'Downloading video JSON', query={
|
||||||
'vrtPlayerToken': token,
|
'vrtPlayerToken': token,
|
||||||
'client': '%s@PROD' % site_id,
|
'client': '%s@PROD' % site_id,
|
||||||
}, expected_status=400)
|
}, expected_status=400)
|
||||||
message = data.get('message')
|
if not data.get('title'):
|
||||||
if message and not data.get('title'):
|
code = data.get('code')
|
||||||
if data.get('code') == 'AUTHENTICATION_REQUIRED':
|
if code == 'AUTHENTICATION_REQUIRED':
|
||||||
self.raise_login_required(message)
|
self.raise_login_required()
|
||||||
raise ExtractorError(message, expected=True)
|
elif code == 'INVALID_LOCATION':
|
||||||
|
self.raise_geo_restricted(countries=['BE'])
|
||||||
|
raise ExtractorError(data.get('message') or code, expected=True)
|
||||||
|
|
||||||
title = data['title']
|
title = data['title']
|
||||||
description = data.get('description')
|
description = data.get('description')
|
||||||
@@ -205,20 +213,24 @@ class CanvasEenIE(InfoExtractor):
|
|||||||
|
|
||||||
class VrtNUIE(GigyaBaseIE):
|
class VrtNUIE(GigyaBaseIE):
|
||||||
IE_DESC = 'VrtNU.be'
|
IE_DESC = 'VrtNU.be'
|
||||||
_VALID_URL = r'https?://(?:www\.)?vrt\.be/(?P<site_id>vrtnu)/(?:[^/]+/)*(?P<id>[^/?#&]+)'
|
_VALID_URL = r'https?://(?:www\.)?vrt\.be/vrtnu/a-z/(?:[^/]+/){2}(?P<id>[^/?#&]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
# Available via old API endpoint
|
# Available via old API endpoint
|
||||||
'url': 'https://www.vrt.be/vrtnu/a-z/postbus-x/1/postbus-x-s1a1/',
|
'url': 'https://www.vrt.be/vrtnu/a-z/postbus-x/1989/postbus-x-s1989a1/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'pbs-pub-2e2d8c27-df26-45c9-9dc6-90c78153044d$vid-90c932b1-e21d-4fb8-99b1-db7b49cf74de',
|
'id': 'pbs-pub-e8713dac-899e-41de-9313-81269f4c04ac$vid-90c932b1-e21d-4fb8-99b1-db7b49cf74de',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'De zwarte weduwe',
|
'title': 'Postbus X - Aflevering 1 (Seizoen 1989)',
|
||||||
'description': 'md5:db1227b0f318c849ba5eab1fef895ee4',
|
'description': 'md5:b704f669eb9262da4c55b33d7c6ed4b7',
|
||||||
'duration': 1457.04,
|
'duration': 1457.04,
|
||||||
'thumbnail': r're:^https?://.*\.jpg$',
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
'season': 'Season 1',
|
'series': 'Postbus X',
|
||||||
'season_number': 1,
|
'season': 'Seizoen 1989',
|
||||||
|
'season_number': 1989,
|
||||||
|
'episode': 'De zwarte weduwe',
|
||||||
'episode_number': 1,
|
'episode_number': 1,
|
||||||
|
'timestamp': 1595822400,
|
||||||
|
'upload_date': '20200727',
|
||||||
},
|
},
|
||||||
'skip': 'This video is only available for registered users',
|
'skip': 'This video is only available for registered users',
|
||||||
'params': {
|
'params': {
|
||||||
@@ -300,69 +312,73 @@ class VrtNUIE(GigyaBaseIE):
|
|||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id = self._match_id(url)
|
display_id = self._match_id(url)
|
||||||
|
|
||||||
webpage, urlh = self._download_webpage_handle(url, display_id)
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
|
||||||
|
attrs = extract_attributes(self._search_regex(
|
||||||
|
r'(<nui-media[^>]+>)', webpage, 'media element'))
|
||||||
|
video_id = attrs['videoid']
|
||||||
|
publication_id = attrs.get('publicationid')
|
||||||
|
if publication_id:
|
||||||
|
video_id = publication_id + '$' + video_id
|
||||||
|
|
||||||
|
page = (self._parse_json(self._search_regex(
|
||||||
|
r'digitalData\s*=\s*({.+?});', webpage, 'digial data',
|
||||||
|
default='{}'), video_id, fatal=False) or {}).get('page') or {}
|
||||||
|
|
||||||
info = self._search_json_ld(webpage, display_id, default={})
|
info = self._search_json_ld(webpage, display_id, default={})
|
||||||
|
|
||||||
# title is optional here since it may be extracted by extractor
|
|
||||||
# that is delegated from here
|
|
||||||
title = strip_or_none(self._html_search_regex(
|
|
||||||
r'(?ms)<h1 class="content__heading">(.+?)</h1>',
|
|
||||||
webpage, 'title', default=None))
|
|
||||||
|
|
||||||
description = self._html_search_regex(
|
|
||||||
r'(?ms)<div class="content__description">(.+?)</div>',
|
|
||||||
webpage, 'description', default=None)
|
|
||||||
|
|
||||||
season = self._html_search_regex(
|
|
||||||
[r'''(?xms)<div\ class="tabs__tab\ tabs__tab--active">\s*
|
|
||||||
<span>seizoen\ (.+?)</span>\s*
|
|
||||||
</div>''',
|
|
||||||
r'<option value="seizoen (\d{1,3})" data-href="[^"]+?" selected>'],
|
|
||||||
webpage, 'season', default=None)
|
|
||||||
|
|
||||||
season_number = int_or_none(season)
|
|
||||||
|
|
||||||
episode_number = int_or_none(self._html_search_regex(
|
|
||||||
r'''(?xms)<div\ class="content__episode">\s*
|
|
||||||
<abbr\ title="aflevering">afl</abbr>\s*<span>(\d+)</span>
|
|
||||||
</div>''',
|
|
||||||
webpage, 'episode_number', default=None))
|
|
||||||
|
|
||||||
release_date = parse_iso8601(self._html_search_regex(
|
|
||||||
r'(?ms)<div class="content__broadcastdate">\s*<time\ datetime="(.+?)"',
|
|
||||||
webpage, 'release_date', default=None))
|
|
||||||
|
|
||||||
# If there's a ? or a # in the URL, remove them and everything after
|
|
||||||
clean_url = urlh.geturl().split('?')[0].split('#')[0].strip('/')
|
|
||||||
securevideo_url = clean_url + '.mssecurevideo.json'
|
|
||||||
|
|
||||||
try:
|
|
||||||
video = self._download_json(securevideo_url, display_id)
|
|
||||||
except ExtractorError as e:
|
|
||||||
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:
|
|
||||||
self.raise_login_required()
|
|
||||||
raise
|
|
||||||
|
|
||||||
# We are dealing with a '../<show>.relevant' URL
|
|
||||||
redirect_url = video.get('url')
|
|
||||||
if redirect_url:
|
|
||||||
return self.url_result(self._proto_relative_url(redirect_url, 'https:'))
|
|
||||||
|
|
||||||
# There is only one entry, but with an unknown key, so just get
|
|
||||||
# the first one
|
|
||||||
video_id = list(video.values())[0].get('videoid')
|
|
||||||
|
|
||||||
return merge_dicts(info, {
|
return merge_dicts(info, {
|
||||||
'_type': 'url_transparent',
|
'_type': 'url_transparent',
|
||||||
'url': 'https://mediazone.vrt.be/api/v1/vrtvideo/assets/%s' % video_id,
|
'url': 'https://mediazone.vrt.be/api/v1/vrtvideo/assets/%s' % video_id,
|
||||||
'ie_key': CanvasIE.ie_key(),
|
'ie_key': CanvasIE.ie_key(),
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'display_id': display_id,
|
'display_id': display_id,
|
||||||
|
'season_number': int_or_none(page.get('episode_season')),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class DagelijkseKostIE(InfoExtractor):
|
||||||
|
IE_DESC = 'dagelijksekost.een.be'
|
||||||
|
_VALID_URL = r'https?://dagelijksekost\.een\.be/gerechten/(?P<id>[^/?#&]+)'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'https://dagelijksekost.een.be/gerechten/hachis-parmentier-met-witloof',
|
||||||
|
'md5': '30bfffc323009a3e5f689bef6efa2365',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'md-ast-27a4d1ff-7d7b-425e-b84f-a4d227f592fa',
|
||||||
|
'display_id': 'hachis-parmentier-met-witloof',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Hachis parmentier met witloof',
|
||||||
|
'description': 'md5:9960478392d87f63567b5b117688cdc5',
|
||||||
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
|
'duration': 283.02,
|
||||||
|
},
|
||||||
|
'expected_warnings': ['is not a supported codec'],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
|
||||||
|
title = strip_or_none(get_element_by_class(
|
||||||
|
'dish-metadata__title', webpage
|
||||||
|
) or self._html_search_meta(
|
||||||
|
'twitter:title', webpage))
|
||||||
|
|
||||||
|
description = clean_html(get_element_by_class(
|
||||||
|
'dish-description', webpage)
|
||||||
|
) or self._html_search_meta(
|
||||||
|
('description', 'twitter:description', 'og:description'),
|
||||||
|
webpage)
|
||||||
|
|
||||||
|
video_id = self._html_search_regex(
|
||||||
|
r'data-url=(["\'])(?P<id>(?:(?!\1).)+)\1', webpage, 'video id',
|
||||||
|
group='id')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'_type': 'url_transparent',
|
||||||
|
'url': 'https://mediazone.vrt.be/api/v1/dako/assets/%s' % video_id,
|
||||||
|
'ie_key': CanvasIE.ie_key(),
|
||||||
|
'id': video_id,
|
||||||
|
'display_id': display_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': description,
|
'description': description,
|
||||||
'season': season,
|
}
|
||||||
'season_number': season_number,
|
|
||||||
'episode_number': episode_number,
|
|
||||||
'release_date': release_date,
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
from xml.sax.saxutils import escape
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
@@ -216,6 +218,29 @@ class CBCWatchBaseIE(InfoExtractor):
|
|||||||
'clearleap': 'http://www.clearleap.com/namespace/clearleap/1.0/',
|
'clearleap': 'http://www.clearleap.com/namespace/clearleap/1.0/',
|
||||||
}
|
}
|
||||||
_GEO_COUNTRIES = ['CA']
|
_GEO_COUNTRIES = ['CA']
|
||||||
|
_LOGIN_URL = 'https://api.loginradius.com/identity/v2/auth/login'
|
||||||
|
_TOKEN_URL = 'https://cloud-api.loginradius.com/sso/jwt/api/token'
|
||||||
|
_API_KEY = '3f4beddd-2061-49b0-ae80-6f1f2ed65b37'
|
||||||
|
_NETRC_MACHINE = 'cbcwatch'
|
||||||
|
|
||||||
|
def _signature(self, email, password):
|
||||||
|
data = json.dumps({
|
||||||
|
'email': email,
|
||||||
|
'password': password,
|
||||||
|
}).encode()
|
||||||
|
headers = {'content-type': 'application/json'}
|
||||||
|
query = {'apikey': self._API_KEY}
|
||||||
|
resp = self._download_json(self._LOGIN_URL, None, data=data, headers=headers, query=query)
|
||||||
|
access_token = resp['access_token']
|
||||||
|
|
||||||
|
# token
|
||||||
|
query = {
|
||||||
|
'access_token': access_token,
|
||||||
|
'apikey': self._API_KEY,
|
||||||
|
'jwtapp': 'jwt',
|
||||||
|
}
|
||||||
|
resp = self._download_json(self._TOKEN_URL, None, headers=headers, query=query)
|
||||||
|
return resp['signature']
|
||||||
|
|
||||||
def _call_api(self, path, video_id):
|
def _call_api(self, path, video_id):
|
||||||
url = path if path.startswith('http') else self._API_BASE_URL + path
|
url = path if path.startswith('http') else self._API_BASE_URL + path
|
||||||
@@ -239,7 +264,8 @@ class CBCWatchBaseIE(InfoExtractor):
|
|||||||
def _real_initialize(self):
|
def _real_initialize(self):
|
||||||
if self._valid_device_token():
|
if self._valid_device_token():
|
||||||
return
|
return
|
||||||
device = self._downloader.cache.load('cbcwatch', 'device') or {}
|
device = self._downloader.cache.load(
|
||||||
|
'cbcwatch', self._cache_device_key()) or {}
|
||||||
self._device_id, self._device_token = device.get('id'), device.get('token')
|
self._device_id, self._device_token = device.get('id'), device.get('token')
|
||||||
if self._valid_device_token():
|
if self._valid_device_token():
|
||||||
return
|
return
|
||||||
@@ -248,16 +274,30 @@ class CBCWatchBaseIE(InfoExtractor):
|
|||||||
def _valid_device_token(self):
|
def _valid_device_token(self):
|
||||||
return self._device_id and self._device_token
|
return self._device_id and self._device_token
|
||||||
|
|
||||||
|
def _cache_device_key(self):
|
||||||
|
email, _ = self._get_login_info()
|
||||||
|
return '%s_device' % hashlib.sha256(email.encode()).hexdigest() if email else 'device'
|
||||||
|
|
||||||
def _register_device(self):
|
def _register_device(self):
|
||||||
self._device_id = self._device_token = None
|
|
||||||
result = self._download_xml(
|
result = self._download_xml(
|
||||||
self._API_BASE_URL + 'device/register',
|
self._API_BASE_URL + 'device/register',
|
||||||
None, 'Acquiring device token',
|
None, 'Acquiring device token',
|
||||||
data=b'<device><type>web</type></device>')
|
data=b'<device><type>web</type></device>')
|
||||||
self._device_id = xpath_text(result, 'deviceId', fatal=True)
|
self._device_id = xpath_text(result, 'deviceId', fatal=True)
|
||||||
|
email, password = self._get_login_info()
|
||||||
|
if email and password:
|
||||||
|
signature = self._signature(email, password)
|
||||||
|
data = '<login><token>{0}</token><device><deviceId>{1}</deviceId><type>web</type></device></login>'.format(
|
||||||
|
escape(signature), escape(self._device_id)).encode()
|
||||||
|
url = self._API_BASE_URL + 'device/login'
|
||||||
|
result = self._download_xml(
|
||||||
|
url, None, data=data,
|
||||||
|
headers={'content-type': 'application/xml'})
|
||||||
|
self._device_token = xpath_text(result, 'token', fatal=True)
|
||||||
|
else:
|
||||||
self._device_token = xpath_text(result, 'deviceToken', fatal=True)
|
self._device_token = xpath_text(result, 'deviceToken', fatal=True)
|
||||||
self._downloader.cache.store(
|
self._downloader.cache.store(
|
||||||
'cbcwatch', 'device', {
|
'cbcwatch', self._cache_device_key(), {
|
||||||
'id': self._device_id,
|
'id': self._device_id,
|
||||||
'token': self._device_token,
|
'token': self._device_token,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class CBSBaseIE(ThePlatformFeedIE):
|
|||||||
|
|
||||||
|
|
||||||
class CBSIE(CBSBaseIE):
|
class CBSIE(CBSBaseIE):
|
||||||
_VALID_URL = r'(?:cbs:|https?://(?:www\.)?(?:cbs\.com/shows/[^/]+/video|colbertlateshow\.com/(?:video|podcasts))/)(?P<id>[\w-]+)'
|
_VALID_URL = r'(?:cbs:|https?://(?:www\.)?(?:(?:cbs|paramountplus)\.com/shows/[^/]+/video|colbertlateshow\.com/(?:video|podcasts))/)(?P<id>[\w-]+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.cbs.com/shows/garth-brooks/video/_u7W953k6la293J7EPTd9oHkSPs6Xn6_/connect-chat-feat-garth-brooks/',
|
'url': 'http://www.cbs.com/shows/garth-brooks/video/_u7W953k6la293J7EPTd9oHkSPs6Xn6_/connect-chat-feat-garth-brooks/',
|
||||||
@@ -52,6 +52,9 @@ class CBSIE(CBSBaseIE):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'http://www.colbertlateshow.com/podcasts/dYSwjqPs_X1tvbV_P2FcPWRa_qT6akTC/in-the-bad-room-with-stephen/',
|
'url': 'http://www.colbertlateshow.com/podcasts/dYSwjqPs_X1tvbV_P2FcPWRa_qT6akTC/in-the-bad-room-with-stephen/',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.paramountplus.com/shows/all-rise/video/QmR1WhNkh1a_IrdHZrbcRklm176X_rVc/all-rise-space/',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _extract_video_info(self, content_id, site='cbs', mpx_acc=2198311517):
|
def _extract_video_info(self, content_id, site='cbs', mpx_acc=2198311517):
|
||||||
|
|||||||
@@ -11,7 +11,47 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class CBSLocalIE(AnvatoIE):
|
class CBSLocalIE(AnvatoIE):
|
||||||
_VALID_URL = r'https?://[a-z]+\.cbslocal\.com/(?:\d+/\d+/\d+|video)/(?P<id>[0-9a-z-]+)'
|
_VALID_URL_BASE = r'https?://[a-z]+\.cbslocal\.com/'
|
||||||
|
_VALID_URL = _VALID_URL_BASE + r'video/(?P<id>\d+)'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://newyork.cbslocal.com/video/3580809-a-very-blue-anniversary/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '3580809',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'A Very Blue Anniversary',
|
||||||
|
'description': 'CBS2’s Cindy Hsu has more.',
|
||||||
|
'thumbnail': 're:^https?://.*',
|
||||||
|
'timestamp': int,
|
||||||
|
'upload_date': r're:^\d{8}$',
|
||||||
|
'uploader': 'CBS',
|
||||||
|
'subtitles': {
|
||||||
|
'en': 'mincount:5',
|
||||||
|
},
|
||||||
|
'categories': [
|
||||||
|
'Stations\\Spoken Word\\WCBSTV',
|
||||||
|
'Syndication\\AOL',
|
||||||
|
'Syndication\\MSN',
|
||||||
|
'Syndication\\NDN',
|
||||||
|
'Syndication\\Yahoo',
|
||||||
|
'Content\\News',
|
||||||
|
'Content\\News\\Local News',
|
||||||
|
],
|
||||||
|
'tags': ['CBS 2 News Weekends', 'Cindy Hsu', 'Blue Man Group'],
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
mcp_id = self._match_id(url)
|
||||||
|
return self.url_result(
|
||||||
|
'anvato:anvato_cbslocal_app_web_prod_547f3e49241ef0e5d30c79b2efbca5d92c698f67:' + mcp_id, 'Anvato', mcp_id)
|
||||||
|
|
||||||
|
|
||||||
|
class CBSLocalArticleIE(AnvatoIE):
|
||||||
|
_VALID_URL = CBSLocalIE._VALID_URL_BASE + r'\d+/\d+/\d+/(?P<id>[0-9a-z-]+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
# Anvato backend
|
# Anvato backend
|
||||||
@@ -52,31 +92,6 @@ class CBSLocalIE(AnvatoIE):
|
|||||||
# m3u8 download
|
# m3u8 download
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
}, {
|
|
||||||
'url': 'http://newyork.cbslocal.com/video/3580809-a-very-blue-anniversary/',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '3580809',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'A Very Blue Anniversary',
|
|
||||||
'description': 'CBS2’s Cindy Hsu has more.',
|
|
||||||
'thumbnail': 're:^https?://.*',
|
|
||||||
'timestamp': int,
|
|
||||||
'upload_date': r're:^\d{8}$',
|
|
||||||
'uploader': 'CBS',
|
|
||||||
'subtitles': {
|
|
||||||
'en': 'mincount:5',
|
|
||||||
},
|
|
||||||
'categories': [
|
|
||||||
'Stations\\Spoken Word\\WCBSTV',
|
|
||||||
'Syndication\\AOL',
|
|
||||||
'Syndication\\MSN',
|
|
||||||
'Syndication\\NDN',
|
|
||||||
'Syndication\\Yahoo',
|
|
||||||
'Content\\News',
|
|
||||||
'Content\\News\\Local News',
|
|
||||||
],
|
|
||||||
'tags': ['CBS 2 News Weekends', 'Cindy Hsu', 'Blue Man Group'],
|
|
||||||
},
|
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class CBSNewsEmbedIE(CBSIE):
|
|||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
item = self._parse_json(zlib.decompress(compat_b64decode(
|
item = self._parse_json(zlib.decompress(compat_b64decode(
|
||||||
compat_urllib_parse_unquote(self._match_id(url))),
|
compat_urllib_parse_unquote(self._match_id(url))),
|
||||||
-zlib.MAX_WBITS), None)['video']['items'][0]
|
-zlib.MAX_WBITS).decode('utf-8'), None)['video']['items'][0]
|
||||||
return self._extract_video_info(item['mpxRefId'], 'cbsnews')
|
return self._extract_video_info(item['mpxRefId'], 'cbsnews')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +1,113 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from .cbs import CBSBaseIE
|
import re
|
||||||
|
|
||||||
|
# from .cbs import CBSBaseIE
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
int_or_none,
|
||||||
|
try_get,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CBSSportsIE(CBSBaseIE):
|
# class CBSSportsEmbedIE(CBSBaseIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?cbssports\.com/[^/]+/(?:video|news)/(?P<id>[^/?#&]+)'
|
class CBSSportsEmbedIE(InfoExtractor):
|
||||||
|
IE_NAME = 'cbssports:embed'
|
||||||
|
_VALID_URL = r'''(?ix)https?://(?:(?:www\.)?cbs|embed\.247)sports\.com/player/embed.+?
|
||||||
|
(?:
|
||||||
|
ids%3D(?P<id>[\da-f]{8}-(?:[\da-f]{4}-){3}[\da-f]{12})|
|
||||||
|
pcid%3D(?P<pcid>\d+)
|
||||||
|
)'''
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.cbssports.com/nba/video/donovan-mitchell-flashes-star-potential-in-game-2-victory-over-thunder/',
|
'url': 'https://www.cbssports.com/player/embed/?args=player_id%3Db56c03a6-231a-4bbe-9c55-af3c8a8e9636%26ids%3Db56c03a6-231a-4bbe-9c55-af3c8a8e9636%26resizable%3D1%26autoplay%3Dtrue%26domain%3Dcbssports.com%26comp_ads_enabled%3Dfalse%26watchAndRead%3D0%26startTime%3D0%26env%3Dprod',
|
||||||
'info_dict': {
|
'only_matching': True,
|
||||||
'id': '1214315075735',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Donovan Mitchell flashes star potential in Game 2 victory over Thunder',
|
|
||||||
'description': 'md5:df6f48622612c2d6bd2e295ddef58def',
|
|
||||||
'timestamp': 1524111457,
|
|
||||||
'upload_date': '20180419',
|
|
||||||
'uploader': 'CBSI-NEW',
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
# m3u8 download
|
|
||||||
'skip_download': True,
|
|
||||||
}
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.cbssports.com/nba/news/nba-playoffs-2018-watch-76ers-vs-heat-game-3-series-schedule-tv-channel-online-stream/',
|
'url': 'https://embed.247sports.com/player/embed/?args=%3fplayer_id%3d1827823171591%26channel%3dcollege-football-recruiting%26pcid%3d1827823171591%26width%3d640%26height%3d360%26autoplay%3dTrue%26comp_ads_enabled%3dFalse%26uvpc%3dhttps%253a%252f%252fwww.cbssports.com%252fapi%252fcontent%252fvideo%252fconfig%252f%253fcfg%253duvp_247sports_v4%2526partner%253d247%26uvpc_m%3dhttps%253a%252f%252fwww.cbssports.com%252fapi%252fcontent%252fvideo%252fconfig%252f%253fcfg%253duvp_247sports_m_v4%2526partner_m%253d247_mobile%26utag%3d247sportssite%26resizable%3dTrue',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _extract_video_info(self, filter_query, video_id):
|
# def _extract_video_info(self, filter_query, video_id):
|
||||||
return self._extract_feed_info('dJ5BDC', 'VxxJg8Ymh8sE', filter_query, video_id)
|
# return self._extract_feed_info('dJ5BDC', 'VxxJg8Ymh8sE', filter_query, video_id)
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
uuid, pcid = re.match(self._VALID_URL, url).groups()
|
||||||
|
query = {'id': uuid} if uuid else {'pcid': pcid}
|
||||||
|
video = self._download_json(
|
||||||
|
'https://www.cbssports.com/api/content/video/',
|
||||||
|
uuid or pcid, query=query)[0]
|
||||||
|
video_id = video['id']
|
||||||
|
title = video['title']
|
||||||
|
metadata = video.get('metaData') or {}
|
||||||
|
# return self._extract_video_info('byId=%d' % metadata['mpxOutletId'], video_id)
|
||||||
|
# return self._extract_video_info('byGuid=' + metadata['mpxRefId'], video_id)
|
||||||
|
|
||||||
|
formats = self._extract_m3u8_formats(
|
||||||
|
metadata['files'][0]['url'], video_id, 'mp4',
|
||||||
|
'm3u8_native', m3u8_id='hls', fatal=False)
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
image = video.get('image')
|
||||||
|
thumbnails = None
|
||||||
|
if image:
|
||||||
|
image_path = image.get('path')
|
||||||
|
if image_path:
|
||||||
|
thumbnails = [{
|
||||||
|
'url': image_path,
|
||||||
|
'width': int_or_none(image.get('width')),
|
||||||
|
'height': int_or_none(image.get('height')),
|
||||||
|
'filesize': int_or_none(image.get('size')),
|
||||||
|
}]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'formats': formats,
|
||||||
|
'thumbnails': thumbnails,
|
||||||
|
'description': video.get('description'),
|
||||||
|
'timestamp': int_or_none(try_get(video, lambda x: x['dateCreated']['epoch'])),
|
||||||
|
'duration': int_or_none(metadata.get('duration')),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CBSSportsBaseIE(InfoExtractor):
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id = self._match_id(url)
|
display_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, display_id)
|
webpage = self._download_webpage(url, display_id)
|
||||||
video_id = self._search_regex(
|
iframe_url = self._search_regex(
|
||||||
[r'(?:=|%26)pcid%3D(\d+)', r'embedVideo(?:Container)?_(\d+)'],
|
r'<iframe[^>]+(?:data-)?src="(https?://[^/]+/player/embed[^"]+)"',
|
||||||
webpage, 'video id')
|
webpage, 'embed url')
|
||||||
return self._extract_video_info('byId=%s' % video_id, video_id)
|
return self.url_result(iframe_url, CBSSportsEmbedIE.ie_key())
|
||||||
|
|
||||||
|
|
||||||
|
class CBSSportsIE(CBSSportsBaseIE):
|
||||||
|
IE_NAME = 'cbssports'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?cbssports\.com/[^/]+/video/(?P<id>[^/?#&]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.cbssports.com/college-football/video/cover-3-stanford-spring-gleaning/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'b56c03a6-231a-4bbe-9c55-af3c8a8e9636',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Cover 3: Stanford Spring Gleaning',
|
||||||
|
'description': 'The Cover 3 crew break down everything you need to know about the Stanford Cardinal this spring.',
|
||||||
|
'timestamp': 1617218398,
|
||||||
|
'upload_date': '20210331',
|
||||||
|
'duration': 502,
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
class TwentyFourSevenSportsIE(CBSSportsBaseIE):
|
||||||
|
IE_NAME = '247sports'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?247sports\.com/Video/(?:[^/?#&]+-)?(?P<id>\d+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://247sports.com/Video/2021-QB-Jake-Garcia-senior-highlights-through-five-games-10084854/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '4f1265cb-c3b5-44a8-bb1d-1914119a0ccc',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '2021 QB Jake Garcia senior highlights through five games',
|
||||||
|
'description': 'md5:8cb67ebed48e2e6adac1701e0ff6e45b',
|
||||||
|
'timestamp': 1607114223,
|
||||||
|
'upload_date': '20201204',
|
||||||
|
'duration': 208,
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user