mirror of
https://github.com/ytdl-org/youtube-dl.git
synced 2025-12-08 15:12:43 +01:00
Compare commits
3911 Commits
2015.02.19
...
2016.02.09
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
085ad71157 | ||
|
|
35972ba172 | ||
|
|
3834d3e35c | ||
|
|
8d0a2a2a4e | ||
|
|
11c0339bec | ||
|
|
915dd77783 | ||
|
|
b6bfa6fb79 | ||
|
|
f070197bd7 | ||
|
|
5a7699bb2e | ||
|
|
8628d26f38 | ||
|
|
8411229bd5 | ||
|
|
72b9ebc65d | ||
|
|
3b799ca14c | ||
|
|
0474512e30 | ||
|
|
f0905c6ec3 | ||
|
|
86296ad2cd | ||
|
|
52f5889f77 | ||
|
|
81e0b4f2d1 | ||
|
|
cbecc9b903 | ||
|
|
b8b465af3e | ||
|
|
59b35c6745 | ||
|
|
7032833011 | ||
|
|
f406c78785 | ||
|
|
f326b5837a | ||
|
|
5dd4b3468f | ||
|
|
d4f8e83404 | ||
|
|
7b8b007cd9 | ||
|
|
3547d26587 | ||
|
|
7e62c2eb6d | ||
|
|
56401e1e5f | ||
|
|
860db2d508 | ||
|
|
4b8874975c | ||
|
|
bd6b6f6622 | ||
|
|
4340727e6c | ||
|
|
3ceccade87 | ||
|
|
28ad7df65d | ||
|
|
79a3508579 | ||
|
|
1b840245bd | ||
|
|
6a3828fddd | ||
|
|
91cb6b5065 | ||
|
|
0826a0b555 | ||
|
|
bcbbb98bfe | ||
|
|
66159b38aa | ||
|
|
23d17e4beb | ||
|
|
d97b0e3241 | ||
|
|
eb2533ec4c | ||
|
|
b7b365067f | ||
|
|
86e284e028 | ||
|
|
d9e543b680 | ||
|
|
c773c232d8 | ||
|
|
58ae24336a | ||
|
|
7d3a035ee0 | ||
|
|
e06e75c7e7 | ||
|
|
593e0f43b4 | ||
|
|
008ab0f814 | ||
|
|
3f7e8750d4 | ||
|
|
f1ed3acae5 | ||
|
|
920d21b9d3 | ||
|
|
2fb35d1c28 | ||
|
|
09be85b8dd | ||
|
|
eadc3ccd50 | ||
|
|
255732f0d3 | ||
|
|
53c269c6fd | ||
|
|
675d001633 | ||
|
|
58be922079 | ||
|
|
c84d3a557d | ||
|
|
d577c79632 | ||
|
|
6ad2b01e14 | ||
|
|
fd3a1f3d60 | ||
|
|
87de7069b9 | ||
|
|
6fba62c87a | ||
|
|
f14be22816 | ||
|
|
1df4141196 | ||
|
|
fae45ede08 | ||
|
|
4e0cff2a50 | ||
|
|
9c74423510 | ||
|
|
5976e7ab57 | ||
|
|
a1a22572fb | ||
|
|
c11875b328 | ||
|
|
8ff648e4f9 | ||
|
|
1bac34556f | ||
|
|
0436157b95 | ||
|
|
ae0db349c1 | ||
|
|
08411970d5 | ||
|
|
dc724e0c8b | ||
|
|
0a5d1ec706 | ||
|
|
58250eff2b | ||
|
|
11a4efc505 | ||
|
|
7537b35fb8 | ||
|
|
33cc74eeeb | ||
|
|
f021acee49 | ||
|
|
abe694ca95 | ||
|
|
b286f201a8 | ||
|
|
bd93a12e85 | ||
|
|
92769650fa | ||
|
|
dc4fe5c6d7 | ||
|
|
566bda51f2 | ||
|
|
f63757ec35 | ||
|
|
7a0ed06909 | ||
|
|
9934fe76be | ||
|
|
a8aad21001 | ||
|
|
d055bf91cc | ||
|
|
0e1b1a011d | ||
|
|
eab3c2895c | ||
|
|
163da6a484 | ||
|
|
324916d11a | ||
|
|
3ccb0655c1 | ||
|
|
e04398e397 | ||
|
|
231ea2a3bb | ||
|
|
b99d88c6a1 | ||
|
|
189d72d5fd | ||
|
|
a7aab0c23e | ||
|
|
a69bee4762 | ||
|
|
9acd33094d | ||
|
|
8e7aad2075 | ||
|
|
ce5879fa14 | ||
|
|
7b7507d6e1 | ||
|
|
14823decf3 | ||
|
|
673fb82e65 | ||
|
|
181cf24bc0 | ||
|
|
89f2602880 | ||
|
|
db9b1dbcd9 | ||
|
|
e881c4bcab | ||
|
|
670ad51ade | ||
|
|
eb6fc7d32a | ||
|
|
ed1a390583 | ||
|
|
809e1857c5 | ||
|
|
7c38af48b9 | ||
|
|
60ad3eb970 | ||
|
|
a7685b3a6b | ||
|
|
8f1fddc816 | ||
|
|
1bf996fa5c | ||
|
|
248ae880b6 | ||
|
|
2d2fa82d17 | ||
|
|
c94678957f | ||
|
|
16f38a699f | ||
|
|
a6c2c24479 | ||
|
|
b8c9926c0a | ||
|
|
df374b5222 | ||
|
|
5ea1eb78f5 | ||
|
|
5d2c0fd9ba | ||
|
|
0803753fea | ||
|
|
2c2f1efdcd | ||
|
|
b323e1707d | ||
|
|
09104e9930 | ||
|
|
5fa1702ca6 | ||
|
|
17b598d30c | ||
|
|
53be8894e4 | ||
|
|
c3deacd562 | ||
|
|
8ab3fe81d8 | ||
|
|
2f0a33d8a3 | ||
|
|
05d0d131a7 | ||
|
|
c140629995 | ||
|
|
7d106a65ca | ||
|
|
0179f6a830 | ||
|
|
830afe85dc | ||
|
|
8bf39420b4 | ||
|
|
71d08b3e29 | ||
|
|
06ffa33485 | ||
|
|
874e05975b | ||
|
|
f5d30d521c | ||
|
|
e047922be0 | ||
|
|
83ab8a79cc | ||
|
|
350cf045d8 | ||
|
|
68a0ea15b4 | ||
|
|
2b4f5e68d1 | ||
|
|
055f417278 | ||
|
|
70029bc348 | ||
|
|
cf57433bbd | ||
|
|
1ac6e794cb | ||
|
|
a853427427 | ||
|
|
50e989e263 | ||
|
|
10e6ed9341 | ||
|
|
38c84acae5 | ||
|
|
29f46c2bee | ||
|
|
39c10a2b6e | ||
|
|
b913348d5f | ||
|
|
2b14cb566f | ||
|
|
b0df5223be | ||
|
|
ed7cd1e859 | ||
|
|
f125d9115b | ||
|
|
a9d5f12fec | ||
|
|
7f32e5dc35 | ||
|
|
c3111ab34f | ||
|
|
9339774af2 | ||
|
|
b0d21deda9 | ||
|
|
fab6f0e65b | ||
|
|
b6c33fd544 | ||
|
|
fb4b345800 | ||
|
|
af9c2a07ae | ||
|
|
ab180fc648 | ||
|
|
682f8c43b5 | ||
|
|
f693213567 | ||
|
|
9165d6bab9 | ||
|
|
2975fe1a7b | ||
|
|
de691a498d | ||
|
|
2e6e742c3c | ||
|
|
e9bd0f772b | ||
|
|
77f785076f | ||
|
|
94278f7202 | ||
|
|
a0d8d704df | ||
|
|
f6861ec96f | ||
|
|
f733b05302 | ||
|
|
6fa73386cb | ||
|
|
5ca01bb9e4 | ||
|
|
1ca59daca9 | ||
|
|
594c4d79a5 | ||
|
|
1f16b958b1 | ||
|
|
4c0d13df9b | ||
|
|
b2c6528baf | ||
|
|
ea17820432 | ||
|
|
1257b049bc | ||
|
|
b969813548 | ||
|
|
10677ece81 | ||
|
|
d570746e45 | ||
|
|
4fcd9d147d | ||
|
|
9c54ae3387 | ||
|
|
24114fee74 | ||
|
|
220ee33f2b | ||
|
|
4118cc02c1 | ||
|
|
32d77eeb04 | ||
|
|
032f232626 | ||
|
|
4d318be195 | ||
|
|
6b45f9aba2 | ||
|
|
1e10d02fec | ||
|
|
51290d8457 | ||
|
|
582f4f834e | ||
|
|
e87d98b0dd | ||
|
|
383496e65e | ||
|
|
4519c1f43c | ||
|
|
a616f65471 | ||
|
|
1f78ed189a | ||
|
|
7dde358adc | ||
|
|
27b83249c9 | ||
|
|
56aa074538 | ||
|
|
9d90e7de03 | ||
|
|
7d4d9c526a | ||
|
|
fe6856b059 | ||
|
|
a54fbf2ca6 | ||
|
|
d8024aebe5 | ||
|
|
8652bd22f1 | ||
|
|
f15a9ca301 | ||
|
|
65ced034b8 | ||
|
|
bec30224ff | ||
|
|
0428106da3 | ||
|
|
73e7442456 | ||
|
|
26de1bba83 | ||
|
|
e0690782b8 | ||
|
|
8fff4f61e5 | ||
|
|
10defdd06a | ||
|
|
485139c15c | ||
|
|
b605ebb609 | ||
|
|
aecfcd4e59 | ||
|
|
942d46196f | ||
|
|
78be2eca7c | ||
|
|
1fa2b9841d | ||
|
|
9fbd0822aa | ||
|
|
e323cf3ff3 | ||
|
|
8ceabd4df3 | ||
|
|
a8776b107b | ||
|
|
096b533982 | ||
|
|
dae503afaa | ||
|
|
b39eab7f94 | ||
|
|
e5a66240c0 | ||
|
|
e0ef13ddeb | ||
|
|
855f90fa6f | ||
|
|
614db89ae3 | ||
|
|
1358b94163 | ||
|
|
350e02d40d | ||
|
|
0b26ba3fc8 | ||
|
|
3a0a78731b | ||
|
|
6be16ed24b | ||
|
|
b555942428 | ||
|
|
b2dca40d81 | ||
|
|
15870bbd01 | ||
|
|
10d33b3473 | ||
|
|
ac25992bc7 | ||
|
|
30783c442d | ||
|
|
a50a8003a0 | ||
|
|
315bdae00a | ||
|
|
2ddfd26f1b | ||
|
|
f3ed5df611 | ||
|
|
b4e44234bc | ||
|
|
4ca2a3cf3c | ||
|
|
33d2fc2f64 | ||
|
|
27a95f51aa | ||
|
|
a78d6a9bb1 | ||
|
|
567f9a5809 | ||
|
|
3a421c724f | ||
|
|
34dd81c03a | ||
|
|
b3f502cdb9 | ||
|
|
587dfd44a4 | ||
|
|
52767c1ba0 | ||
|
|
014b5c59d8 | ||
|
|
fad7a336a1 | ||
|
|
ffbc0baf72 | ||
|
|
345f12196c | ||
|
|
5769b68bc0 | ||
|
|
4e2743abd9 | ||
|
|
be2d40a58a | ||
|
|
81549898c0 | ||
|
|
0baedd1851 | ||
|
|
6b559c2fbc | ||
|
|
986986064e | ||
|
|
4654c1d016 | ||
|
|
163e8369b0 | ||
|
|
5cc9c5dfa8 | ||
|
|
fbd90643cb | ||
|
|
30e2f2d76f | ||
|
|
11c60089a8 | ||
|
|
abb893e6e4 | ||
|
|
4511c1976d | ||
|
|
4240d50496 | ||
|
|
6240b0a278 | ||
|
|
e37afbe0b8 | ||
|
|
40cf7fcbd2 | ||
|
|
cc28492d31 | ||
|
|
bc0550c262 | ||
|
|
b83b782dc4 | ||
|
|
16a348475c | ||
|
|
709185a264 | ||
|
|
9cb1a06b6c | ||
|
|
be27283ef6 | ||
|
|
b924bfad68 | ||
|
|
192b9a571c | ||
|
|
6ec6cb4e95 | ||
|
|
36a0e46c39 | ||
|
|
dfb1b1468c | ||
|
|
3c91e41614 | ||
|
|
7e8a800f29 | ||
|
|
2334762b03 | ||
|
|
3fc088f8c7 | ||
|
|
a9bbd26f1d | ||
|
|
6e99d5762a | ||
|
|
15b1c6656f | ||
|
|
d412794205 | ||
|
|
0a899a1448 | ||
|
|
7a34302e95 | ||
|
|
27783821af | ||
|
|
b374af6ebd | ||
|
|
16f1131a4d | ||
|
|
d5f071afb5 | ||
|
|
14b4f038c0 | ||
|
|
bcac2a0710 | ||
|
|
1a6d92847f | ||
|
|
6a16fd4a1a | ||
|
|
44731e308c | ||
|
|
4763b624a6 | ||
|
|
6609b3ce37 | ||
|
|
7e182627d9 | ||
|
|
5777f5d386 | ||
|
|
5dbe81a1d3 | ||
|
|
4cf096a4a9 | ||
|
|
18e6c97c48 | ||
|
|
97afd99a18 | ||
|
|
23f13e9754 | ||
|
|
2e02ecbccc | ||
|
|
e4f49a8753 | ||
|
|
51d3045de2 | ||
|
|
76048b23e8 | ||
|
|
f20756fb10 | ||
|
|
17b2d7ca77 | ||
|
|
40f796288a | ||
|
|
2f546d0a3c | ||
|
|
18c782ab26 | ||
|
|
33cee6c7f6 | ||
|
|
a2e51e7b49 | ||
|
|
bd19aa0ed3 | ||
|
|
8f4c56f334 | ||
|
|
1dcc38b233 | ||
|
|
fff79f1867 | ||
|
|
3f17c357d9 | ||
|
|
9938a17f92 | ||
|
|
9746f4314a | ||
|
|
0238451fc0 | ||
|
|
2098aee7d6 | ||
|
|
fb588f6a56 | ||
|
|
896c7a23cd | ||
|
|
1463c5b9ac | ||
|
|
c6270b2ed5 | ||
|
|
ab3176af34 | ||
|
|
5aa535c329 | ||
|
|
133b1886fc | ||
|
|
66295fa4a6 | ||
|
|
e54c44eeab | ||
|
|
a7aaa39863 | ||
|
|
ea6abd740f | ||
|
|
e1a0bfdffe | ||
|
|
3f3343cd3e | ||
|
|
4059eabd58 | ||
|
|
6b46102661 | ||
|
|
141a273a8b | ||
|
|
2fffb1dcd0 | ||
|
|
e698e4e533 | ||
|
|
b7546397f0 | ||
|
|
0311677258 | ||
|
|
88fb59d91b | ||
|
|
a1d9f6c5dc | ||
|
|
c579c5e967 | ||
|
|
c9c194053d | ||
|
|
f20a11ed25 | ||
|
|
76a353c9e5 | ||
|
|
392f04d586 | ||
|
|
94de6cf59c | ||
|
|
8af2804a5d | ||
|
|
054479754c | ||
|
|
5bafcf6525 | ||
|
|
306c51c669 | ||
|
|
27bfd4e526 | ||
|
|
ca227c8698 | ||
|
|
32f9036447 | ||
|
|
190ef07981 | ||
|
|
82597f0ec0 | ||
|
|
8499d21158 | ||
|
|
c9154514c4 | ||
|
|
0d5095fc65 | ||
|
|
034caf70b2 | ||
|
|
e565cf6048 | ||
|
|
59f197aec1 | ||
|
|
a0e5beb0fb | ||
|
|
c1e90619bd | ||
|
|
fec09bf15d | ||
|
|
a0d7ede350 | ||
|
|
b26afec81f | ||
|
|
8f7c4f7d2e | ||
|
|
0416006a30 | ||
|
|
7f9134fb2d | ||
|
|
91e274546c | ||
|
|
69f8595256 | ||
|
|
930087f2f6 | ||
|
|
9f9f7664b5 | ||
|
|
72528252e3 | ||
|
|
e4bd63f9c0 | ||
|
|
9accfed4e7 | ||
|
|
f1e21efe63 | ||
|
|
b05641ce40 | ||
|
|
fec040e754 | ||
|
|
34a9da136f | ||
|
|
c43fda4c1a | ||
|
|
7de81fcc53 | ||
|
|
9d46608efa | ||
|
|
80b8b72cb8 | ||
|
|
9787c5f4c8 | ||
|
|
d5f6429de8 | ||
|
|
df827a983a | ||
|
|
29f3683901 | ||
|
|
c7932289e7 | ||
|
|
7a0b07c719 | ||
|
|
4d402db521 | ||
|
|
7109903e61 | ||
|
|
3092fc4035 | ||
|
|
f5bc4b5f95 | ||
|
|
69759a5990 | ||
|
|
453fe2a345 | ||
|
|
ff18735cb2 | ||
|
|
030dfb04e0 | ||
|
|
06e4874c99 | ||
|
|
0d8a0fdc30 | ||
|
|
53365f74a7 | ||
|
|
0368181998 | ||
|
|
6101f45ef9 | ||
|
|
bf96b45238 | ||
|
|
98d7c0f4f7 | ||
|
|
f2017cb020 | ||
|
|
f889ac45b8 | ||
|
|
eccde5e9de | ||
|
|
ce7d243c7e | ||
|
|
6c4d6609ad | ||
|
|
db710571fd | ||
|
|
574dd17882 | ||
|
|
422f7c112c | ||
|
|
e974356f32 | ||
|
|
b19ad2fb53 | ||
|
|
126d7701b0 | ||
|
|
ecf17d1653 | ||
|
|
7447661e9b | ||
|
|
7e5edcfd33 | ||
|
|
39d60b715a | ||
|
|
d497a201ca | ||
|
|
54537cdfb3 | ||
|
|
bc737bd61a | ||
|
|
a5c1d95500 | ||
|
|
c1f49e1684 | ||
|
|
9f66931e16 | ||
|
|
6c6b8bd5cc | ||
|
|
04e24906be | ||
|
|
974c1b2d42 | ||
|
|
bca9bea1c1 | ||
|
|
bd3f9ecabe | ||
|
|
c047270c02 | ||
|
|
97f18fac3a | ||
|
|
c71d2e2087 | ||
|
|
59185202c6 | ||
|
|
bee83e84f6 | ||
|
|
82e02ea5fc | ||
|
|
a95c26a06a | ||
|
|
0b0a17ae9d | ||
|
|
30f51acbc8 | ||
|
|
e0898585a1 | ||
|
|
62bdc9fecc | ||
|
|
e73277c7e8 | ||
|
|
8d29e47f54 | ||
|
|
2db772b9ea | ||
|
|
7b81316508 | ||
|
|
05358deeca | ||
|
|
9f610f3a9e | ||
|
|
dbfd06730c | ||
|
|
5b025168c7 | ||
|
|
46124a49b2 | ||
|
|
608cc3b85c | ||
|
|
6afe044b51 | ||
|
|
15aad84dc5 | ||
|
|
f7e1d82d40 | ||
|
|
339b1944e7 | ||
|
|
85367c3a47 | ||
|
|
607d65fbce | ||
|
|
9f0ee2a388 | ||
|
|
1fc0b47fdf | ||
|
|
6418b2439b | ||
|
|
06d5556dfa | ||
|
|
fb8e402ad2 | ||
|
|
c24044635b | ||
|
|
67ba388efb | ||
|
|
e41604227c | ||
|
|
8a609c32fd | ||
|
|
96db61ffb8 | ||
|
|
c153bd8b2f | ||
|
|
383003653f | ||
|
|
fc383f199e | ||
|
|
2c566d02fe | ||
|
|
a8f1d167f6 | ||
|
|
261b4c23c7 | ||
|
|
dcdc352371 | ||
|
|
be514c856c | ||
|
|
128eb31d90 | ||
|
|
747b028412 | ||
|
|
7fe37d8a05 | ||
|
|
f10c27b8cb | ||
|
|
60427f63d1 | ||
|
|
178b47e6af | ||
|
|
3a70ed9ebe | ||
|
|
89abf7bf4d | ||
|
|
cfe9e5aa6c | ||
|
|
4c24ed9464 | ||
|
|
11208ebbf1 | ||
|
|
774ce35571 | ||
|
|
dbee18b552 | ||
|
|
31d9ea4a3e | ||
|
|
3b68efdc6a | ||
|
|
2be689b7e2 | ||
|
|
2db5806991 | ||
|
|
220bc3f0e3 | ||
|
|
48a6c984b8 | ||
|
|
dc016bf521 | ||
|
|
ff43d2365f | ||
|
|
ed63cbd6ee | ||
|
|
5f432ac8f5 | ||
|
|
c7224074d6 | ||
|
|
eed30fea75 | ||
|
|
5625bd0617 | ||
|
|
5ef5d25b15 | ||
|
|
0f15ad7b9b | ||
|
|
f11d00fa41 | ||
|
|
61ebb401b7 | ||
|
|
5c5a3ecf1b | ||
|
|
0197004f78 | ||
|
|
2c28da8e05 | ||
|
|
c7fa5fa42c | ||
|
|
7ba71e30fb | ||
|
|
7cb0952474 | ||
|
|
a8ae232fa9 | ||
|
|
5b251628e9 | ||
|
|
b9a324c0da | ||
|
|
5b95419ca5 | ||
|
|
ecbccea703 | ||
|
|
c240ab6ecf | ||
|
|
6882c0870e | ||
|
|
b0eeaf4f40 | ||
|
|
c6ed6fadc2 | ||
|
|
e462474e1d | ||
|
|
6b77d52b1f | ||
|
|
9b9c5355e4 | ||
|
|
d890b4cc0a | ||
|
|
2c74e6fa77 | ||
|
|
c0384f221e | ||
|
|
8e60dc7526 | ||
|
|
8900ab4d9b | ||
|
|
fb043a6e4e | ||
|
|
7f8b271465 | ||
|
|
fdae235858 | ||
|
|
1deb710f26 | ||
|
|
ec6504b39c | ||
|
|
dd85e4d707 | ||
|
|
fa64a84311 | ||
|
|
e0f06eae43 | ||
|
|
0f206ee814 | ||
|
|
cc0f378d54 | ||
|
|
e33c9cba7c | ||
|
|
989e9f8ead | ||
|
|
8f097af4ec | ||
|
|
c40dbb19ab | ||
|
|
ffaf6e66e3 | ||
|
|
74c730174f | ||
|
|
c82a8dd14c | ||
|
|
f8253af561 | ||
|
|
ed370ff0e6 | ||
|
|
ee0f0393cf | ||
|
|
db2fe38b55 | ||
|
|
d631d5f9f2 | ||
|
|
4f29fa9906 | ||
|
|
5b72fda140 | ||
|
|
f81ccbb3df | ||
|
|
9fd0f67678 | ||
|
|
15d50aca64 | ||
|
|
7234d1d9c7 | ||
|
|
9796a9b20c | ||
|
|
016dd82050 | ||
|
|
b95779be21 | ||
|
|
10171468d9 | ||
|
|
bf597a6dd1 | ||
|
|
45dad8bab9 | ||
|
|
64ccbf18c0 | ||
|
|
9dc1d94a0c | ||
|
|
7824e1f6a6 | ||
|
|
2469a6aecb | ||
|
|
8f0afda028 | ||
|
|
35e22b6b32 | ||
|
|
323f82a7e0 | ||
|
|
8534bf1f00 | ||
|
|
eb4f27405b | ||
|
|
2d3b70271c | ||
|
|
ad1b6017cd | ||
|
|
05467d5a52 | ||
|
|
ae5e94808e | ||
|
|
d7ffcfcf97 | ||
|
|
0cb58b0259 | ||
|
|
31b2051e21 | ||
|
|
eb0bdc2c3e | ||
|
|
6583c741cd | ||
|
|
2d9295643e | ||
|
|
ee86e2c6d7 | ||
|
|
02a63fadc3 | ||
|
|
f3711edcf1 | ||
|
|
22d07ba4e4 | ||
|
|
f6abca506e | ||
|
|
b5424acdb9 | ||
|
|
47c7f3d995 | ||
|
|
0014ffa829 | ||
|
|
c03943f394 | ||
|
|
deb1e8d20e | ||
|
|
174964a7bc | ||
|
|
9c568178fb | ||
|
|
dbb7d7e26c | ||
|
|
ade2340971 | ||
|
|
4d77550cf0 | ||
|
|
c683454e7e | ||
|
|
f133fd326b | ||
|
|
1faa66f005 | ||
|
|
8773f3158f | ||
|
|
7e37c39485 | ||
|
|
14c17cafa1 | ||
|
|
8696a7fd13 | ||
|
|
bb4b8c57b9 | ||
|
|
d63cfc3f0f | ||
|
|
f377f44dae | ||
|
|
0b1bb1ac3a | ||
|
|
f208e52a76 | ||
|
|
b091529a3c | ||
|
|
b323a3cbff | ||
|
|
b59623ef43 | ||
|
|
9c163950da | ||
|
|
d357bbd375 | ||
|
|
f542a3d26b | ||
|
|
59a4ff482a | ||
|
|
40ca5b04f4 | ||
|
|
411e5b88c9 | ||
|
|
b4c299bad0 | ||
|
|
ab4bdc913f | ||
|
|
1fe248a51b | ||
|
|
2559b9d017 | ||
|
|
4db43567e8 | ||
|
|
5333842a1d | ||
|
|
98c3806b15 | ||
|
|
b6afc225c8 | ||
|
|
ad30dc1e20 | ||
|
|
ff51983e15 | ||
|
|
fdf01663d1 | ||
|
|
4b94288301 | ||
|
|
4bf99ade15 | ||
|
|
e3e166d8cf | ||
|
|
3da3999612 | ||
|
|
d50116b8ac | ||
|
|
75ed53320e | ||
|
|
17b786ae73 | ||
|
|
dfd42a43c3 | ||
|
|
f7b8dd63f0 | ||
|
|
a8abf124c8 | ||
|
|
176ccefcd8 | ||
|
|
cbd2ffd031 | ||
|
|
0b534d2adc | ||
|
|
526a20bd16 | ||
|
|
51094b1b08 | ||
|
|
f1ac2033ab | ||
|
|
a1b8d815f5 | ||
|
|
8b756bd98e | ||
|
|
46047c58d0 | ||
|
|
374c761e77 | ||
|
|
6c7b26e13f | ||
|
|
b51b108045 | ||
|
|
86e8c89488 | ||
|
|
41c3b34b1f | ||
|
|
47f48f5d85 | ||
|
|
e15e2ef7a0 | ||
|
|
d0c8b279da | ||
|
|
612d83b51d | ||
|
|
9c30efeb7e | ||
|
|
39fa4cc107 | ||
|
|
b09c122373 | ||
|
|
3348243b7b | ||
|
|
b46b65ed37 | ||
|
|
18e4088fad | ||
|
|
5fd6cd64f9 | ||
|
|
3d24bbfbe4 | ||
|
|
1775612512 | ||
|
|
0d2d967cc7 | ||
|
|
a5e52a1fd4 | ||
|
|
291a93bafa | ||
|
|
c4737bea17 | ||
|
|
45dad7ba1b | ||
|
|
db7c9da871 | ||
|
|
bc92621ade | ||
|
|
fd8e559c3a | ||
|
|
222e11d4ae | ||
|
|
7d682f0acb | ||
|
|
8364b6b0b1 | ||
|
|
9f6b517671 | ||
|
|
7ac40e5521 | ||
|
|
36066dd3ee | ||
|
|
636aa83ed3 | ||
|
|
33d152b6cc | ||
|
|
51c4fec0d5 | ||
|
|
0017486dca | ||
|
|
edc70f4aaf | ||
|
|
756926ff00 | ||
|
|
cb160dd531 | ||
|
|
77334ccb44 | ||
|
|
796db21295 | ||
|
|
535d7b681b | ||
|
|
7db2897ded | ||
|
|
960e038886 | ||
|
|
ea14422ff1 | ||
|
|
38d05d17e5 | ||
|
|
db9bd5267f | ||
|
|
ab3b773bbe | ||
|
|
0bc4ee60e0 | ||
|
|
a3ef0e1cdd | ||
|
|
679bacf0b5 | ||
|
|
02e3952f3b | ||
|
|
64b7e89c0c | ||
|
|
bee4c5571a | ||
|
|
96929dd1e8 | ||
|
|
53e06b2507 | ||
|
|
b80d4bebf3 | ||
|
|
55bec9b658 | ||
|
|
2a63b0f110 | ||
|
|
07b88cffce | ||
|
|
58c8451f36 | ||
|
|
3047121c63 | ||
|
|
7079f8ff1f | ||
|
|
2c3b9f3570 | ||
|
|
fad2428f47 | ||
|
|
c3d3110f6a | ||
|
|
79ec00276c | ||
|
|
9c117d345f | ||
|
|
46cc1c65a4 | ||
|
|
71d9fe7818 | ||
|
|
4ccabf93db | ||
|
|
6612a34939 | ||
|
|
e5b4225f7c | ||
|
|
b2ca35ddbc | ||
|
|
76ab842d9b | ||
|
|
78653a33aa | ||
|
|
24dc1ed715 | ||
|
|
682d8dcd21 | ||
|
|
640bb54e73 | ||
|
|
e0977d7686 | ||
|
|
112ab398db | ||
|
|
af93fcfa05 | ||
|
|
62d231c004 | ||
|
|
49358274d7 | ||
|
|
7b1e379ca9 | ||
|
|
22d7368dfb | ||
|
|
24121bc703 | ||
|
|
9fc87fa767 | ||
|
|
328f82d59a | ||
|
|
78717fc328 | ||
|
|
3b35c3425e | ||
|
|
874ae0354e | ||
|
|
4c6b4764f0 | ||
|
|
59ee8a8647 | ||
|
|
af284305d5 | ||
|
|
d53a4af1a4 | ||
|
|
2e1b928540 | ||
|
|
040ac68679 | ||
|
|
049d71d874 | ||
|
|
bf2c8c8f82 | ||
|
|
ef428960c9 | ||
|
|
992fc9d6e1 | ||
|
|
8639f89f51 | ||
|
|
0424ec307b | ||
|
|
ac5a69af45 | ||
|
|
94e8c80473 | ||
|
|
87f0e62d94 | ||
|
|
46b4070f3f | ||
|
|
2a776f9788 | ||
|
|
f4c7ef9862 | ||
|
|
50e12e9df1 | ||
|
|
b7faebbac8 | ||
|
|
4191fdf147 | ||
|
|
9a4f12be98 | ||
|
|
7ad4258add | ||
|
|
9945c4994c | ||
|
|
5faf9fed7e | ||
|
|
13a9b69b09 | ||
|
|
4975650e00 | ||
|
|
0cc7178546 | ||
|
|
41f24c321d | ||
|
|
4b3fbafdd2 | ||
|
|
7ac40086f5 | ||
|
|
313dfc45f5 | ||
|
|
78a55d7a28 | ||
|
|
bb6ac83698 | ||
|
|
9d0e366880 | ||
|
|
9bff48a0e7 | ||
|
|
60121eb514 | ||
|
|
527ca1da4f | ||
|
|
7689413e42 | ||
|
|
ba7a92b0ce | ||
|
|
4c7d816dd7 | ||
|
|
032f2f260f | ||
|
|
20e98bf6c0 | ||
|
|
5c2266df4b | ||
|
|
67dda51722 | ||
|
|
e4c4bcf36f | ||
|
|
82d8a8b6e2 | ||
|
|
13a10d5aa3 | ||
|
|
9022726446 | ||
|
|
94bfcd23b7 | ||
|
|
526b3b0716 | ||
|
|
61f92af1cf | ||
|
|
a72778d364 | ||
|
|
5ae17037a3 | ||
|
|
02f0da20b0 | ||
|
|
b41631c4e6 | ||
|
|
0e49d9a6b0 | ||
|
|
4a7d108ab3 | ||
|
|
3cfd000849 | ||
|
|
1b38185361 | ||
|
|
9cb9a5df77 | ||
|
|
5035536e3f | ||
|
|
3e12bc583a | ||
|
|
e568c2233e | ||
|
|
061a75edd6 | ||
|
|
82c4d7b0ce | ||
|
|
136dadde95 | ||
|
|
0c14841585 | ||
|
|
0eebf34d9d | ||
|
|
cf186b77a7 | ||
|
|
a3372437bf | ||
|
|
4c57b4853d | ||
|
|
38eb2968ab | ||
|
|
bea56c9569 | ||
|
|
7e508ff2cf | ||
|
|
563772eda4 | ||
|
|
0533915aad | ||
|
|
c3a227d1c4 | ||
|
|
f6c903e708 | ||
|
|
7dc011c063 | ||
|
|
4e3b303016 | ||
|
|
7e1f5447e7 | ||
|
|
7e3472758b | ||
|
|
328a22e175 | ||
|
|
417b453699 | ||
|
|
6ea7190a3e | ||
|
|
b54b08c91b | ||
|
|
c30943b1c0 | ||
|
|
2abf7cab80 | ||
|
|
4137196899 | ||
|
|
019839faaa | ||
|
|
f52183a878 | ||
|
|
750b9ff032 | ||
|
|
28602e747c | ||
|
|
6cc37c69e2 | ||
|
|
a5cd0eb8a4 | ||
|
|
c23e266427 | ||
|
|
651acffbe5 | ||
|
|
71bd93b89c | ||
|
|
6da620de58 | ||
|
|
bdceea7afd | ||
|
|
d80a39cec8 | ||
|
|
5b5fae5f20 | ||
|
|
01b06aedcf | ||
|
|
c711383811 | ||
|
|
17cc153435 | ||
|
|
67446fd49b | ||
|
|
325bb615a7 | ||
|
|
ee5cd8418e | ||
|
|
342609a1b4 | ||
|
|
f270cf1a26 | ||
|
|
371c3b796c | ||
|
|
6b7ceee1b9 | ||
|
|
fdb20a27a3 | ||
|
|
2c94198eb6 | ||
|
|
e8110b8125 | ||
|
|
c39fd7b1ca | ||
|
|
a9c09a7c62 | ||
|
|
82beaabb41 | ||
|
|
63b4295d20 | ||
|
|
312a3f389b | ||
|
|
609af1ae1c | ||
|
|
4cd759f73d | ||
|
|
e156e70281 | ||
|
|
9b464929fe | ||
|
|
0c176d7bde | ||
|
|
7a3f0c00ad | ||
|
|
7aefc49c40 | ||
|
|
741dd8ea65 | ||
|
|
76adc82068 | ||
|
|
bd1512d196 | ||
|
|
9a4acbfaf5 | ||
|
|
ad1f4e7902 | ||
|
|
b328295910 | ||
|
|
828b2a5cd9 | ||
|
|
2ff7cbeaaa | ||
|
|
b2f7738830 | ||
|
|
dc0279532a | ||
|
|
0c59d02bdc | ||
|
|
0f72beb515 | ||
|
|
d781e29316 | ||
|
|
3b3e8ed332 | ||
|
|
dcdfeb33d2 | ||
|
|
0d85c3a732 | ||
|
|
903d136942 | ||
|
|
9d584da7d0 | ||
|
|
31752f76f7 | ||
|
|
5f1b2aea80 | ||
|
|
4479600d57 | ||
|
|
a90189c3ad | ||
|
|
d8a1caf04f | ||
|
|
cb33d389ed | ||
|
|
967e0955f0 | ||
|
|
e01b432ad3 | ||
|
|
fd91257c40 | ||
|
|
c7b959ce38 | ||
|
|
75eac8961e | ||
|
|
3b7d9aa487 | ||
|
|
1f4b722b00 | ||
|
|
f6519f89b0 | ||
|
|
24af85298e | ||
|
|
e721d857c2 | ||
|
|
5c17f0a67a | ||
|
|
4fcaa4f4a5 | ||
|
|
536f819eda | ||
|
|
a662489877 | ||
|
|
a2973eb597 | ||
|
|
4e21b3a94f | ||
|
|
b703ebeeaf | ||
|
|
b84a5f0337 | ||
|
|
a1ec9a7553 | ||
|
|
91d644b5ba | ||
|
|
5d6c3d6a66 | ||
|
|
1ebb4717df | ||
|
|
cf5881fc4d | ||
|
|
fcd817a326 | ||
|
|
031ec536f0 | ||
|
|
668db403f9 | ||
|
|
b9ad101926 | ||
|
|
435911029f | ||
|
|
699ed30cee | ||
|
|
9eab37dca0 | ||
|
|
9a8a12b7d8 | ||
|
|
a4c2ab35c1 | ||
|
|
3d9c4bf09a | ||
|
|
8b8a39e279 | ||
|
|
82393e2bb2 | ||
|
|
2eb99a4b98 | ||
|
|
6abce58a12 | ||
|
|
990e6e8fa3 | ||
|
|
bfd88516eb | ||
|
|
d8b7e80d29 | ||
|
|
37120974dc | ||
|
|
42fc93c709 | ||
|
|
a625e56543 | ||
|
|
9b738b2caa | ||
|
|
90bb5667bf | ||
|
|
d3d3e2e3aa | ||
|
|
37ca7b22b5 | ||
|
|
50f84a9ae1 | ||
|
|
ff29bf81f8 | ||
|
|
b25f753397 | ||
|
|
6a5d6de1e3 | ||
|
|
1c31a5b0e0 | ||
|
|
4f5cdf7c9b | ||
|
|
f09a767d31 | ||
|
|
cc8034cc4c | ||
|
|
50506cb607 | ||
|
|
aa8d2d5be6 | ||
|
|
114e6025b0 | ||
|
|
fda2717ef9 | ||
|
|
937511dfc0 | ||
|
|
d5c181a14e | ||
|
|
e8ce2375e0 | ||
|
|
6fdb39ded1 | ||
|
|
8e3a2bd620 | ||
|
|
a06bf87a2c | ||
|
|
ee4337d100 | ||
|
|
cff551c0b0 | ||
|
|
63b728f06f | ||
|
|
3793090b1b | ||
|
|
6d02b9a392 | ||
|
|
2c740cf28d | ||
|
|
5214f1e31d | ||
|
|
5d0f84d32c | ||
|
|
ee223abb88 | ||
|
|
21d0c33ecd | ||
|
|
8b6d9406db | ||
|
|
686f98816e | ||
|
|
0fa6b17dcc | ||
|
|
472404953a | ||
|
|
ae4ddf9efa | ||
|
|
ea8ed40b2f | ||
|
|
71bb016160 | ||
|
|
179ffab69c | ||
|
|
deb85c32bb | ||
|
|
92366d189e | ||
|
|
81413c0165 | ||
|
|
1e2eb4b40d | ||
|
|
01003d072c | ||
|
|
5003e4283b | ||
|
|
123c781044 | ||
|
|
a641b24592 | ||
|
|
e68dd1921a | ||
|
|
6953d8e95a | ||
|
|
967c9076a3 | ||
|
|
b3613d36da | ||
|
|
53472df857 | ||
|
|
2549e113b8 | ||
|
|
b15c44cd36 | ||
|
|
f93ded9852 | ||
|
|
89ea063eeb | ||
|
|
44b2264fea | ||
|
|
cb5a470635 | ||
|
|
17d1900581 | ||
|
|
5d501a0901 | ||
|
|
c13722480b | ||
|
|
e7d34c03f2 | ||
|
|
264cd00fff | ||
|
|
a4a6b7b80f | ||
|
|
aebb42d32b | ||
|
|
b4ef6a0038 | ||
|
|
5d235ca7f6 | ||
|
|
c3459d24f1 | ||
|
|
e3778cce0e | ||
|
|
ad607563a2 | ||
|
|
236cb2131b | ||
|
|
66d041f250 | ||
|
|
f3cb54e6d9 | ||
|
|
0aeb9a106e | ||
|
|
fd8102820c | ||
|
|
bfdf891fd3 | ||
|
|
3fa3ff1bc3 | ||
|
|
0a0110fc6b | ||
|
|
852fad922f | ||
|
|
fc68d52bb9 | ||
|
|
dde9fe9788 | ||
|
|
a230068ff7 | ||
|
|
6a75040278 | ||
|
|
c514b0ec65 | ||
|
|
eb97f46e8b | ||
|
|
c90d16cf36 | ||
|
|
ab6ca04802 | ||
|
|
f3003531a5 | ||
|
|
146672254e | ||
|
|
999079b454 | ||
|
|
02fb980451 | ||
|
|
8a06999ba0 | ||
|
|
80dcee5cd5 | ||
|
|
9550ca506f | ||
|
|
30eecc6a04 | ||
|
|
dbd82a1d4f | ||
|
|
76f0c50d3d | ||
|
|
dc519b5421 | ||
|
|
ae12bc3ebb | ||
|
|
e327b736ca | ||
|
|
82b69a5cbb | ||
|
|
11465da702 | ||
|
|
578c074575 | ||
|
|
8cdb5c8453 | ||
|
|
2b1b2d83ca | ||
|
|
c3040bd00a | ||
|
|
8c1aa28c27 | ||
|
|
50b9dd7344 | ||
|
|
78d7ee19dc | ||
|
|
892015b088 | ||
|
|
47f2d01a5a | ||
|
|
33a513faf7 | ||
|
|
720334659a | ||
|
|
240384afe6 | ||
|
|
6722ebd437 | ||
|
|
957e0db1d2 | ||
|
|
804afc5871 | ||
|
|
00d24327ef | ||
|
|
9a605c8859 | ||
|
|
402ca40c9d | ||
|
|
30bd1c16c8 | ||
|
|
721f5a277c | ||
|
|
6fb8ace671 | ||
|
|
ae37338e68 | ||
|
|
03c2c162f9 | ||
|
|
52c3a6e49d | ||
|
|
4e16c1f80b | ||
|
|
7ccb2b84dd | ||
|
|
0a192fbea7 | ||
|
|
a526167d40 | ||
|
|
f78546272c | ||
|
|
c137cc0d33 | ||
|
|
6e4b8b2891 | ||
|
|
5dadae079b | ||
|
|
cd08d806b1 | ||
|
|
5f9f87c06f | ||
|
|
387db16a78 | ||
|
|
36e6f62cd0 | ||
|
|
755ff8d22c | ||
|
|
7b3a19e533 | ||
|
|
4f13f8f798 | ||
|
|
feb7711cf5 | ||
|
|
589c33dade | ||
|
|
e572a1010b | ||
|
|
7e0dc61334 | ||
|
|
8e82ecfe8f | ||
|
|
ec29539e06 | ||
|
|
8cd9614abf | ||
|
|
324ac0a243 | ||
|
|
3711304510 | ||
|
|
50b936936d | ||
|
|
d97da29da2 | ||
|
|
7687b354c5 | ||
|
|
36d7281037 | ||
|
|
865d1fbafc | ||
|
|
ac21e71968 | ||
|
|
943a1e24b8 | ||
|
|
50f01302d3 | ||
|
|
0198807ef9 | ||
|
|
6856139705 | ||
|
|
c93153852f | ||
|
|
ab9c7214ee | ||
|
|
dae69640d0 | ||
|
|
edeb3e7cb1 | ||
|
|
5c43afd40f | ||
|
|
9170ca5b16 | ||
|
|
65d49afa48 | ||
|
|
ab03c0b47c | ||
|
|
7690787553 | ||
|
|
a65402ef42 | ||
|
|
7033bc1a51 | ||
|
|
89d5fbf354 | ||
|
|
8c3533ba97 | ||
|
|
44d6dd08b2 | ||
|
|
cc449417c4 | ||
|
|
497f5fd93f | ||
|
|
7308b8cb3d | ||
|
|
4211c83aa4 | ||
|
|
d01949dc89 | ||
|
|
63a6494834 | ||
|
|
8bea039b83 | ||
|
|
d65889bbc0 | ||
|
|
4a8963770e | ||
|
|
5b0aa2c7b1 | ||
|
|
b6aa99aff8 | ||
|
|
4bf5614195 | ||
|
|
0be30bafa4 | ||
|
|
7b091c370c | ||
|
|
334b5c3b72 | ||
|
|
b7cedb1604 | ||
|
|
2038ad6ee7 | ||
|
|
b243340f0c | ||
|
|
8cc83d301d | ||
|
|
d762f86e94 | ||
|
|
264b23e1a4 | ||
|
|
a6e0afa2bb | ||
|
|
4285a47f40 | ||
|
|
e36963e0eb | ||
|
|
dedd35c6bc | ||
|
|
608945d44a | ||
|
|
b1bf063503 | ||
|
|
14bddf35fb | ||
|
|
ef6c868f23 | ||
|
|
6682049dee | ||
|
|
b0f001a6cb | ||
|
|
dd67702a3e | ||
|
|
05a3879f1c | ||
|
|
4a7b790384 | ||
|
|
09ff81316e | ||
|
|
c88aec845a | ||
|
|
77a54b6a65 | ||
|
|
575036b405 | ||
|
|
f6dfd6603a | ||
|
|
e04edad621 | ||
|
|
f322bfb063 | ||
|
|
014e880372 | ||
|
|
01d22d4703 | ||
|
|
48aae2d2cf | ||
|
|
c571dea953 | ||
|
|
8b172c2e10 | ||
|
|
0a67a3632b | ||
|
|
985e4fdc07 | ||
|
|
1e399778ee | ||
|
|
2e022397c4 | ||
|
|
02835c6bf4 | ||
|
|
91816e8f16 | ||
|
|
10c38c7ca2 | ||
|
|
94a773feb9 | ||
|
|
448ef1f31c | ||
|
|
49941c4e4f | ||
|
|
80f48920c8 | ||
|
|
520e753390 | ||
|
|
355c7ad361 | ||
|
|
5a11b793fe | ||
|
|
7593fbaa12 | ||
|
|
2eb0f72a0e | ||
|
|
8e5b121948 | ||
|
|
648e6a1ffe | ||
|
|
55af2b26e0 | ||
|
|
583882fdce | ||
|
|
9eb31b265f | ||
|
|
ddeb1b3de2 | ||
|
|
59fe4824f8 | ||
|
|
dd8417526b | ||
|
|
09670d5ba4 | ||
|
|
41a7b00f18 | ||
|
|
d90e40305b | ||
|
|
350c948133 | ||
|
|
e5e9966199 | ||
|
|
fbd9f6ea80 | ||
|
|
6df7179e6c | ||
|
|
36eb802baf | ||
|
|
2ccb37beb9 | ||
|
|
246ce10858 | ||
|
|
ba717dca97 | ||
|
|
1e52776ac3 | ||
|
|
8daeeedc06 | ||
|
|
cce9d15d01 | ||
|
|
dd414c970b | ||
|
|
6744f36db7 | ||
|
|
77302fe5c9 | ||
|
|
497ca088a6 | ||
|
|
90bddb6cdd | ||
|
|
fafc7950e2 | ||
|
|
a13d06de42 | ||
|
|
1db82381e3 | ||
|
|
cb8961eeed | ||
|
|
af98f8ff37 | ||
|
|
caf80631f0 | ||
|
|
1812afb7b3 | ||
|
|
9fb66c780c | ||
|
|
ab953c64a0 | ||
|
|
db0a8ad979 | ||
|
|
1c29e81e62 | ||
|
|
7a6d76a64d | ||
|
|
4aa353673b | ||
|
|
f49b3d6efc | ||
|
|
36bb63fad1 | ||
|
|
1f36085df9 | ||
|
|
26669ea3cf | ||
|
|
db8e38b8cf | ||
|
|
e09f58b3bc | ||
|
|
3dc582e5ea | ||
|
|
506e261d20 | ||
|
|
b30c4992a9 | ||
|
|
3eeff489e8 | ||
|
|
5946cda7c6 | ||
|
|
ee2edd838a | ||
|
|
73e732eb6b | ||
|
|
cd7364a89c | ||
|
|
57d1db8dd0 | ||
|
|
964e7b2dd0 | ||
|
|
f101079ae0 | ||
|
|
0f61db4469 | ||
|
|
1bd3903582 | ||
|
|
da4daed5ef | ||
|
|
7d49502ab0 | ||
|
|
03e3b4e119 | ||
|
|
d8348c351d | ||
|
|
70cb4d51c9 | ||
|
|
7c84562945 | ||
|
|
68f3b61f0e | ||
|
|
08fd433f3e | ||
|
|
35a3ff1d33 | ||
|
|
fff496c689 | ||
|
|
e5c209a1bc | ||
|
|
75f105d455 | ||
|
|
00cde0b8dc | ||
|
|
58cd7e173e | ||
|
|
f4076bb736 | ||
|
|
6f7893653c | ||
|
|
55ebae26f9 | ||
|
|
ae8bdfd1a1 | ||
|
|
6a74719060 | ||
|
|
f6295bcb04 | ||
|
|
78f9d84318 | ||
|
|
b1ec70e4a9 | ||
|
|
c936d8cc7b | ||
|
|
e6174ee975 | ||
|
|
f790c43f6e | ||
|
|
8119597d6f | ||
|
|
43abd79950 | ||
|
|
97ae4d166c | ||
|
|
87cc0fbd18 | ||
|
|
baf39a1aa8 | ||
|
|
975977860d | ||
|
|
b5d48cb1ef | ||
|
|
de66571371 | ||
|
|
dab062fb6e | ||
|
|
6a959f2e52 | ||
|
|
47c165c3a9 | ||
|
|
4180a3d8b7 | ||
|
|
05ad5409b4 | ||
|
|
1ef1563649 | ||
|
|
ef47b2c15f | ||
|
|
57935b2564 | ||
|
|
54a5428518 | ||
|
|
9d5fb3b58d | ||
|
|
cbb7287204 | ||
|
|
0553d0ee40 | ||
|
|
7faf7e7523 | ||
|
|
4356d907c1 | ||
|
|
c677e49bd1 | ||
|
|
96c4855331 | ||
|
|
b90b0c4ffa | ||
|
|
c725e2c8b0 | ||
|
|
db7a28eccb | ||
|
|
6a11bb77ba | ||
|
|
181f814e57 | ||
|
|
bd5376c182 | ||
|
|
ecf6de5b02 | ||
|
|
139f27827e | ||
|
|
945e5c56e3 | ||
|
|
fc10824cb6 | ||
|
|
83a5668694 | ||
|
|
f648e682a7 | ||
|
|
f2dbc54066 | ||
|
|
86be82610c | ||
|
|
4810c48d6d | ||
|
|
c4af7684d8 | ||
|
|
fcc2546269 | ||
|
|
40fbb05e1c | ||
|
|
dc5756fd77 | ||
|
|
41db733308 | ||
|
|
0bf219889e | ||
|
|
f2a7ed77ef | ||
|
|
4853eb63fe | ||
|
|
5820c4a29e | ||
|
|
7fd4ed9939 | ||
|
|
88c86d211b | ||
|
|
5d84b79a30 | ||
|
|
140ac73965 | ||
|
|
2a27e66234 | ||
|
|
e759a00119 | ||
|
|
9d5332518c | ||
|
|
90ab741e90 | ||
|
|
96229998c2 | ||
|
|
0659dfccfe | ||
|
|
9c544e2537 | ||
|
|
d7fc56318b | ||
|
|
4bba371644 | ||
|
|
ef5acfe32d | ||
|
|
30787f7259 | ||
|
|
85557f635a | ||
|
|
60d23e5e59 | ||
|
|
97d5bfcba6 | ||
|
|
c233e6bcc3 | ||
|
|
28809ab07a | ||
|
|
bad84757eb | ||
|
|
13118a50b8 | ||
|
|
5495937f46 | ||
|
|
adccf33632 | ||
|
|
b203095d4c | ||
|
|
f3b098fb90 | ||
|
|
af17794c65 | ||
|
|
8fc226ef99 | ||
|
|
3bb3f04108 | ||
|
|
59a9efe85b | ||
|
|
0facd2af3e | ||
|
|
7d0ada5ff9 | ||
|
|
44451f22d5 | ||
|
|
06c6efa970 | ||
|
|
e5851b963a | ||
|
|
4de6131090 | ||
|
|
3a1341a7bc | ||
|
|
c78e48177c | ||
|
|
6edaa0e25b | ||
|
|
fb97809e64 | ||
|
|
0c996b9f48 | ||
|
|
acfb717a18 | ||
|
|
647eab4541 | ||
|
|
1e5bcdec02 | ||
|
|
e7d8e98a9f | ||
|
|
2b3f951a2e | ||
|
|
6751a1284d | ||
|
|
b83831df1f | ||
|
|
f540b93706 | ||
|
|
8466336104 | ||
|
|
f88f1b40ce | ||
|
|
386a7b52d5 | ||
|
|
2e885de796 | ||
|
|
687c04cbb8 | ||
|
|
40c931de4b | ||
|
|
93bc7ef165 | ||
|
|
ee2d190253 | ||
|
|
aedb930cfc | ||
|
|
c596ce91cd | ||
|
|
c01e1a96aa | ||
|
|
8a64969404 | ||
|
|
c254f75bbb | ||
|
|
86692c019c | ||
|
|
1ab1c4ef57 | ||
|
|
926fb62eec | ||
|
|
817690ff73 | ||
|
|
98e1c935a1 | ||
|
|
f30e9976d6 | ||
|
|
80e98aed69 | ||
|
|
6a24cb3d22 | ||
|
|
e13b9e7885 | ||
|
|
dd467d33d0 | ||
|
|
c6b8f4d0c9 | ||
|
|
95240b8093 | ||
|
|
2f962d0a91 | ||
|
|
3c63e1bb57 | ||
|
|
c471b34575 | ||
|
|
d045f0bdb7 | ||
|
|
22becac4bd | ||
|
|
9d632b1b27 | ||
|
|
95c5e10103 | ||
|
|
a5d09d684e | ||
|
|
8aab976bbd | ||
|
|
26c6d1922e | ||
|
|
cd1bb54990 | ||
|
|
d4cd06138c | ||
|
|
961c5cbf17 | ||
|
|
b65e5bb72f | ||
|
|
54914380c0 | ||
|
|
26ccc68bed | ||
|
|
ee3d5a6d47 | ||
|
|
46fde8a1a2 | ||
|
|
fe1d858e35 | ||
|
|
fc42bc6ec9 | ||
|
|
fe6ad195ae | ||
|
|
7193650641 | ||
|
|
5db34f680f | ||
|
|
a82ba8d0ce | ||
|
|
3706fb5dc8 | ||
|
|
08bea4adde | ||
|
|
4c917d0314 | ||
|
|
4866b72eb2 | ||
|
|
2d00be0477 | ||
|
|
3d09aa4c82 | ||
|
|
c44c7895b8 | ||
|
|
8de28761c4 | ||
|
|
711762f0b7 | ||
|
|
5773803961 | ||
|
|
6aeba407db | ||
|
|
140359fc2c | ||
|
|
8ddf48d59f | ||
|
|
2e40a12225 | ||
|
|
dade7245af | ||
|
|
1f9fb20fcd | ||
|
|
0940c5b4c6 | ||
|
|
42ca72dff3 | ||
|
|
2949a6cda9 | ||
|
|
882fc9052e | ||
|
|
9b166fc1f8 | ||
|
|
d4364f30bd | ||
|
|
857421024d | ||
|
|
80faa7a152 | ||
|
|
545a23f11b | ||
|
|
caedb0721e | ||
|
|
47024eb564 | ||
|
|
9c58885c70 | ||
|
|
9fbd4b35a2 | ||
|
|
05b476a270 | ||
|
|
4395ca2e04 | ||
|
|
19f93d906e | ||
|
|
57565375c8 | ||
|
|
eb11cbe867 | ||
|
|
53407e3f38 | ||
|
|
b306c439d7 | ||
|
|
f102819463 | ||
|
|
b942db3dc3 | ||
|
|
78f9fb902b | ||
|
|
d8fef8faac | ||
|
|
8ea6bd2802 | ||
|
|
c659022b5c | ||
|
|
8ca2e93e1a | ||
|
|
5600e214c3 | ||
|
|
6400f8ec0f | ||
|
|
c3a4e2ec40 | ||
|
|
e28c794699 | ||
|
|
da9f180835 | ||
|
|
6b8ce312e3 | ||
|
|
de3fc356e1 | ||
|
|
d0fed4ac02 | ||
|
|
7ce50a355c | ||
|
|
9612f23399 | ||
|
|
cccedc1aa4 | ||
|
|
c430802e32 | ||
|
|
cb4e421901 | ||
|
|
8e97596b7b | ||
|
|
92085e7099 | ||
|
|
c6aa838b51 | ||
|
|
9f5e8d16b3 | ||
|
|
82c06a40ac | ||
|
|
4423eba49b | ||
|
|
5b4c54631a | ||
|
|
5a1a2e9454 | ||
|
|
f005f96ea5 | ||
|
|
5e39123b3b | ||
|
|
393ca8c94d | ||
|
|
f817adc468 | ||
|
|
6c91a5a7f5 | ||
|
|
749b09616d | ||
|
|
5de5ab89b4 | ||
|
|
1d67c96640 | ||
|
|
d1c694ea4a | ||
|
|
06368a232a | ||
|
|
8a7bbd1606 | ||
|
|
131d05033b | ||
|
|
1806a75415 | ||
|
|
659ffe204c | ||
|
|
4647fd8910 | ||
|
|
d492dad8f4 | ||
|
|
3368d70dce | ||
|
|
0e1b2566ff | ||
|
|
369e60162e | ||
|
|
d5e7657fe2 | ||
|
|
f84ce1ebaf | ||
|
|
12bc242944 | ||
|
|
88060cce10 | ||
|
|
272e4db5c7 | ||
|
|
6e21cc3b67 | ||
|
|
0391bc8176 | ||
|
|
3b9264a049 | ||
|
|
2b3c254678 | ||
|
|
287be8c615 | ||
|
|
953fed280f | ||
|
|
e2ff3df314 | ||
|
|
31208a07c2 | ||
|
|
ac7a1b0dfb | ||
|
|
c246773599 | ||
|
|
25cd56a715 | ||
|
|
82c18e2a53 | ||
|
|
d5d38d16ae | ||
|
|
e1cbf33573 | ||
|
|
2ffe3bc14b | ||
|
|
d5867276a9 | ||
|
|
f665ef8fc5 | ||
|
|
b264c21302 | ||
|
|
349b3a2ea0 | ||
|
|
87813a8570 | ||
|
|
aab135516b | ||
|
|
141ba36996 | ||
|
|
d434ca5448 | ||
|
|
94e507aea7 | ||
|
|
3ebc121293 | ||
|
|
41ebd6530b | ||
|
|
2ec7b7b79b | ||
|
|
60ed60353b | ||
|
|
586f1cc532 | ||
|
|
73eb13dfc7 | ||
|
|
ed1269000f | ||
|
|
689fb748ee | ||
|
|
1721fef28b | ||
|
|
364ca0582e | ||
|
|
133a2b4ac2 | ||
|
|
d85187eb74 | ||
|
|
cc1ac11017 | ||
|
|
73f536439e | ||
|
|
b17e7d9a9b | ||
|
|
2f29b758e0 | ||
|
|
482aa3fecc | ||
|
|
d9c19db340 | ||
|
|
6c4d243de5 | ||
|
|
d1561ef777 | ||
|
|
7b4137c351 | ||
|
|
1072336249 | ||
|
|
75bb5c7028 | ||
|
|
376e1ad081 | ||
|
|
b58a22b963 | ||
|
|
47004d9579 | ||
|
|
12810c9cd3 | ||
|
|
7a459170fa | ||
|
|
3cf0df568a | ||
|
|
b88ebd472e | ||
|
|
436416afe2 | ||
|
|
64997815c4 | ||
|
|
8b55cadc83 | ||
|
|
3ecc527209 | ||
|
|
b1b7d1ffba | ||
|
|
4003bd82b0 | ||
|
|
8801255d7d | ||
|
|
3b18f539a7 | ||
|
|
c67a055d16 | ||
|
|
bc973e06d0 | ||
|
|
aeb3c8a0e8 | ||
|
|
cf33a47df0 | ||
|
|
daeb0f04cd | ||
|
|
97243fe395 | ||
|
|
9dbdb65abe | ||
|
|
9af461de35 | ||
|
|
4d71e200c6 | ||
|
|
8e0bdabed2 | ||
|
|
bca553caac | ||
|
|
a2f42a3baf | ||
|
|
7465222a9c | ||
|
|
e28034c5ac | ||
|
|
12bbd32ad0 | ||
|
|
266e466ee4 | ||
|
|
cf83f532ae | ||
|
|
cd019668dc | ||
|
|
515fc8776f | ||
|
|
c7c0996d8c | ||
|
|
b3e64671cc | ||
|
|
4abe214499 | ||
|
|
e94cb5ae7e | ||
|
|
e213c98df1 | ||
|
|
1639282434 | ||
|
|
be0e5dbd83 | ||
|
|
ad72917274 | ||
|
|
6a3f4c3f82 | ||
|
|
a6420bf50c | ||
|
|
eb387896e9 | ||
|
|
f43c163158 | ||
|
|
673bf566fc | ||
|
|
f95c5e1218 | ||
|
|
f33f32f159 | ||
|
|
8df5ae15d1 | ||
|
|
75b399f455 | ||
|
|
12439dd5ec | ||
|
|
3513d41436 | ||
|
|
cab792abe5 | ||
|
|
8870358b1b | ||
|
|
ee087c79ad | ||
|
|
51f579b635 | ||
|
|
c23c3d7d7d | ||
|
|
4abf617b9c | ||
|
|
486375154c | ||
|
|
3026164b16 | ||
|
|
9dd73ef4a4 | ||
|
|
75c72a1e67 | ||
|
|
08354db47b | ||
|
|
027eb5a6b0 | ||
|
|
f71264490c | ||
|
|
6270239a6d | ||
|
|
1195a38f46 | ||
|
|
66e289bab4 | ||
|
|
52c6f26cab | ||
|
|
dc534b674f | ||
|
|
f30c2e8e98 | ||
|
|
c482b3c69a | ||
|
|
266b0ad676 | ||
|
|
87f70ab39d | ||
|
|
8e636da499 | ||
|
|
22889ab175 | ||
|
|
5d2354f177 | ||
|
|
a41fb80ce1 | ||
|
|
2e2575e213 | ||
|
|
8e2898edf9 | ||
|
|
26c61e0809 | ||
|
|
e7a8c3032d | ||
|
|
b477da2094 | ||
|
|
725d1c58aa | ||
|
|
bd6742137f | ||
|
|
9f4921bfa0 | ||
|
|
e8dcfa3d69 | ||
|
|
88720ed09b | ||
|
|
1e804244d0 | ||
|
|
198492bbf0 | ||
|
|
8f9d522f62 | ||
|
|
cbae233aba | ||
|
|
b17ca9c945 | ||
|
|
ebf4ca39ba | ||
|
|
e5e78797e6 | ||
|
|
080997b808 | ||
|
|
77306e8b97 | ||
|
|
6917d2a2f0 | ||
|
|
36c15522c1 | ||
|
|
804c343a4f | ||
|
|
cd5d75427e | ||
|
|
5ddc127da6 | ||
|
|
f859695b49 | ||
|
|
cb3d2eb9e9 | ||
|
|
33eae08f04 | ||
|
|
aa3f98677d | ||
|
|
fffccaaf41 | ||
|
|
cdc8d0c373 | ||
|
|
d14f0c45fc | ||
|
|
39955b0451 | ||
|
|
52dfb7ffe2 | ||
|
|
93462856e1 | ||
|
|
615f155a3a | ||
|
|
fcd9e423ec | ||
|
|
db8f2bfd99 | ||
|
|
55801fc76e | ||
|
|
d3d89c3256 | ||
|
|
8875b3d572 | ||
|
|
aabc2be693 | ||
|
|
c9afb51cea | ||
|
|
c0a656876c | ||
|
|
17a647630b | ||
|
|
c88e118b3c | ||
|
|
ae6a802106 | ||
|
|
b184f94413 | ||
|
|
ee3ec091f4 | ||
|
|
ef49b59053 | ||
|
|
1f8125805e | ||
|
|
efd712c69b | ||
|
|
109a4156e1 | ||
|
|
678d33295b | ||
|
|
5e58956d0a | ||
|
|
e276fd2cb3 | ||
|
|
9b22cb10c4 | ||
|
|
8ca31a0e05 | ||
|
|
20149a5da1 | ||
|
|
054d43bb11 | ||
|
|
65488b820c | ||
|
|
c3c9f87954 | ||
|
|
56f447be9f | ||
|
|
79fa9db0da | ||
|
|
071c10137b | ||
|
|
a4962b80d6 | ||
|
|
5307c33232 | ||
|
|
1b660cce12 | ||
|
|
8df8c278b6 | ||
|
|
d7e8264517 | ||
|
|
f11c316347 | ||
|
|
f62e02c24f | ||
|
|
70113c38c9 | ||
|
|
3d8132f5e2 | ||
|
|
39affb5aa4 | ||
|
|
a882c5f474 | ||
|
|
61a7ff1622 | ||
|
|
42e7373bd3 | ||
|
|
e269d3ae7d | ||
|
|
e7ddaef5bd | ||
|
|
62984e4584 | ||
|
|
3c53455d15 | ||
|
|
bbb43a39fd | ||
|
|
43e7d3c945 | ||
|
|
2f72e83bbd | ||
|
|
57179b4ca1 | ||
|
|
4bc8eec4eb | ||
|
|
baf510bf8c | ||
|
|
6d53cdd6ce | ||
|
|
ebbf078c7d | ||
|
|
95e431e9ec | ||
|
|
eba470f2f2 | ||
|
|
11addc50ff | ||
|
|
e4df2f98cc | ||
|
|
e7c14660d3 | ||
|
|
90076b6172 | ||
|
|
28b83495d8 | ||
|
|
551c7837ac | ||
|
|
59e6acc757 | ||
|
|
9990c960f2 | ||
|
|
2006a06eff | ||
|
|
2b6bda1ed8 | ||
|
|
468083d2f5 | ||
|
|
483fc223bb | ||
|
|
66ce97024d | ||
|
|
8c97f81943 | ||
|
|
d7c1630570 | ||
|
|
5e1a5ac8de | ||
|
|
9eb4ab6ad9 | ||
|
|
4932a817a0 | ||
|
|
5d003e29b1 | ||
|
|
dc95bd503e | ||
|
|
f738dd7b7c | ||
|
|
f908b74fa3 | ||
|
|
c687ac745b | ||
|
|
912e0b7e46 | ||
|
|
03bc7237ad | ||
|
|
061f62da54 | ||
|
|
dd565ac1ad | ||
|
|
5cdefc4625 | ||
|
|
ce00af8767 | ||
|
|
51047444aa | ||
|
|
aa6cd05ed8 | ||
|
|
dac14bf311 | ||
|
|
05fe2594e4 | ||
|
|
26e1c3514f | ||
|
|
22c83245c5 | ||
|
|
7900aede14 | ||
|
|
f877c6ae5a | ||
|
|
ca681f7041 | ||
|
|
a01da8bbf8 | ||
|
|
f3a65d9636 | ||
|
|
559f4c550f | ||
|
|
03c635a4b5 | ||
|
|
34a4cd0a34 | ||
|
|
3b9b32f404 | ||
|
|
9c724a9802 | ||
|
|
7a6e8a1b17 | ||
|
|
369c12e038 | ||
|
|
0fa5795b85 | ||
|
|
c00c7c0af0 | ||
|
|
cbaed4bb5e | ||
|
|
f74a7348f6 | ||
|
|
8626b23e4e | ||
|
|
0086874277 | ||
|
|
7fc18d9309 | ||
|
|
974f1a385a | ||
|
|
6900b4f6f5 | ||
|
|
d90e4bdb74 | ||
|
|
276c989772 | ||
|
|
ea99110d24 | ||
|
|
221a59fe6f | ||
|
|
eaa5646483 | ||
|
|
041bc3adc5 | ||
|
|
e64b756943 | ||
|
|
201ea3ee8e | ||
|
|
9303ce3e69 | ||
|
|
06c085ab6e | ||
|
|
c576ef1e7c | ||
|
|
11bed5827d | ||
|
|
fab83e2456 | ||
|
|
1d25e9d173 | ||
|
|
9c21f22923 | ||
|
|
3aa697f993 | ||
|
|
8b9848ac56 | ||
|
|
8b8c1093b6 | ||
|
|
d0d6c097fc | ||
|
|
6be5e46994 | ||
|
|
45694b504a | ||
|
|
41dbc50f9c | ||
|
|
4d2ad866f3 | ||
|
|
3cafca04aa | ||
|
|
594f51b859 | ||
|
|
fb56131dd9 | ||
|
|
a34e19629c | ||
|
|
3c12a027d4 | ||
|
|
cb28e03386 | ||
|
|
7393746da2 | ||
|
|
6828c809e4 | ||
|
|
28479149cc | ||
|
|
237c03c8ea | ||
|
|
e73c85cb23 | ||
|
|
b6b2711298 | ||
|
|
3b7130439a | ||
|
|
2c919adb74 | ||
|
|
60231c65b9 | ||
|
|
f196047832 | ||
|
|
240ca32e57 | ||
|
|
fa37c26c4d | ||
|
|
d7dbfc7cc1 | ||
|
|
d9ab5262b1 | ||
|
|
fb124e3741 | ||
|
|
479bf783d2 | ||
|
|
f0f3a6c99d | ||
|
|
f57b7835e2 | ||
|
|
1df3186e0e | ||
|
|
0b7c27828d | ||
|
|
0a19d4ccd6 | ||
|
|
9f3da13860 | ||
|
|
bf812ef714 | ||
|
|
b1ac38fadc | ||
|
|
fb0d12c6cb | ||
|
|
34952f09e1 | ||
|
|
34a7de2970 | ||
|
|
0ff827419e | ||
|
|
b29440aee6 | ||
|
|
11b5605815 | ||
|
|
844587669e | ||
|
|
f6c3664d71 | ||
|
|
c5864a8ce6 | ||
|
|
27c7114af6 | ||
|
|
0791ac1b44 | ||
|
|
1de5cd3ba5 | ||
|
|
729accb482 | ||
|
|
942acef594 | ||
|
|
fb2f339fec | ||
|
|
98044462b1 | ||
|
|
0dcb318f62 | ||
|
|
f32143469f | ||
|
|
3a30508b94 | ||
|
|
e0b9d78fab | ||
|
|
8d6765cf48 | ||
|
|
12bb392a0f | ||
|
|
08df685fe7 | ||
|
|
c8d1be772d | ||
|
|
887e9bc7b5 | ||
|
|
9f2e7c2f34 | ||
|
|
d7bb8884af | ||
|
|
464e792496 | ||
|
|
18c3281f9e | ||
|
|
8e2b1be127 | ||
|
|
b61b7787cb | ||
|
|
b465083f45 | ||
|
|
154655a85a | ||
|
|
59e89e62d7 | ||
|
|
d5d7bdaeb5 | ||
|
|
b2f82948ee | ||
|
|
428e4e4a85 | ||
|
|
1e83741c9a | ||
|
|
621d6a9516 | ||
|
|
3550821fb4 | ||
|
|
5b0c40da24 | ||
|
|
e0ac521438 | ||
|
|
c29458f3ec | ||
|
|
bf94d763ba | ||
|
|
8a37aa1517 | ||
|
|
f3d24df6f2 | ||
|
|
fd5d8270dc | ||
|
|
be612d9e0c | ||
|
|
4a7434d0b0 | ||
|
|
ad2141be2d | ||
|
|
f94639fadf | ||
|
|
89faae660f | ||
|
|
0f422256d6 | ||
|
|
acc1adbe7a | ||
|
|
8002ac9e0a | ||
|
|
6d30cf04db | ||
|
|
430b092a5f | ||
|
|
3eb5fdb581 | ||
|
|
9663bd3abb | ||
|
|
5a4d9ddb21 | ||
|
|
3be3c622dc | ||
|
|
cd6b555e19 | ||
|
|
671302b5c0 | ||
|
|
4f34cdb0a8 | ||
|
|
bd690a9f93 | ||
|
|
51f267d9d4 | ||
|
|
47f53ad958 | ||
|
|
c73cdd800f | ||
|
|
f535ec8278 | ||
|
|
238755752f | ||
|
|
c71a3195af | ||
|
|
54a9328b20 | ||
|
|
3e48522477 | ||
|
|
251a44b776 | ||
|
|
be7a8379b4 | ||
|
|
defce60385 | ||
|
|
354b4b8604 | ||
|
|
5b7dab2dd6 | ||
|
|
8a5601e42f | ||
|
|
232541df44 | ||
|
|
a346b1ff57 | ||
|
|
d96d604e53 | ||
|
|
e704f87f86 | ||
|
|
8f5639afcb | ||
|
|
03950c90f7 | ||
|
|
47a8b7c14a | ||
|
|
2a04d2c799 | ||
|
|
8de922724b | ||
|
|
67b8a28a2f | ||
|
|
51a575159a | ||
|
|
524229a297 | ||
|
|
754e70cf3e | ||
|
|
84bc4dcb0f | ||
|
|
10eaa8ef1d | ||
|
|
c3124c3085 | ||
|
|
8d5b8b477e | ||
|
|
d7d2a9a3db | ||
|
|
25a4c5a9ed | ||
|
|
5c45bbe57b | ||
|
|
d41d04c0f5 | ||
|
|
e422d7f4f7 | ||
|
|
cdc682d5a4 | ||
|
|
9cc93c64aa | ||
|
|
fa7a1cc5ef | ||
|
|
17712eeb19 | ||
|
|
41c3a5a7be | ||
|
|
8765222d22 | ||
|
|
645f814544 | ||
|
|
308cfe0ab3 | ||
|
|
e5e8d20a3a | ||
|
|
a107193e4b | ||
|
|
55eae65b39 | ||
|
|
3f125c8c70 | ||
|
|
75e8b2ac87 | ||
|
|
ee114368ad | ||
|
|
525a87f58e | ||
|
|
44cae2fb2e | ||
|
|
30a453884e | ||
|
|
3b58d94f71 | ||
|
|
8abb86fec4 | ||
|
|
16a089780e | ||
|
|
09b6468d30 | ||
|
|
80fb6d4aa4 | ||
|
|
1f04873517 | ||
|
|
799207e838 | ||
|
|
34866b4836 | ||
|
|
be530dfea2 | ||
|
|
d12a1a47d5 | ||
|
|
8d42e3501e | ||
|
|
2711e41bcd | ||
|
|
5e1eddb939 | ||
|
|
23e7f53bd3 | ||
|
|
000b6b5ae5 | ||
|
|
864f24bd2c | ||
|
|
5d8df28d27 | ||
|
|
f9a5affad9 | ||
|
|
ab81ef8fa7 | ||
|
|
95d8f7ea12 | ||
|
|
5316bf7487 | ||
|
|
a6f774e901 | ||
|
|
f171bc8b59 | ||
|
|
289bbb350e | ||
|
|
d247a2c8bf | ||
|
|
88ed52aec9 | ||
|
|
cb23bcba29 | ||
|
|
2c7ed24796 | ||
|
|
4c6bd5b5b6 | ||
|
|
aeb7b41d44 | ||
|
|
5bdec59de1 | ||
|
|
dfaba1ab95 | ||
|
|
a62fd1af27 | ||
|
|
7a89681722 | ||
|
|
51da40e621 | ||
|
|
d8f0a9ecea | ||
|
|
cf7e015f25 | ||
|
|
1af330f29f | ||
|
|
9afa1770d1 | ||
|
|
3ebbcce1c7 | ||
|
|
2c7c721933 | ||
|
|
7523647391 | ||
|
|
9700cd9097 | ||
|
|
eab7faa0c1 | ||
|
|
a56c1e38c7 | ||
|
|
40a2d17052 | ||
|
|
b14fa8e687 | ||
|
|
678e436f2e | ||
|
|
ff81c4c99c | ||
|
|
420658e6cb | ||
|
|
593ddd851b | ||
|
|
1243402657 | ||
|
|
1a117a7728 | ||
|
|
2b2ee140c3 | ||
|
|
d97f5cd795 | ||
|
|
f3f0b8e403 | ||
|
|
660f9459da | ||
|
|
10952eb2cf | ||
|
|
cdad742700 | ||
|
|
a9e8f60ef6 | ||
|
|
a8b7b26068 | ||
|
|
ba911137fa | ||
|
|
d3f007af18 | ||
|
|
2929fa0e79 | ||
|
|
297a564bee | ||
|
|
8e92d21ebf | ||
|
|
36dbca8784 | ||
|
|
d1cc05e17e | ||
|
|
3b3d531965 | ||
|
|
653789afc7 | ||
|
|
2d651a2d02 | ||
|
|
3e5f3df172 | ||
|
|
f120a7ab5e | ||
|
|
984e4d4875 | ||
|
|
53b8247cb5 | ||
|
|
59db9f8018 | ||
|
|
b73b14f72c | ||
|
|
41597d9bed | ||
|
|
b37317d8b0 | ||
|
|
87dc451108 | ||
|
|
ca4456eda8 | ||
|
|
993df6bc22 | ||
|
|
61be92e26a | ||
|
|
c59b61c0da | ||
|
|
3e214851a4 | ||
|
|
a47b602b08 | ||
|
|
a083b859e4 | ||
|
|
948199deac | ||
|
|
c356620ec1 | ||
|
|
f79ebf09a2 | ||
|
|
c7620992d2 | ||
|
|
ce1bafdce9 | ||
|
|
9872e588c8 | ||
|
|
d609edf4f1 | ||
|
|
3a99d321a8 | ||
|
|
4bb3d999ac | ||
|
|
40101dc311 | ||
|
|
e9c6deffee | ||
|
|
9c29bc69f7 | ||
|
|
1e12429564 | ||
|
|
795704f0f1 | ||
|
|
981b9cdc8c | ||
|
|
3f724339db | ||
|
|
70c857b728 | ||
|
|
9e7e0dffd5 | ||
|
|
c3dea3f878 | ||
|
|
f57f84f606 | ||
|
|
c84683c88b | ||
|
|
b68a2613f8 | ||
|
|
28afa6e77a | ||
|
|
496ce6b349 | ||
|
|
ce9512b78b | ||
|
|
4eb59a6b1c | ||
|
|
80b1ee0a4c | ||
|
|
f993afb26d | ||
|
|
7c80519cbf | ||
|
|
8250c32f49 | ||
|
|
2fe1ff8582 | ||
|
|
17ee98e1a5 | ||
|
|
2ee8f5d80f | ||
|
|
3f302bca8c | ||
|
|
c909e5820e | ||
|
|
a1b85269a4 | ||
|
|
faa1f83ab4 | ||
|
|
308c505c3d | ||
|
|
0eacd2aaae | ||
|
|
18ae46ad4b | ||
|
|
65c2b21df1 | ||
|
|
772acaf31f | ||
|
|
f8d0745e27 | ||
|
|
d719c6a5ab | ||
|
|
769efa16af | ||
|
|
86b4e98ac6 | ||
|
|
3bf8c316a6 | ||
|
|
e37c92ec6d | ||
|
|
a5dd9a0c5d | ||
|
|
7a4a945f13 | ||
|
|
1d18e26eca | ||
|
|
ac4b8df5e4 | ||
|
|
3bc9fb5889 | ||
|
|
632cbb8efa | ||
|
|
789a12aaaf | ||
|
|
ecdbe09e10 | ||
|
|
1dc31c2786 | ||
|
|
32470bf619 | ||
|
|
8b61bfd638 | ||
|
|
8a7a208905 | ||
|
|
0215103e92 | ||
|
|
c2d1be8981 | ||
|
|
4951c9f821 | ||
|
|
726adc43ec | ||
|
|
3c6ae8b59e | ||
|
|
605be3f7f8 | ||
|
|
c51bc70e0f | ||
|
|
e89d7e3029 | ||
|
|
3c07a729a6 | ||
|
|
84c0ed50a5 | ||
|
|
02c126a7c2 | ||
|
|
114ed20e64 | ||
|
|
4b0f45f667 | ||
|
|
36068ae019 | ||
|
|
9d681c2bb3 | ||
|
|
3af1fac7b0 | ||
|
|
761ee0d827 | ||
|
|
fb8bc3f818 | ||
|
|
826a7da808 | ||
|
|
cbd55ade68 | ||
|
|
5705ee6ef8 | ||
|
|
3f5c6d0c1b | ||
|
|
bfed4813b2 | ||
|
|
e58066e244 | ||
|
|
ee48b6a88f | ||
|
|
9ac09ed4de | ||
|
|
22603348aa | ||
|
|
fec73daaa3 | ||
|
|
c6b68648f4 | ||
|
|
1ecb5d1d83 | ||
|
|
dc786d3db5 | ||
|
|
74fe23ec35 | ||
|
|
b0bff54b08 | ||
|
|
1b541d8d6e | ||
|
|
f29ac588dd | ||
|
|
0696667734 | ||
|
|
1793d71db6 | ||
|
|
4211e1941b | ||
|
|
4bdfef5a18 | ||
|
|
8a37f53685 | ||
|
|
4e1ad6e9a8 | ||
|
|
fb10e1aa57 | ||
|
|
3c283a381e | ||
|
|
dac4d5be12 | ||
|
|
530857182d | ||
|
|
9441f77faa | ||
|
|
3cc8b4c327 | ||
|
|
6b19647d57 | ||
|
|
7bd42d0d96 | ||
|
|
c60e8cfaf7 | ||
|
|
7fd002c006 | ||
|
|
db6c50f109 | ||
|
|
aa4789d632 | ||
|
|
ee8de13e14 | ||
|
|
7dde5f6a8d | ||
|
|
736f003f2e | ||
|
|
47af21e8f1 | ||
|
|
605cbef653 | ||
|
|
388ad0c05c | ||
|
|
2ebbb6f1f7 | ||
|
|
d54f1c7477 | ||
|
|
b78f5ec4c3 | ||
|
|
9fd3bf04b7 | ||
|
|
e97bb3de83 | ||
|
|
c2daf8dfa4 | ||
|
|
09b718c439 | ||
|
|
c177bb3a50 | ||
|
|
977a247a06 | ||
|
|
899a3e2f13 | ||
|
|
8ee4ecb48d | ||
|
|
f7e6f7fa23 | ||
|
|
1f80e360fc | ||
|
|
d7011316d0 | ||
|
|
d3671b344f | ||
|
|
a60cccbf9f | ||
|
|
3e72f5f10e | ||
|
|
b94b78971c | ||
|
|
4d08161ac2 | ||
|
|
8954e48140 | ||
|
|
aa99aa4e85 | ||
|
|
d79febcd06 | ||
|
|
13fc7f3a05 | ||
|
|
14309e1ddc | ||
|
|
5513967926 | ||
|
|
eacd875f3b | ||
|
|
c4fe07c7af | ||
|
|
1186e3f91a | ||
|
|
f354385bf5 | ||
|
|
cabe001590 | ||
|
|
89f691e141 | ||
|
|
4a63291144 | ||
|
|
593b77064c | ||
|
|
9fefc88656 | ||
|
|
eb08081330 | ||
|
|
a3bfddfa5e | ||
|
|
36da48798a | ||
|
|
a0f28f90fa | ||
|
|
851229a01f | ||
|
|
c9c854cea7 | ||
|
|
a38436e889 | ||
|
|
23fc384f2c | ||
|
|
1540119723 | ||
|
|
574f42d79a | ||
|
|
536b0700b0 | ||
|
|
5ba761eb85 | ||
|
|
611ac379bb | ||
|
|
03f32a7ead | ||
|
|
50ea2bb20d | ||
|
|
525daedd5a | ||
|
|
e118031ef8 | ||
|
|
45eedbe58c | ||
|
|
e37c932fca | ||
|
|
5eb778bf4d | ||
|
|
ab9b890b52 | ||
|
|
31c746e5dc | ||
|
|
f01f731107 | ||
|
|
70f0f5a8ca | ||
|
|
cc357c4db8 | ||
|
|
97f4aecfc1 | ||
|
|
2af0f87c8b | ||
|
|
b062d94eef | ||
|
|
6c1b0c0ed2 | ||
|
|
ddcdc684e2 | ||
|
|
eae89f92e6 | ||
|
|
01d115b06b | ||
|
|
79057965a8 | ||
|
|
dcd4d95c8e | ||
|
|
cf61d96df0 | ||
|
|
f8da79f828 | ||
|
|
9750e7d70e | ||
|
|
50aa2bb6b9 | ||
|
|
1d1dd597ed | ||
|
|
cfe5537ee5 | ||
|
|
7869eb3fc4 | ||
|
|
6dfa0602f0 | ||
|
|
f870544302 | ||
|
|
75a40b2251 | ||
|
|
28fb109ed0 | ||
|
|
48607afac5 | ||
|
|
b6ea9ef21a | ||
|
|
b8dd44baa9 | ||
|
|
c4f1fde75b | ||
|
|
667170e2c7 | ||
|
|
53429e6551 | ||
|
|
ac8f97f2b3 | ||
|
|
41c0d2f8cb | ||
|
|
1f3a43dbe6 | ||
|
|
369e195a44 | ||
|
|
15006fedb9 | ||
|
|
e35b23f54d | ||
|
|
f72b0a6032 | ||
|
|
ac9ed061ec | ||
|
|
d919fa3344 | ||
|
|
79913fde35 | ||
|
|
da634d0a8b | ||
|
|
fac54cb426 | ||
|
|
3f19b9b7c1 | ||
|
|
dc48695ab9 | ||
|
|
0a31a35098 | ||
|
|
86f2541695 | ||
|
|
181c4ccaaa | ||
|
|
ed848087d5 | ||
|
|
edd66be5be | ||
|
|
246995dbc8 | ||
|
|
b931fbe5ab | ||
|
|
e014ff015d | ||
|
|
4fa5f40232 | ||
|
|
9b15be97aa | ||
|
|
a7ada46bd9 | ||
|
|
9d16788ad9 | ||
|
|
6ce89aecc3 | ||
|
|
963d0ce7e3 | ||
|
|
0f08d7f851 | ||
|
|
44c514eb9c | ||
|
|
513cbdda93 | ||
|
|
e1ba152352 | ||
|
|
446e764500 | ||
|
|
901d00caa6 | ||
|
|
094790d2c9 | ||
|
|
1c0163a5cc | ||
|
|
8fa7e5817a | ||
|
|
01b89d5682 | ||
|
|
9f01c1a803 | ||
|
|
46f0f50016 | ||
|
|
b8070dbbd7 | ||
|
|
3b16d803c9 | ||
|
|
de195c23a6 | ||
|
|
d3b8908886 | ||
|
|
2688176c77 | ||
|
|
a5839317aa | ||
|
|
a0aab26a41 | ||
|
|
27713812a0 | ||
|
|
cf2c5fda4f | ||
|
|
a9684c0dbf | ||
|
|
c0bf5e1c4d | ||
|
|
a31e3e7dcb | ||
|
|
17b41a3337 | ||
|
|
89a683ae74 | ||
|
|
008661069b | ||
|
|
9296e92e1c | ||
|
|
a34af8d066 | ||
|
|
8726e04629 | ||
|
|
2a01c940ec | ||
|
|
4eab60cbd2 | ||
|
|
a0e060ac1e | ||
|
|
397a8ea96e | ||
|
|
15830339ef | ||
|
|
b29280285e | ||
|
|
1633491bff | ||
|
|
2b0fa1f7dd | ||
|
|
02b386f80a | ||
|
|
bf20b9c540 | ||
|
|
06a12933f3 | ||
|
|
6dd94d3a79 | ||
|
|
f2f89c762a | ||
|
|
e6c2d9ad29 | ||
|
|
83423254cc | ||
|
|
1c20ddc966 | ||
|
|
675e9f22ea | ||
|
|
77c6fb5b24 | ||
|
|
082a0140ef | ||
|
|
9e535ce055 | ||
|
|
d76dea001b | ||
|
|
af0f9b0e95 | ||
|
|
e2082ea942 | ||
|
|
68923e52a3 | ||
|
|
9281f6d253 | ||
|
|
4647845679 | ||
|
|
cf9cf7dd04 | ||
|
|
1316b54956 | ||
|
|
cbc1fadd6f | ||
|
|
4dd09c9add | ||
|
|
267dc07e6b | ||
|
|
d7b4d5dd50 | ||
|
|
7f220b2fac | ||
|
|
275c0423aa | ||
|
|
d3ee4bbc5a | ||
|
|
85a064861f | ||
|
|
d0b436bff2 | ||
|
|
92b2f18072 | ||
|
|
dfc4eca21f | ||
|
|
fc7ae675e2 | ||
|
|
804ad79985 | ||
|
|
da839880e9 | ||
|
|
e9d33454b5 | ||
|
|
d80891efc4 | ||
|
|
37c1e4025c | ||
|
|
59a83d3e5b | ||
|
|
13af92fdc4 | ||
|
|
0c20ee7d4b | ||
|
|
89d42c2c75 | ||
|
|
04611765a4 | ||
|
|
9dfc4fa1a1 | ||
|
|
43232d5c14 | ||
|
|
f7c272d4fa | ||
|
|
ede21449c8 | ||
|
|
bb8e553662 | ||
|
|
f5f4a27a96 | ||
|
|
d7c9a3e976 | ||
|
|
35eb649e9d | ||
|
|
e56a4c9e9b | ||
|
|
95506e37af | ||
|
|
e41840c522 | ||
|
|
2a46a27e6c | ||
|
|
0bcdc27653 | ||
|
|
ddf0f74de7 | ||
|
|
91b21b2334 | ||
|
|
66e568de3b | ||
|
|
f5ca97e393 | ||
|
|
8d06a62485 | ||
|
|
93f9420993 | ||
|
|
5b61070c70 | ||
|
|
dbe1a93526 | ||
|
|
aa5d9a79d6 | ||
|
|
86511ea417 | ||
|
|
1866432db7 | ||
|
|
cf2ac6df68 | ||
|
|
33f1f81b8b | ||
|
|
9d0b581fea | ||
|
|
c05724cb18 | ||
|
|
f0714c9f86 | ||
|
|
cf386750c9 | ||
|
|
54f428f645 | ||
|
|
dc2bd20e55 | ||
|
|
c608ee491f | ||
|
|
0130afb76e | ||
|
|
738b926322 | ||
|
|
bea41c7f3f | ||
|
|
1bbe660dfa | ||
|
|
c4bd188da4 | ||
|
|
5acfa126c8 | ||
|
|
67134eaba1 | ||
|
|
5414623791 | ||
|
|
c93d53f5e3 | ||
|
|
507683780e | ||
|
|
e8b9ee5e08 | ||
|
|
d16154d163 | ||
|
|
c342041fba | ||
|
|
bf42a9906d | ||
|
|
9603e8a7d9 | ||
|
|
c7c040b825 | ||
|
|
ac0474f89d | ||
|
|
bb512e57dc | ||
|
|
db652ea186 | ||
|
|
5a9cc19972 | ||
|
|
1a5fd4eebc | ||
|
|
8a1b49ff19 | ||
|
|
b971abe897 | ||
|
|
43b925ce74 | ||
|
|
62b742ece3 | ||
|
|
d16ef949ca | ||
|
|
23e7cba87f | ||
|
|
a8e6f30d8e | ||
|
|
9c49410898 | ||
|
|
802d74aa6b | ||
|
|
71f9e49e67 | ||
|
|
82ea1051b5 | ||
|
|
6c4d20cd6f | ||
|
|
04c27802c0 | ||
|
|
c3b7202f4f | ||
|
|
81103ef35d | ||
|
|
0eb5c1c62a | ||
|
|
a9de951744 | ||
|
|
a42a1bb09d | ||
|
|
9fbfc9bd4d | ||
|
|
242a998bdc | ||
|
|
9d1bf70234 | ||
|
|
b8c1cc1a51 | ||
|
|
eedd20ef96 | ||
|
|
7c197ad96d | ||
|
|
654fd03c73 | ||
|
|
cee16e0fa3 | ||
|
|
73c471e9ef | ||
|
|
533b99fbf9 | ||
|
|
f39eb98bab | ||
|
|
da77d856a1 | ||
|
|
b2575b38e7 | ||
|
|
0a3cf9ad3d | ||
|
|
00334d0de0 | ||
|
|
226b886ca8 | ||
|
|
bc93bdb5bb | ||
|
|
af214c3a79 | ||
|
|
4eb10f6621 | ||
|
|
7d7d469025 | ||
|
|
fd40bdc0be | ||
|
|
7e0480ae0e | ||
|
|
d80265ccd6 | ||
|
|
1b5a1ae257 | ||
|
|
d8d24a922a | ||
|
|
03339b7b5b | ||
|
|
2028c6e03d | ||
|
|
2988835af5 | ||
|
|
62cca96b72 | ||
|
|
b4dea075a3 | ||
|
|
533f67d3fa | ||
|
|
906e2f0eac | ||
|
|
b8091db6b9 | ||
|
|
381c067755 | ||
|
|
2182ab5187 | ||
|
|
a1593a4a0e | ||
|
|
aa5740fb61 | ||
|
|
da92eeae42 | ||
|
|
12e9e8445d | ||
|
|
8084be78c5 | ||
|
|
1ac1c4c26e | ||
|
|
d4f58034f7 | ||
|
|
f843300fe5 | ||
|
|
03b9c94437 | ||
|
|
a219d175c6 | ||
|
|
3c3b4176bd | ||
|
|
022383139b | ||
|
|
1a1251e877 | ||
|
|
18b5e1e534 | ||
|
|
2fece970b8 | ||
|
|
e20d0c1e69 | ||
|
|
a9dcf4a860 | ||
|
|
255f5694aa | ||
|
|
25701d5a2c | ||
|
|
a5158f38a3 | ||
|
|
c76799c555 | ||
|
|
2bb5b6d0a1 | ||
|
|
0bbba43ed0 | ||
|
|
98ca102441 | ||
|
|
3f3308cd75 | ||
|
|
6f96e308d0 | ||
|
|
756f574e4e | ||
|
|
78294e6a9c | ||
|
|
4e33577173 | ||
|
|
607841af64 | ||
|
|
396726244a | ||
|
|
c5895d5dbd | ||
|
|
b407e173e4 | ||
|
|
6a745c2c0f | ||
|
|
2da0cad6ae | ||
|
|
af1fa6234e | ||
|
|
c9ac7fa909 | ||
|
|
964afd0689 | ||
|
|
2a282a3b5f | ||
|
|
7bb23aeca4 | ||
|
|
de939d89eb | ||
|
|
77c975f536 | ||
|
|
75ab0ebcf5 | ||
|
|
10273d6e08 | ||
|
|
16d6973f8a | ||
|
|
edcd2d665b | ||
|
|
385c3e5e91 | ||
|
|
c8e337450b | ||
|
|
10464af5d1 | ||
|
|
cbcd1a5474 | ||
|
|
c9bebed294 | ||
|
|
d5552a3477 | ||
|
|
a8b081a052 | ||
|
|
9e96dc8b35 | ||
|
|
360075e28a | ||
|
|
accf79b107 | ||
|
|
4d58b24c15 | ||
|
|
c33a8639a7 | ||
|
|
25fa8d66e6 | ||
|
|
974a6146fe | ||
|
|
0392ac98d2 | ||
|
|
5e3915cbe3 | ||
|
|
29b809de68 | ||
|
|
8f73e89ca0 | ||
|
|
0d0d5d3717 | ||
|
|
a69e8bfdd9 | ||
|
|
062a3fdf36 | ||
|
|
028a33d7f2 | ||
|
|
14835de9fb | ||
|
|
447053668f | ||
|
|
f3aecb27a4 | ||
|
|
7f0172b3e5 | ||
|
|
79fe954d79 | ||
|
|
0501bfa159 | ||
|
|
a155b7e76c | ||
|
|
5228b756af | ||
|
|
04e7596680 | ||
|
|
f1e66cb2eb | ||
|
|
4fd35ee072 | ||
|
|
ee69799262 | ||
|
|
636a9637f4 | ||
|
|
9383e66f94 | ||
|
|
99e6833c85 | ||
|
|
c203be3fb4 | ||
|
|
02175a7986 | ||
|
|
8117df4cd9 | ||
|
|
7c7dd9dc7f | ||
|
|
054932f403 | ||
|
|
aed473ccf9 | ||
|
|
8268e94cd4 | ||
|
|
4af98ecdfb | ||
|
|
4b9f9010b0 | ||
|
|
2a0fcf6113 | ||
|
|
67d95f177c | ||
|
|
44773ad125 | ||
|
|
5774ef35c4 | ||
|
|
b95cfa9170 | ||
|
|
afa1ded425 | ||
|
|
00ac23e6e0 | ||
|
|
7d0c934a3e | ||
|
|
8f75761f24 | ||
|
|
9fd24e3a22 | ||
|
|
755a9d3d1a | ||
|
|
ac499cb61c | ||
|
|
180940e02d | ||
|
|
976b03c56b | ||
|
|
450d89ddc1 | ||
|
|
463b2e5542 | ||
|
|
70a2002399 | ||
|
|
a617b10075 | ||
|
|
0029071adb | ||
|
|
ad49fe7c8f | ||
|
|
49bc802f81 | ||
|
|
af9cdee9cb | ||
|
|
b4e1576aee | ||
|
|
78e2b74bb9 | ||
|
|
65d161c480 | ||
|
|
9fcbd5db2a | ||
|
|
4f3bf679f5 | ||
|
|
0072afca8e | ||
|
|
61aa5ba36e | ||
|
|
9f4323252a | ||
|
|
8b6c896c4b | ||
|
|
185dbc4974 | ||
|
|
3d535e0471 | ||
|
|
d22dec74ff | ||
|
|
9872d3110c | ||
|
|
b859971873 | ||
|
|
e5095f1198 | ||
|
|
499a077761 | ||
|
|
5da7177729 | ||
|
|
3507766bd0 | ||
|
|
f37bdbe537 | ||
|
|
2da09ff8b0 | ||
|
|
5ccddb7ecf | ||
|
|
954c1d0529 | ||
|
|
494f20cbdc | ||
|
|
29902c8ec0 | ||
|
|
9f15bdabc8 | ||
|
|
fff3455f58 | ||
|
|
87446dc618 | ||
|
|
99ac0390f5 | ||
|
|
ff0f0b9172 | ||
|
|
97b570a94c | ||
|
|
a9d56c6843 | ||
|
|
f98470df69 | ||
|
|
eb8be1fe76 | ||
|
|
8a1a26ce4c | ||
|
|
5bf3276e8d | ||
|
|
7ebd5376fe | ||
|
|
93dfcb9357 | ||
|
|
0c8662d2b6 | ||
|
|
d84f1d14b5 | ||
|
|
70219b0f43 | ||
|
|
bd5bc0cd5a | ||
|
|
6e054aacca | ||
|
|
9d581f3d52 | ||
|
|
9bf99891d0 | ||
|
|
d9cf48e81e | ||
|
|
e1b9322b09 | ||
|
|
627b964825 | ||
|
|
a55e36f48d | ||
|
|
01e21b89ee | ||
|
|
788be3313d | ||
|
|
e1ec93304d | ||
|
|
edb99d4c18 | ||
|
|
68477c3dab | ||
|
|
65ba8b23f4 | ||
|
|
621ed9f5f4 | ||
|
|
b26733ba7f | ||
|
|
9836cfb8d6 | ||
|
|
665b6c1236 | ||
|
|
9414338a48 | ||
|
|
de390ea077 | ||
|
|
717b0239fd | ||
|
|
d00735a0c5 | ||
|
|
c23d5ce926 | ||
|
|
b5a3c7f109 | ||
|
|
9c5f685ef1 | ||
|
|
08bb8ef201 | ||
|
|
865ab62f43 | ||
|
|
9948113590 | ||
|
|
c4ee87022b | ||
|
|
ffba4edb06 | ||
|
|
958d0b659b | ||
|
|
aacda28b28 | ||
|
|
29e7e0781b | ||
|
|
7012620e2b | ||
|
|
f1da861018 | ||
|
|
4da31bd566 | ||
|
|
05aa9c82d9 | ||
|
|
a9e58ecd3f | ||
|
|
223544552f | ||
|
|
3d8e9573a4 | ||
|
|
54eb81a087 | ||
|
|
c33c547d66 | ||
|
|
dfe7dd9bdb | ||
|
|
63ccf6474d | ||
|
|
e8ac61e840 | ||
|
|
f00a650705 | ||
|
|
4bde5ce992 | ||
|
|
d31573fa37 | ||
|
|
8b8cde2140 | ||
|
|
0e805e782b | ||
|
|
f5c78d118b | ||
|
|
9d4f213f90 | ||
|
|
168db222c6 | ||
|
|
3d6388e34e | ||
|
|
3ce9bc712a | ||
|
|
e52c0bd0eb | ||
|
|
56c837ccb7 | ||
|
|
423d2be5f8 | ||
|
|
453a1617aa | ||
|
|
b9258c6178 | ||
|
|
55e5841f14 | ||
|
|
ed15e9ba02 | ||
|
|
eedda32e6b | ||
|
|
4c8fea92f3 | ||
|
|
d073055dcd | ||
|
|
e4ac7bb1e5 | ||
|
|
9bac8c57e3 | ||
|
|
6800d3372f | ||
|
|
3153a2c98d | ||
|
|
15b74b94be | ||
|
|
687cb3ad35 | ||
|
|
8f94784124 | ||
|
|
23dd1fc74c | ||
|
|
fa971259e6 | ||
|
|
b0cda32f72 | ||
|
|
08b7968e28 | ||
|
|
4b5fe1349f | ||
|
|
d23da75b32 | ||
|
|
06e027992d | ||
|
|
b5597738d4 | ||
|
|
bc03e58565 | ||
|
|
a00234f1c5 | ||
|
|
34c0f95db2 | ||
|
|
fcb04bcaca | ||
|
|
9464a194db | ||
|
|
9f4b9118cc | ||
|
|
60158217ef | ||
|
|
923e79e2e4 | ||
|
|
866b296d0f | ||
|
|
4053ee9104 | ||
|
|
47fd8c2f76 | ||
|
|
96b9690985 | ||
|
|
df15ef8dab | ||
|
|
002c0fb511 | ||
|
|
7584e38ce4 | ||
|
|
eb47569f8a | ||
|
|
d2a9de78df | ||
|
|
c5138a7ce4 | ||
|
|
c5fa81fe81 | ||
|
|
a074e92296 | ||
|
|
1ddb9456c4 | ||
|
|
58bde34a23 | ||
|
|
339516072b | ||
|
|
931bc3c3a7 | ||
|
|
db1e9ee771 | ||
|
|
a2d971309b | ||
|
|
d05a1dbe70 | ||
|
|
a80601f8d9 | ||
|
|
1c22238756 | ||
|
|
9ff811c5cd | ||
|
|
1ebc05df91 | ||
|
|
386bdfa698 | ||
|
|
1ae7ff771b | ||
|
|
5196b98897 | ||
|
|
e6e63e91a7 | ||
|
|
b4dd98358f | ||
|
|
181c7053e3 | ||
|
|
4d454c5e4b | ||
|
|
5c2191a605 | ||
|
|
bba5bfc890 | ||
|
|
1a5b77dc21 | ||
|
|
b2cf6543b2 | ||
|
|
0385d64223 | ||
|
|
6ebdfe43e4 | ||
|
|
fafec39d41 | ||
|
|
670861bd20 | ||
|
|
08f7db20c1 | ||
|
|
605ec701b7 | ||
|
|
d6aa68ce75 | ||
|
|
eb6cb9fbe9 | ||
|
|
84e1e036c2 | ||
|
|
1498940b10 | ||
|
|
806598b94d | ||
|
|
e26be70bca | ||
|
|
ca45246627 | ||
|
|
f9355dc989 | ||
|
|
9e0b579128 | ||
|
|
ff4a1279f2 | ||
|
|
9b254aa177 | ||
|
|
703d78bbf5 | ||
|
|
d9446c7319 | ||
|
|
b25b645d51 | ||
|
|
d90b3854ca | ||
|
|
bf24c3d017 | ||
|
|
f0bfaa2d7d | ||
|
|
f9f3e3df9a | ||
|
|
f8d5e1cfb5 | ||
|
|
c23848b3c5 | ||
|
|
6d00a2dcd1 | ||
|
|
b535170b21 | ||
|
|
1434184c57 | ||
|
|
7a372b64df | ||
|
|
5406af92bc | ||
|
|
7d65242dc3 | ||
|
|
544a8693b7 | ||
|
|
35a4f24a37 | ||
|
|
ff305edd64 | ||
|
|
efec4358b9 | ||
|
|
db3ca36403 | ||
|
|
42833b44b5 | ||
|
|
5d0a33eebc | ||
|
|
ba2df04b41 | ||
|
|
c6bbdadd79 | ||
|
|
b885bae634 | ||
|
|
680f9744c4 | ||
|
|
2c935c0c72 | ||
|
|
7198063d96 | ||
|
|
d41ebe146b | ||
|
|
4b4e1af059 | ||
|
|
80240b347e | ||
|
|
04b3b3df05 | ||
|
|
2ad5708c43 | ||
|
|
63f3cab4ae | ||
|
|
8cdf03a7a2 | ||
|
|
d78c834ead | ||
|
|
05a976cd99 | ||
|
|
34fb7e46ad | ||
|
|
abac15f3c6 | ||
|
|
b700055ba4 | ||
|
|
23905927e1 | ||
|
|
56be5f1567 | ||
|
|
1807ae22dd | ||
|
|
71646e4653 | ||
|
|
1335c3aca8 | ||
|
|
30455ce255 | ||
|
|
9bf87ae3aa | ||
|
|
abca34cbc0 | ||
|
|
d386878af9 | ||
|
|
685c74d315 | ||
|
|
69e0f1b445 | ||
|
|
79979c6897 | ||
|
|
ba64547616 | ||
|
|
ed5a637d62 | ||
|
|
8a278a1d7e | ||
|
|
77d9cb2f04 | ||
|
|
0459432d96 | ||
|
|
43150d7ac3 | ||
|
|
afe8b594be | ||
|
|
878563c847 | ||
|
|
06947add03 | ||
|
|
5cd47a5e4f | ||
|
|
53de95da5e | ||
|
|
663004ac2b | ||
|
|
6ad9cb224a | ||
|
|
e7752cd578 | ||
|
|
4d2f42361e | ||
|
|
4d8ee01389 | ||
|
|
d01924f488 | ||
|
|
bc56355ec6 | ||
|
|
ac20d95f97 | ||
|
|
1a83c731bd | ||
|
|
ca57a59883 | ||
|
|
b0d619fde2 | ||
|
|
cc7051efd7 | ||
|
|
5137adb94d | ||
|
|
0b9f7cd074 | ||
|
|
2632941f32 | ||
|
|
137597b0ea | ||
|
|
051df9ad99 | ||
|
|
f670ef1c8e | ||
|
|
d9d747a06a | ||
|
|
b813d8caf1 | ||
|
|
ecee572411 | ||
|
|
1b0427e6c4 | ||
|
|
2aa64b89b3 | ||
|
|
484c9d2d5b | ||
|
|
5d8dcb5342 | ||
|
|
2328f2fe68 | ||
|
|
4f514c7e88 | ||
|
|
5bdc520cf1 | ||
|
|
fc6e75dd57 | ||
|
|
4a5a898a8f | ||
|
|
ba9d16291b | ||
|
|
725652e924 | ||
|
|
8da0e0e946 | ||
|
|
588b82bbf8 | ||
|
|
bc0f937b55 | ||
|
|
baa43cbaf0 | ||
|
|
adb6b1b316 | ||
|
|
1c18de0019 | ||
|
|
4d52f2eb7f | ||
|
|
363cf58645 | ||
|
|
7e760fc188 | ||
|
|
ef2dcbe4ad | ||
|
|
9354a5fad4 | ||
|
|
1c97b0a777 | ||
|
|
2f3bdab2b9 | ||
|
|
0d7f036429 | ||
|
|
2cda13213d | ||
|
|
70d0d43b5e | ||
|
|
25c3a7348f | ||
|
|
9123d64592 | ||
|
|
b827a6015c | ||
|
|
d40a3b5b55 | ||
|
|
ef28a6cb26 | ||
|
|
1436a6835e | ||
|
|
e8cfacae37 | ||
|
|
3a7382950b | ||
|
|
eeb23eb7ea | ||
|
|
34fe5a94ba | ||
|
|
6181864290 | ||
|
|
e9ca615a98 | ||
|
|
62c95fd5fc | ||
|
|
25f14e9f93 | ||
|
|
ae670a6ed8 | ||
|
|
a7b8467ac0 | ||
|
|
15da7ce7fb | ||
|
|
e9eaf3fbcf | ||
|
|
3884dcf313 | ||
|
|
c4fc559f45 | ||
|
|
2bc4330303 | ||
|
|
12675275a1 | ||
|
|
3a105f7b20 | ||
|
|
1ae72fb23d | ||
|
|
7ec676bb3d | ||
|
|
29ea57283e | ||
|
|
5488973961 | ||
|
|
96d45a5489 | ||
|
|
7a012d5a16 | ||
|
|
fa6a16996e | ||
|
|
82245a6de7 | ||
|
|
ff28ede2d1 | ||
|
|
98b8ec8616 | ||
|
|
88f9d8748c | ||
|
|
7d57d2e18b | ||
|
|
38caa00d18 | ||
|
|
c827d4cfdb | ||
|
|
509c630db8 | ||
|
|
fbff30d2db | ||
|
|
86c7fdb17c | ||
|
|
62bd6589c7 | ||
|
|
2cc6d13547 | ||
|
|
bb8ca1d112 | ||
|
|
8e59539752 | ||
|
|
372744c544 | ||
|
|
83880949a1 | ||
|
|
3749e36e9f | ||
|
|
0b4253fa37 | ||
|
|
86ec1e487c | ||
|
|
fd4eefed39 | ||
|
|
b480e7874b | ||
|
|
41333b97b9 | ||
|
|
c1c924abfe | ||
|
|
1c7e2e64f6 | ||
|
|
7dff03636a | ||
|
|
5332fd91bf | ||
|
|
d4b963d0a6 | ||
|
|
6d3f5935e5 | ||
|
|
968ee17677 | ||
|
|
81ed3bb9c0 | ||
|
|
5115652828 | ||
|
|
1f92865494 | ||
|
|
e41f450f28 | ||
|
|
97fcf1bbd0 | ||
|
|
13763ce599 | ||
|
|
7fcb605b82 | ||
|
|
70484b9f8a | ||
|
|
69b46b3d95 | ||
|
|
95c5534f8e | ||
|
|
370b39e8ec | ||
|
|
3da8038918 | ||
|
|
a6762c4a22 | ||
|
|
98c2c0febc | ||
|
|
63cbd19f50 | ||
|
|
1934f3a0ea | ||
|
|
a909e6ad43 | ||
|
|
1dcb52188d | ||
|
|
28ebef0b1b | ||
|
|
f03a8a3c4e | ||
|
|
03f760b1c0 | ||
|
|
f900dc3fb9 | ||
|
|
95eb1adda8 | ||
|
|
c6ddbdb66c | ||
|
|
3800b908b1 | ||
|
|
69fe3a5f09 | ||
|
|
754270313a | ||
|
|
057ebeaca3 | ||
|
|
480065172d | ||
|
|
f2e0056579 | ||
|
|
32fffff2cc | ||
|
|
3c47824d6b | ||
|
|
0892090a56 | ||
|
|
d592b42f5c | ||
|
|
3b5f65a64c | ||
|
|
5c0b2c16a8 | ||
|
|
d39e0f05db | ||
|
|
6d14d08e06 | ||
|
|
32060c6d6b | ||
|
|
3dbec410a0 | ||
|
|
de765f6c31 | ||
|
|
dc455a5f88 | ||
|
|
bab19a8e91 | ||
|
|
322915014f | ||
|
|
79998cd5af | ||
|
|
50b9013064 | ||
|
|
bb03fdae0d | ||
|
|
4384cf9e7d | ||
|
|
d47e980d0d | ||
|
|
fe373287eb | ||
|
|
cbe443362f | ||
|
|
2c0c9dc46c | ||
|
|
0ceab84749 | ||
|
|
34e7dc81a9 | ||
|
|
4e6e9d21bd | ||
|
|
d1feb30811 | ||
|
|
43837189c1 | ||
|
|
249962ffa2 | ||
|
|
541168039d | ||
|
|
a650110ba7 | ||
|
|
54b31d149e | ||
|
|
a745475808 | ||
|
|
7ef00afe9d | ||
|
|
156fc83a55 | ||
|
|
46be82b811 | ||
|
|
09b412dafa | ||
|
|
5268a05e47 | ||
|
|
406224be52 | ||
|
|
3799834dcf | ||
|
|
553e412bda | ||
|
|
f22834a372 | ||
|
|
bd349a8704 | ||
|
|
bc08873cff | ||
|
|
aafe273990 | ||
|
|
c09593c04e | ||
|
|
84bf31aaf8 | ||
|
|
05d5392cda | ||
|
|
d9a743d917 | ||
|
|
ac6c358c2a | ||
|
|
ad0c0ad3b4 | ||
|
|
1ed34f3dd6 | ||
|
|
6a8f9cd22e | ||
|
|
e8b9ab8957 | ||
|
|
74f728249f | ||
|
|
d6a1738892 | ||
|
|
b326b07adc | ||
|
|
07d2921c6d | ||
|
|
22e462c97a | ||
|
|
dcf8077906 | ||
|
|
3408f6e64a | ||
|
|
e10dc0e1f0 | ||
|
|
ce5c1ae517 | ||
|
|
bbe718c97f | ||
|
|
01e4b1ee14 | ||
|
|
815ac0293e | ||
|
|
6568382d6f | ||
|
|
f943b7ddce | ||
|
|
ff9d68e7be | ||
|
|
7212560f4d | ||
|
|
1aa43d77c0 | ||
|
|
e038d5c4e3 | ||
|
|
dfad3aac98 | ||
|
|
df8418ffcf | ||
|
|
50aa43b3ae | ||
|
|
a90552663e | ||
|
|
883340c107 | ||
|
|
0fe2ff78e6 | ||
|
|
dc1eed93be | ||
|
|
b2f82360d7 | ||
|
|
782e0568ef | ||
|
|
90b4b0eabe | ||
|
|
cec04ef3a6 | ||
|
|
71fa56b887 | ||
|
|
b9b3ab45ea | ||
|
|
957b794c26 | ||
|
|
8001607e90 | ||
|
|
3e7202c1bc | ||
|
|
848edeab89 | ||
|
|
1748d67aea | ||
|
|
5477ca8239 | ||
|
|
233c1c0e76 | ||
|
|
d0fd305023 | ||
|
|
8dab1e9072 | ||
|
|
963aea5279 | ||
|
|
0a64aa7355 | ||
|
|
0669c89c55 | ||
|
|
2699da8041 | ||
|
|
98727e123f | ||
|
|
b29e0000e6 | ||
|
|
b3ed15b760 | ||
|
|
666a9a2b95 | ||
|
|
a4bcaad773 | ||
|
|
e65e4c8874 | ||
|
|
21f6330274 | ||
|
|
38c6902b90 | ||
|
|
2ddcd88129 | ||
|
|
dd8920653c | ||
|
|
c938c35f95 | ||
|
|
2eb0192155 | ||
|
|
d948e09b61 | ||
|
|
89966a5aea | ||
|
|
8e3df9dfee | ||
|
|
5890eef6b0 | ||
|
|
083c1bb960 | ||
|
|
861e65eb05 | ||
|
|
650cfd0cb0 | ||
|
|
e68ae99a41 | ||
|
|
8683b4d8d9 | ||
|
|
1dbd717eb4 | ||
|
|
6a8422b942 | ||
|
|
cb202fd286 | ||
|
|
67fc8ecd53 | ||
|
|
df8301fef5 | ||
|
|
4070b458ec | ||
|
|
ffbc3901d2 | ||
|
|
7a03280df4 | ||
|
|
482a1258de | ||
|
|
cd298882cd | ||
|
|
e01c56f9e1 | ||
|
|
4d72df4031 | ||
|
|
f7f1df1d82 | ||
|
|
c4a21bc9db | ||
|
|
621ffe7bf4 | ||
|
|
8dd5418803 | ||
|
|
965cb8d530 | ||
|
|
b2e8e7dab5 | ||
|
|
59d814f793 | ||
|
|
bb865f3a5e | ||
|
|
9ee53a49f0 | ||
|
|
79adb09baa | ||
|
|
cf0649f8b7 | ||
|
|
f8690631e2 | ||
|
|
5456d78f0c | ||
|
|
cbbece96a2 | ||
|
|
9d8ba307ef | ||
|
|
ec7c1e85e0 | ||
|
|
e70c7568c0 | ||
|
|
39b62db116 | ||
|
|
2edce52584 | ||
|
|
10831b5ec9 | ||
|
|
3a0f0c263a | ||
|
|
2419a376b9 | ||
|
|
e206740fd7 | ||
|
|
290a5a8d85 | ||
|
|
e2dc351d25 | ||
|
|
c86b61428b | ||
|
|
40b96352c9 | ||
|
|
189ba90996 | ||
|
|
c8183e661d | ||
|
|
053c94f1b3 | ||
|
|
b9d76a9571 | ||
|
|
a01cfc2951 | ||
|
|
4eb5c65bee | ||
|
|
06d07c4000 | ||
|
|
74f8654a53 | ||
|
|
9e105a858c | ||
|
|
cd8a07a764 | ||
|
|
aa49acd15a | ||
|
|
642f23bd81 | ||
|
|
2e24e6bd17 | ||
|
|
2a09c1b8ab | ||
|
|
a5ebf77d87 | ||
|
|
b874495b1f | ||
|
|
b860f5dfd4 | ||
|
|
b19fc36c81 | ||
|
|
d2d8248f68 | ||
|
|
f54bab4d67 | ||
|
|
bf6427d2fb | ||
|
|
672f1bd849 | ||
|
|
529d26c3e1 | ||
|
|
857f00ed94 | ||
|
|
e4a5e772f2 | ||
|
|
a542e372ab | ||
|
|
0d1bd5d62f | ||
|
|
9f3fa89f7c | ||
|
|
92995e6265 | ||
|
|
a4196c3ea5 | ||
|
|
db37e0c273 | ||
|
|
d0aefec99a | ||
|
|
66be4b89d7 | ||
|
|
870744ce8f | ||
|
|
2ad978532b | ||
|
|
5090d93f2c | ||
|
|
c8ff645766 | ||
|
|
25f7d1beba | ||
|
|
09aa111918 | ||
|
|
10fb7710e8 | ||
|
|
c0ea8ebb9b | ||
|
|
31fd9c7601 | ||
|
|
ddbed36455 | ||
|
|
a9b0d4e1f4 | ||
|
|
4d6a3ff411 | ||
|
|
7fb993e1f4 | ||
|
|
02f502f435 | ||
|
|
4515cb43ca | ||
|
|
d740333224 | ||
|
|
c610f38ba9 | ||
|
|
6447353f52 | ||
|
|
b46ed49996 | ||
|
|
cd9fdccde0 | ||
|
|
2a8137272d | ||
|
|
762155cc90 | ||
|
|
f8610ba1ca | ||
|
|
c99f4098c4 | ||
|
|
3eec9fef30 | ||
|
|
8c8826176d | ||
|
|
14a2d6789f | ||
|
|
7513f298b0 | ||
|
|
c04c3e334c | ||
|
|
f8e51f60b3 | ||
|
|
33b066bda0 | ||
|
|
14f41bc2fb | ||
|
|
008bee0f50 | ||
|
|
29492f3332 | ||
|
|
bc94bd510b | ||
|
|
9dd8e46a2d | ||
|
|
8be2bdfabd | ||
|
|
b4c0806963 | ||
|
|
cc38fa6cfb | ||
|
|
6de5dbafee | ||
|
|
60bf45c80d | ||
|
|
eb0f3e7ec0 | ||
|
|
ed553379df | ||
|
|
5c1e6f69c4 | ||
|
|
757cda0a96 | ||
|
|
e94443de80 | ||
|
|
0954cd8aa4 | ||
|
|
da55dac047 | ||
|
|
13a11b195f | ||
|
|
92dcba1e1c | ||
|
|
2fe1b5bd2a | ||
|
|
f91e1a8739 | ||
|
|
24e21613b6 | ||
|
|
c6391cd587 | ||
|
|
006ce15a0c | ||
|
|
edf4216119 | ||
|
|
ae8953409e | ||
|
|
bda44f31a1 | ||
|
|
6621ca39a3 | ||
|
|
14f7abfa71 | ||
|
|
0f0b5736da | ||
|
|
6728187ac0 | ||
|
|
17c8675853 | ||
|
|
cfbee8a431 | ||
|
|
736785ab63 | ||
|
|
3ded7bac16 | ||
|
|
b524a001d6 | ||
|
|
7b071e317b | ||
|
|
a380509259 | ||
|
|
c0dea0a782 | ||
|
|
70947ea7b1 | ||
|
|
81cd954a51 | ||
|
|
feccf29c87 | ||
|
|
5b5fbc0867 | ||
|
|
f158799bbe | ||
|
|
8b0e8990c2 | ||
|
|
880ee801cf | ||
|
|
163965d861 | ||
|
|
6e218b3f9a | ||
|
|
cc9b9df0b6 | ||
|
|
31f224008e | ||
|
|
f32cb5cb14 | ||
|
|
fec2d97ca2 | ||
|
|
f2eeafb061 | ||
|
|
8f4e8bf280 | ||
|
|
cc36e2295a | ||
|
|
d47aeb2252 | ||
|
|
14523ed969 | ||
|
|
592e97e855 | ||
|
|
53faa3ca5f | ||
|
|
c62566971f | ||
|
|
902be27cf9 | ||
|
|
bf12cbe07c | ||
|
|
f52e66505a | ||
|
|
ca75235d3d | ||
|
|
ecc6bd1341 | ||
|
|
ce81b1411d | ||
|
|
7691a7a3bd | ||
|
|
214e74bf6f | ||
|
|
c5826a491b | ||
|
|
d8e7ef04dc | ||
|
|
08f2a92c9c | ||
|
|
3220c50f9a | ||
|
|
024ebb2706 | ||
|
|
f11554092b | ||
|
|
954352c4c0 | ||
|
|
4aec95f3c9 | ||
|
|
be531ef1ec | ||
|
|
65c1a750f5 | ||
|
|
5141249c59 | ||
|
|
6225984681 | ||
|
|
5cb91ceaa5 | ||
|
|
89c09e2a08 | ||
|
|
fbbb219409 | ||
|
|
820b064804 | ||
|
|
355c524bfa | ||
|
|
c052ce6cde | ||
|
|
c9a779695d | ||
|
|
a685ae511a | ||
|
|
5edea45fab | ||
|
|
8afff9f849 | ||
|
|
a2043572aa | ||
|
|
5d98908b26 | ||
|
|
d6fd958c5f | ||
|
|
d0eb724e22 | ||
|
|
afe4a8c769 | ||
|
|
9fc03aa87c | ||
|
|
c798f15b98 | ||
|
|
2dcc114f84 | ||
|
|
0dfe9bc9d2 | ||
|
|
4d1cdb5bfe | ||
|
|
9c5335a027 | ||
|
|
ae849ca170 | ||
|
|
94c1255782 | ||
|
|
476e1095fa | ||
|
|
8da1bb0418 | ||
|
|
01c58f8473 | ||
|
|
edfcf7abe2 | ||
|
|
37b44fe7c1 | ||
|
|
8f02ad4f12 | ||
|
|
51f1244600 | ||
|
|
7bd930368c | ||
|
|
fb69240ca0 | ||
|
|
830d53bfae | ||
|
|
c36a959549 | ||
|
|
e91b2d14e3 | ||
|
|
ac58e68bc3 | ||
|
|
504c1cedfe | ||
|
|
9a4d8fae82 | ||
|
|
7d2ba6394c | ||
|
|
b04b94da5f | ||
|
|
9933857f67 | ||
|
|
ed5641e249 | ||
|
|
a4257017ef | ||
|
|
18153f1b32 | ||
|
|
7a91d1fc43 | ||
|
|
af14ded75e | ||
|
|
65939effb5 | ||
|
|
66ee7b3234 | ||
|
|
cd47a628fc | ||
|
|
d7c78decb0 | ||
|
|
8749477ed0 | ||
|
|
7088f5b5fa | ||
|
|
5bb6328cb9 | ||
|
|
ce9f47de99 | ||
|
|
4c4780c25e | ||
|
|
64f1aba8f1 | ||
|
|
3359fb661f | ||
|
|
58a9f1b864 | ||
|
|
6ac41a4ef5 | ||
|
|
aa2af7ba74 | ||
|
|
ce73839fe4 | ||
|
|
1dc2726f8d | ||
|
|
af76e8174d | ||
|
|
402a3efc92 | ||
|
|
372f08c990 | ||
|
|
dd29eb7f81 | ||
|
|
bca788ab1d | ||
|
|
aef8fdba11 | ||
|
|
0a1603634b | ||
|
|
a662163fd5 | ||
|
|
bd7a6478a2 | ||
|
|
4a20c9f628 | ||
|
|
418c5cc3fc | ||
|
|
cc55d08832 | ||
|
|
de5c545648 | ||
|
|
a35099bd33 | ||
|
|
5f4b5cf044 | ||
|
|
beb10f843f | ||
|
|
29713e4268 | ||
|
|
8e4b83b96b | ||
|
|
ae603b500e | ||
|
|
d97aae7572 | ||
|
|
a55e2f04a0 | ||
|
|
6e53c91608 | ||
|
|
d2272fcf6e | ||
|
|
c7ac5dce8c | ||
|
|
5c1d459ae9 | ||
|
|
2e7daef502 | ||
|
|
6410229681 | ||
|
|
e40bd5f06b | ||
|
|
06b491eb7b | ||
|
|
3a9fadd6df | ||
|
|
0de9312a7e | ||
|
|
27fe5e3473 | ||
|
|
f67dcc09f5 | ||
|
|
fefc9d121d | ||
|
|
a319c33d8b | ||
|
|
218d6bcc05 | ||
|
|
7d25463972 | ||
|
|
aff84bec07 | ||
|
|
ac651e974e | ||
|
|
e21a55abcc | ||
|
|
bc03228ab5 | ||
|
|
f05d0e73c6 | ||
|
|
aed2d4b31e | ||
|
|
184a197441 | ||
|
|
ed676e8c0a | ||
|
|
8e1f937473 | ||
|
|
1a68d39211 | ||
|
|
4ba7d5b14c | ||
|
|
1a48181a9f | ||
|
|
6b70a4eb7d | ||
|
|
f01855813b | ||
|
|
4a3cdf81af | ||
|
|
f777397aca | ||
|
|
8fb2e5a4f5 | ||
|
|
4e8cc1e973 | ||
|
|
ff02a228e3 | ||
|
|
424266abb1 | ||
|
|
3fde134791 | ||
|
|
7c39a65543 | ||
|
|
8cf70de428 | ||
|
|
15ac8413c7 | ||
|
|
79c21abba7 | ||
|
|
d5c418f29f | ||
|
|
536b94e56f | ||
|
|
5c29dbd0c7 | ||
|
|
ba9e68f402 | ||
|
|
e9f65f8749 | ||
|
|
ae0dd4b298 | ||
|
|
f1ce35af1a | ||
|
|
6e617ed0b6 | ||
|
|
7cf97daf77 | ||
|
|
3d24d997ae | ||
|
|
115c281672 | ||
|
|
cce23e43a9 | ||
|
|
ff556f5c09 | ||
|
|
16fa01291b | ||
|
|
01534bf54f | ||
|
|
cd341b6e06 | ||
|
|
185a7e25e7 | ||
|
|
e81a474603 | ||
|
|
ff2be6e180 | ||
|
|
3da4b31359 | ||
|
|
4bbeb19fc7 | ||
|
|
a9cbab1735 | ||
|
|
6b7556a554 | ||
|
|
a3c7019e06 | ||
|
|
416b9c29f7 | ||
|
|
2ec8e04cac | ||
|
|
e03bfb30ce | ||
|
|
f5b669113f | ||
|
|
d08225edf4 | ||
|
|
8075d4f99d | ||
|
|
1a944d8a2a | ||
|
|
7cf02b6619 | ||
|
|
55cde6ef3c | ||
|
|
69c3af567d | ||
|
|
60e1fe0079 | ||
|
|
4669393070 | ||
|
|
ce3bfe5d57 | ||
|
|
2a0c2ca2b8 | ||
|
|
c89fbfb385 | ||
|
|
facecb84a1 | ||
|
|
ed06e9949b | ||
|
|
e15307a612 | ||
|
|
5cbb2699ee | ||
|
|
a2edf2e7ff | ||
|
|
1d31e7a2fc | ||
|
|
a2a4d5fa31 | ||
|
|
a28ccbabc6 | ||
|
|
edd7344820 | ||
|
|
c808ef81bb | ||
|
|
fd203fe357 | ||
|
|
5bb7ab9928 | ||
|
|
87270c8416 | ||
|
|
ebc2f7a2db | ||
|
|
7700207ec7 | ||
|
|
4d5d14f5cf | ||
|
|
72b249bf1f | ||
|
|
9b4774b21b | ||
|
|
2ddf083588 | ||
|
|
8343a03357 | ||
|
|
ad320e9b83 | ||
|
|
ecb750a446 | ||
|
|
5f88e02818 | ||
|
|
616af2f4b9 | ||
|
|
5a3b315b5f | ||
|
|
b7a2268e7b | ||
|
|
20d729228c | ||
|
|
af8c93086c | ||
|
|
79fd11ab8e | ||
|
|
cb88671e37 | ||
|
|
ff79552f13 | ||
|
|
643fe72717 | ||
|
|
4747e2183a | ||
|
|
c59e701e35 | ||
|
|
8e678af4ba | ||
|
|
70a1165b32 | ||
|
|
af14000215 | ||
|
|
998e6cdba0 | ||
|
|
2315fb5e5f | ||
|
|
157e9e5aa5 | ||
|
|
c496ec0848 | ||
|
|
15b67a268a | ||
|
|
31c4809827 | ||
|
|
ac0df2350a | ||
|
|
223b27f46c | ||
|
|
425142be60 | ||
|
|
7e17ec8c71 | ||
|
|
448830ce7b | ||
|
|
8896b614a9 | ||
|
|
a7fce980ad | ||
|
|
91757b0f37 | ||
|
|
fbfcc2972b | ||
|
|
db40364b87 | ||
|
|
094ce39c45 | ||
|
|
ae67d082fe | ||
|
|
8f76df7f37 | ||
|
|
5c19d18cbf | ||
|
|
838b93405b | ||
|
|
2676caf344 | ||
|
|
17941321ab | ||
|
|
48c971e073 | ||
|
|
f5e2efbbf0 | ||
|
|
5d1f0e607b | ||
|
|
b0872c19ea | ||
|
|
9f790b9901 | ||
|
|
c41a2ec4af | ||
|
|
575dad3c98 | ||
|
|
32d687f55e | ||
|
|
93f787070f | ||
|
|
f9544f6e8f | ||
|
|
336d19044c | ||
|
|
7866c9e173 | ||
|
|
1a4123de04 | ||
|
|
cf2e2eb1c0 | ||
|
|
2051acdeb2 | ||
|
|
cefdf970cc | ||
|
|
a1d0aa7b88 | ||
|
|
49aeedb8cb | ||
|
|
ef249a2cd7 | ||
|
|
a09141548a | ||
|
|
5379a2d40d | ||
|
|
c9450c7ab1 | ||
|
|
faa1b5c292 | ||
|
|
393d9fc6d2 | ||
|
|
4e6a228689 | ||
|
|
179d6678b1 | ||
|
|
85698c5086 | ||
|
|
a7d9ded45d | ||
|
|
531980d89c | ||
|
|
1887ecd4d6 | ||
|
|
cd32c2caba | ||
|
|
1c9a1457fc | ||
|
|
038b0eb1da | ||
|
|
f20bf146e2 | ||
|
|
01218f919b | ||
|
|
2684871bc1 | ||
|
|
ccf3960eec | ||
|
|
eecc0685c9 | ||
|
|
2ed849eccf | ||
|
|
3378d67a18 | ||
|
|
f3c0c667a6 | ||
|
|
0ae8bbac2d | ||
|
|
cbc3cfcab4 | ||
|
|
b30ef07c6c | ||
|
|
73900846b1 | ||
|
|
d1dc7e3991 | ||
|
|
3073a6d5e9 | ||
|
|
aae53774f2 | ||
|
|
7a757b7194 | ||
|
|
fa8ce26904 | ||
|
|
2c2c06e359 | ||
|
|
ee580538fa | ||
|
|
c3c5c31517 | ||
|
|
ed9a25dd61 | ||
|
|
9ef4f12b53 | ||
|
|
84f8101606 | ||
|
|
b1337948eb | ||
|
|
98f02fdde2 | ||
|
|
048fdc2292 | ||
|
|
2ca1c5aa9f | ||
|
|
674fb0fcc5 | ||
|
|
00bfe40e4d | ||
|
|
cd459b1d49 | ||
|
|
92a4793b3c | ||
|
|
dc03a42537 | ||
|
|
219da6bb68 | ||
|
|
0499cd866e | ||
|
|
13047f4135 | ||
|
|
af69cab21d | ||
|
|
d41a3fa1b4 | ||
|
|
733be371af | ||
|
|
576904bce6 | ||
|
|
cf47794f09 | ||
|
|
c06a9f8730 | ||
|
|
2e90dff2c2 | ||
|
|
90183a46d8 | ||
|
|
b68eedba23 | ||
|
|
d5b559393b | ||
|
|
1de4ac1385 | ||
|
|
39aa42ffbb | ||
|
|
ec1b9577ba | ||
|
|
3b4444f99a | ||
|
|
613b2d9dc6 | ||
|
|
8f4cc22455 | ||
|
|
7c42327e0e | ||
|
|
873383e9bd | ||
|
|
8508557e77 | ||
|
|
4d1652484f | ||
|
|
88cf6fb368 | ||
|
|
e7db87f700 | ||
|
|
2cb434e53e | ||
|
|
cd65491c30 | ||
|
|
082b1155a3 | ||
|
|
9202b1b787 | ||
|
|
a7e01c438d | ||
|
|
05be67e77d | ||
|
|
85741b9986 | ||
|
|
f247a199fe | ||
|
|
29171bc2d2 | ||
|
|
7be5a62ed7 | ||
|
|
3647136f24 | ||
|
|
13598940e3 | ||
|
|
0eb365868e | ||
|
|
28c6411e49 | ||
|
|
bba3fc7960 | ||
|
|
fcd877013e | ||
|
|
ba1d4c0488 | ||
|
|
517bcca299 | ||
|
|
1b53778175 | ||
|
|
b7a0304d92 | ||
|
|
545315a985 | ||
|
|
3f4327520c | ||
|
|
4a34f69ea6 | ||
|
|
fb7e68833c | ||
|
|
486dd09e0b | ||
|
|
054b99a330 | ||
|
|
65c5e044c7 | ||
|
|
11984c7467 | ||
|
|
3946864c8a | ||
|
|
b84037013e | ||
|
|
1dbfc62d75 | ||
|
|
d7d79106c7 | ||
|
|
1138491631 | ||
|
|
71705fa70d | ||
|
|
602814adab | ||
|
|
3a77719c5a | ||
|
|
7e195d0e92 | ||
|
|
e04793401d | ||
|
|
a3fbd18824 | ||
|
|
c6052b8c14 | ||
|
|
c792b5011f | ||
|
|
32aaeca775 | ||
|
|
1593194c63 | ||
|
|
614a7e1e23 | ||
|
|
2ebfeacabc | ||
|
|
f5d8f58a17 | ||
|
|
937daef4a7 | ||
|
|
dd77f14c64 | ||
|
|
c36cbe5a8a | ||
|
|
41b2194f86 | ||
|
|
d1e2e8f583 | ||
|
|
47fe42e1ab | ||
|
|
4c60393854 | ||
|
|
f848215dfc | ||
|
|
dcca581967 | ||
|
|
d475b3384c | ||
|
|
dd7831fe94 | ||
|
|
cc08b11d16 | ||
|
|
8bba753cca | ||
|
|
43d6280d0a | ||
|
|
e5a11a2293 | ||
|
|
f18ef2d144 | ||
|
|
1bb5c511a5 | ||
|
|
d55de57b67 | ||
|
|
a2aaf4dbc6 | ||
|
|
bdf6eee0ae | ||
|
|
8b910bda0c | ||
|
|
24993e3b39 | ||
|
|
11101076a1 | ||
|
|
f838875726 | ||
|
|
28778d6bae | ||
|
|
1132eae56d | ||
|
|
d34e79492d | ||
|
|
ab205b9dc8 | ||
|
|
7dcad95d4f | ||
|
|
8a48223a7b | ||
|
|
d47ae7f620 | ||
|
|
135c9c42bf | ||
|
|
0bf79ac455 | ||
|
|
98998cded6 | ||
|
|
14137b5781 | ||
|
|
a172d96292 | ||
|
|
23ba76bc0e | ||
|
|
61e00a9775 | ||
|
|
d1508cd68d | ||
|
|
9c85b5376d | ||
|
|
3c6f245083 | ||
|
|
f207019ce5 | ||
|
|
bd05aa4e24 | ||
|
|
8dc9d361c2 | ||
|
|
d0e958c71c | ||
|
|
a0bb7c5593 | ||
|
|
7feddd9fc7 | ||
|
|
55969016e9 | ||
|
|
9609f02e3c | ||
|
|
5c7495a194 | ||
|
|
5ee6fc974e | ||
|
|
c2ebea6580 | ||
|
|
12a129ec6d | ||
|
|
f28fe66970 | ||
|
|
123397317c | ||
|
|
dc570c4951 | ||
|
|
22d3628319 | ||
|
|
50c9949d7a | ||
|
|
376817c6d4 | ||
|
|
63fc800057 | ||
|
|
e0d0572b73 | ||
|
|
7fde87c77d | ||
|
|
938c3f65b6 | ||
|
|
2461f79d2a | ||
|
|
499bfcbfd0 | ||
|
|
07490f8017 | ||
|
|
91410c9bfa | ||
|
|
a7440261c5 | ||
|
|
76c73715fb | ||
|
|
c75f0b361a | ||
|
|
295df4edb9 | ||
|
|
562ceab13d | ||
|
|
2f0f6578c3 | ||
|
|
30cbd4e0d6 | ||
|
|
549e58069c | ||
|
|
7594be85ff | ||
|
|
3630034609 | ||
|
|
4e01501bbf | ||
|
|
1aa5172f56 | ||
|
|
f7e2ee8fa6 | ||
|
|
66dc9a3701 | ||
|
|
31bd39256b | ||
|
|
003c69a84b | ||
|
|
0134901108 | ||
|
|
eee6293d57 | ||
|
|
8237bec4f0 | ||
|
|
29cad7ad13 | ||
|
|
0d103de3b0 | ||
|
|
a0090691d0 | ||
|
|
6c87c2eea8 | ||
|
|
58c2ec6ab3 | ||
|
|
df5ae3eb16 | ||
|
|
efda2d7854 | ||
|
|
e143f5dae9 | ||
|
|
48218cdb97 | ||
|
|
e9fade72f3 | ||
|
|
0f2c0d335b | ||
|
|
40b077bc7e | ||
|
|
a931092cb3 | ||
|
|
bd3749ed69 | ||
|
|
4ffbf77886 | ||
|
|
781a7ef60a | ||
|
|
5b2949ee0b | ||
|
|
a0d646135a | ||
|
|
7862ad88b7 | ||
|
|
f3bff94cf9 | ||
|
|
0eba1e1782 | ||
|
|
e3216b82bf | ||
|
|
da419e2332 | ||
|
|
0d97ef43be | ||
|
|
1a2313a6f2 | ||
|
|
250a9bdfe2 | ||
|
|
6317a3e9da | ||
|
|
7ab7c9e932 | ||
|
|
e129c5bc0d | ||
|
|
2e241242a3 | ||
|
|
9724e5d336 | ||
|
|
63a562f95e | ||
|
|
5c340b0387 | ||
|
|
1c6510f57a | ||
|
|
2a15a98a6a | ||
|
|
72a406e7aa | ||
|
|
feccc3ff37 | ||
|
|
265bfa2c79 | ||
|
|
8faf9b9b41 | ||
|
|
84be7c230c | ||
|
|
3e675fabe0 | ||
|
|
cd5b4b0bc2 | ||
|
|
7ef822021b | ||
|
|
9a48926a57 | ||
|
|
13cd97f3df | ||
|
|
183139340b | ||
|
|
1c69bca258 | ||
|
|
c10ea454dc | ||
|
|
9504fc21b5 | ||
|
|
13d8fbef30 | ||
|
|
b8988b63a6 | ||
|
|
5eaaeb7c31 | ||
|
|
c4f8c453ae | ||
|
|
6f4ba54079 | ||
|
|
637570326b | ||
|
|
37f885650c | ||
|
|
c8c34ccb20 | ||
|
|
e765ed3a9c | ||
|
|
677063594e | ||
|
|
59c7cbd482 | ||
|
|
570311610e | ||
|
|
41b264e77c | ||
|
|
df4bd0d53f | ||
|
|
7f09a662a0 | ||
|
|
4f3b21e1c7 | ||
|
|
54233c9080 | ||
|
|
db8e13ef71 | ||
|
|
5a42414b9c | ||
|
|
9c665ab72e | ||
|
|
b665ba6aa6 | ||
|
|
ec5913b5cd | ||
|
|
25ac63ed71 | ||
|
|
99209c2916 | ||
|
|
1fbaa0a521 | ||
|
|
3037b91e05 | ||
|
|
ffdf972b91 | ||
|
|
459e5fbd5f | ||
|
|
bfc993cc91 | ||
|
|
4432db35d9 | ||
|
|
591ab1dff9 | ||
|
|
5bca2424bc | ||
|
|
bd61a9e770 | ||
|
|
3438e7acd2 | ||
|
|
09c200acf2 | ||
|
|
716889cab1 | ||
|
|
409693984f | ||
|
|
04e8c11080 | ||
|
|
80af2b73ab | ||
|
|
3cc57f9645 | ||
|
|
a65d4e7f14 | ||
|
|
b531cfc019 | ||
|
|
543ec2136b | ||
|
|
93b5071f73 | ||
|
|
ddc369f073 | ||
|
|
1b40dc92eb | ||
|
|
fcc3e6138b | ||
|
|
9fe6ef7ab2 | ||
|
|
c010af6f19 | ||
|
|
35b7982303 | ||
|
|
f311cfa231 | ||
|
|
80970e531b | ||
|
|
b7bb76df05 | ||
|
|
98c70d6fc7 | ||
|
|
ab84349b16 | ||
|
|
03091e372f | ||
|
|
4d17184817 | ||
|
|
e086e0eb6c | ||
|
|
314368c822 | ||
|
|
c5181ab410 | ||
|
|
ea5152cae1 | ||
|
|
255fca5eea | ||
|
|
4aeccadf4e | ||
|
|
93540ee10e | ||
|
|
8fb3ac3649 | ||
|
|
77b2986b5b | ||
|
|
62b013df0d | ||
|
|
fad6768bd1 | ||
|
|
a78125f925 | ||
|
|
a00a8bcc8a | ||
|
|
1e9a9e167d | ||
|
|
3da0db62e6 | ||
|
|
e14ced7918 | ||
|
|
ab9d02f53b | ||
|
|
a461a11989 | ||
|
|
1bd838608f | ||
|
|
365577f567 | ||
|
|
50efb383f0 | ||
|
|
5da6bd0083 | ||
|
|
5e9a033e6e | ||
|
|
fb7cb6823e | ||
|
|
dd0a58f5f0 | ||
|
|
a21420389e | ||
|
|
6140baf4e1 | ||
|
|
8fc642eb5b | ||
|
|
e66e1a0046 | ||
|
|
d5c69f1da4 | ||
|
|
f13b1e7d7f | ||
|
|
5c8a3f862a | ||
|
|
8807f1277f | ||
|
|
a3b9157f49 | ||
|
|
b88ba05356 | ||
|
|
b74d505577 | ||
|
|
9e2d7dca87 | ||
|
|
d236b37ac9 | ||
|
|
e880c66bd8 | ||
|
|
383456aa29 | ||
|
|
1a13940c8d | ||
|
|
3d54788495 | ||
|
|
71d53ace2f | ||
|
|
f37e3f99f0 | ||
|
|
bd03ffc16e | ||
|
|
1ac1af9b47 | ||
|
|
3bf5705316 | ||
|
|
1c2528c8a3 | ||
|
|
4f7cea6c53 | ||
|
|
afbdd3acc3 | ||
|
|
01561da142 | ||
|
|
0af25f784b | ||
|
|
b9b42f2ea0 | ||
|
|
311c393838 | ||
|
|
18c1c42405 | ||
|
|
37dd5d4629 | ||
|
|
edab9dbf4d | ||
|
|
9868ea4936 | ||
|
|
85920dd01d | ||
|
|
bd7fe0cf66 | ||
|
|
48246541da | ||
|
|
360e1ca5cc | ||
|
|
a1f2a06b34 | ||
|
|
c84dd8a90d | ||
|
|
65469a7f8b | ||
|
|
6b597516c1 | ||
|
|
b5857f62e2 | ||
|
|
a504ced097 |
@@ -2,11 +2,11 @@ language: python
|
|||||||
python:
|
python:
|
||||||
- "2.6"
|
- "2.6"
|
||||||
- "2.7"
|
- "2.7"
|
||||||
|
- "3.2"
|
||||||
- "3.3"
|
- "3.3"
|
||||||
- "3.4"
|
- "3.4"
|
||||||
before_install:
|
- "3.5"
|
||||||
- sudo apt-get update -qq
|
sudo: false
|
||||||
- sudo apt-get install -yqq rtmpdump
|
|
||||||
script: nosetests test --verbose
|
script: nosetests test --verbose
|
||||||
notifications:
|
notifications:
|
||||||
email:
|
email:
|
||||||
|
|||||||
46
AUTHORS
46
AUTHORS
@@ -111,3 +111,49 @@ Paul Hartmann
|
|||||||
Frans de Jonge
|
Frans de Jonge
|
||||||
Robin de Rooij
|
Robin de Rooij
|
||||||
Ryan Schmidt
|
Ryan Schmidt
|
||||||
|
Leslie P. Polzer
|
||||||
|
Duncan Keall
|
||||||
|
Alexander Mamay
|
||||||
|
Devin J. Pohly
|
||||||
|
Eduardo Ferro Aldama
|
||||||
|
Jeff Buchbinder
|
||||||
|
Amish Bhadeshia
|
||||||
|
Joram Schrijver
|
||||||
|
Will W.
|
||||||
|
Mohammad Teimori Pabandi
|
||||||
|
Roman Le Négrate
|
||||||
|
Matthias Küch
|
||||||
|
Julian Richen
|
||||||
|
Ping O.
|
||||||
|
Mister Hat
|
||||||
|
Peter Ding
|
||||||
|
jackyzy823
|
||||||
|
George Brighton
|
||||||
|
Remita Amine
|
||||||
|
Aurélio A. Heckert
|
||||||
|
Bernhard Minks
|
||||||
|
sceext
|
||||||
|
Zach Bruggeman
|
||||||
|
Tjark Saul
|
||||||
|
slangangular
|
||||||
|
Behrouz Abbasi
|
||||||
|
ngld
|
||||||
|
nyuszika7h
|
||||||
|
Shaun Walbridge
|
||||||
|
Lee Jenkins
|
||||||
|
Anssi Hannula
|
||||||
|
Lukáš Lalinský
|
||||||
|
Qijiang Fan
|
||||||
|
Rémy Léone
|
||||||
|
Marco Ferragina
|
||||||
|
reiv
|
||||||
|
Muratcan Simsek
|
||||||
|
Evan Lu
|
||||||
|
flatgreen
|
||||||
|
Brian Foley
|
||||||
|
Vignesh Venkat
|
||||||
|
Tom Gijselinck
|
||||||
|
Founder Fang
|
||||||
|
Andrew Alexeyew
|
||||||
|
Saso Bezlaj
|
||||||
|
Erwin de Haan
|
||||||
|
|||||||
@@ -1,6 +1,20 @@
|
|||||||
**Please include the full output of youtube-dl when run with `-v`**.
|
**Please include the full output of youtube-dl when run with `-v`**, i.e. add `-v` flag to your command line, copy the **whole** output and post it in the issue body wrapped in \`\`\` for better formatting. It should look similar to this:
|
||||||
|
```
|
||||||
|
$ youtube-dl -v http://www.youtube.com/watch?v=BaW_jenozKcj
|
||||||
|
[debug] System config: []
|
||||||
|
[debug] User config: []
|
||||||
|
[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] youtube-dl version 2015.12.06
|
||||||
|
[debug] Git HEAD: 135392e
|
||||||
|
[debug] Python version 2.6.6 - Windows-2003Server-5.2.3790-SP2
|
||||||
|
[debug] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4
|
||||||
|
[debug] Proxy map: {}
|
||||||
|
...
|
||||||
|
```
|
||||||
|
**Do not post screenshots of verbose log only plain text is acceptable.**
|
||||||
|
|
||||||
The output (including the first lines) contain important debugging information. Issues without the full output are often not reproducible and therefore do not get solved in short order, if ever.
|
The output (including the first lines) contains important debugging information. Issues without the full output are often not reproducible and therefore do not get solved in short order, if ever.
|
||||||
|
|
||||||
Please re-read your issue once again to avoid a couple of common mistakes (you can and should use this as a checklist):
|
Please re-read your issue once again to avoid a couple of common mistakes (you can and should use this as a checklist):
|
||||||
|
|
||||||
@@ -14,19 +28,21 @@ So please elaborate on what feature you are requesting, or what bug you want to
|
|||||||
- How it could be fixed
|
- How it could be fixed
|
||||||
- How your proposed solution would look like
|
- How your proposed solution would look like
|
||||||
|
|
||||||
If your report is shorter than two lines, it is almost certainly missing some of these, which makes it hard for us to respond to it. We're often too polite to close the issue outright, but the missing info makes misinterpretation likely. As a commiter myself, I often get frustrated by these issues, since the only possible way for me to move forward on them is to ask for clarification over and over.
|
If your report is shorter than two lines, it is almost certainly missing some of these, which makes it hard for us to respond to it. We're often too polite to close the issue outright, but the missing info makes misinterpretation likely. As a committer myself, I often get frustrated by these issues, since the only possible way for me to move forward on them is to ask for clarification over and over.
|
||||||
|
|
||||||
For bug reports, this means that your report should contain the *complete* output of youtube-dl when called with the -v flag. The error message you get for (most) bugs even says so, but you would not believe how many of our bug reports do not contain this information.
|
For bug reports, this means that your report should contain the *complete* output of youtube-dl when called with the `-v` flag. The error message you get for (most) bugs even says so, but you would not believe how many of our bug reports do not contain this information.
|
||||||
|
|
||||||
Site support requests **must contain an example URL**. An example URL is a URL you might want to download, like http://www.youtube.com/watch?v=BaW_jenozKc . There should be an obvious video present. Except under very special circumstances, the main page of a video service (e.g. http://www.youtube.com/ ) is *not* an example URL.
|
If your server has multiple IPs or you suspect censorship, adding `--call-home` may be a good idea to get more diagnostics. If the error is `ERROR: Unable to extract ...` and you cannot reproduce it from multiple countries, add `--dump-pages` (warning: this will yield a rather large output, redirect it to the file `log.txt` by adding `>log.txt 2>&1` to your command-line) or upload the `.dump` files you get when you add `--write-pages` [somewhere](https://gist.github.com/).
|
||||||
|
|
||||||
|
**Site support requests must contain an example URL**. An example URL is a URL you might want to download, like `http://www.youtube.com/watch?v=BaW_jenozKc`. There should be an obvious video present. Except under very special circumstances, the main page of a video service (e.g. `http://www.youtube.com/`) is *not* an example URL.
|
||||||
|
|
||||||
### Are you using the latest version?
|
### Are you using the latest version?
|
||||||
|
|
||||||
Before reporting any issue, type youtube-dl -U. This should report that you're up-to-date. About 20% of the reports we receive are already fixed, but people are using outdated versions. This goes for feature requests as well.
|
Before reporting any issue, type `youtube-dl -U`. This should report that you're up-to-date. About 20% of the reports we receive are already fixed, but people are using outdated versions. This goes for feature requests as well.
|
||||||
|
|
||||||
### Is the issue already documented?
|
### Is the issue already documented?
|
||||||
|
|
||||||
Make sure that someone has not already opened the issue you're trying to open. Search at the top of the window or at https://github.com/rg3/youtube-dl/search?type=Issues . If there is an issue, feel free to write something along the lines of "This affects me as well, with version 2015.01.01. Here is some more information on the issue: ...". While some issues may be old, a new post into them often spurs rapid activity.
|
Make sure that someone has not already opened the issue you're trying to open. Search at the top of the window or browse the [GitHub Issues](https://github.com/rg3/youtube-dl/search?type=Issues) of this repository. If there is an issue, feel free to write something along the lines of "This affects me as well, with version 2015.01.01. Here is some more information on the issue: ...". While some issues may be old, a new post into them often spurs rapid activity.
|
||||||
|
|
||||||
### Why are existing options not enough?
|
### Why are existing options not enough?
|
||||||
|
|
||||||
@@ -112,18 +128,19 @@ If you want to add support for a new site, you can follow this quick list (assum
|
|||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
# TODO more code goes here, for example ...
|
# TODO more code goes here, for example ...
|
||||||
title = self._html_search_regex(r'<h1>(.*?)</h1>', webpage, 'title')
|
title = self._html_search_regex(r'<h1>(.+?)</h1>', webpage, 'title')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': self._og_search_description(webpage),
|
'description': self._og_search_description(webpage),
|
||||||
|
'uploader': self._search_regex(r'<div[^>]+id="uploader"[^>]*>([^<]+)<', webpage, 'uploader', fatal=False),
|
||||||
# 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/__init__.py`](https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/__init__.py).
|
5. Add an import in [`youtube_dl/extractor/__init__.py`](https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/__init__.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 be then be named `TestDownload.test_YourExtractor`, `TestDownload.test_YourExtractor_1`, `TestDownload.test_YourExtractor_2`, etc.
|
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.
|
||||||
7. Have a look at [`youtube_dl/common/extractor/common.py`](https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/common.py) for possible helper methods and a [detailed description of what your extractor should return](https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/common.py#L38). Add tests and code for as many as you want.
|
7. Have a look at [`youtube_dl/extractor/common.py`](https://github.com/rg3/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/rg3/youtube-dl/blob/master/youtube_dl/extractor/common.py#L62-L200). Add tests and code for as many as you want.
|
||||||
8. If you can, check the code with [flake8](https://pypi.python.org/pypi/flake8).
|
8. If you can, check the code with [flake8](https://pypi.python.org/pypi/flake8).
|
||||||
9. When the tests pass, [add](http://git-scm.com/docs/git-add) the new files and [commit](http://git-scm.com/docs/git-commit) them and [push](http://git-scm.com/docs/git-push) the result, like this:
|
9. When the tests pass, [add](http://git-scm.com/docs/git-add) the new files and [commit](http://git-scm.com/docs/git-commit) them and [push](http://git-scm.com/docs/git-push) the result, like this:
|
||||||
|
|
||||||
|
|||||||
19
Makefile
19
Makefile
@@ -1,7 +1,8 @@
|
|||||||
all: youtube-dl README.md CONTRIBUTING.md README.txt youtube-dl.1 youtube-dl.bash-completion youtube-dl.zsh youtube-dl.fish supportedsites
|
all: youtube-dl README.md CONTRIBUTING.md README.txt youtube-dl.1 youtube-dl.bash-completion youtube-dl.zsh youtube-dl.fish supportedsites
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf youtube-dl.1.temp.md youtube-dl.1 youtube-dl.bash-completion README.txt MANIFEST build/ dist/ .coverage cover/ youtube-dl.tar.gz youtube-dl.zsh youtube-dl.fish *.dump *.part *.info.json *.mp4 *.flv *.mp3 CONTRIBUTING.md.tmp youtube-dl youtube-dl.exe
|
rm -rf youtube-dl.1.temp.md youtube-dl.1 youtube-dl.bash-completion README.txt MANIFEST build/ dist/ .coverage cover/ youtube-dl.tar.gz youtube-dl.zsh youtube-dl.fish *.dump *.part *.info.json *.mp4 *.flv *.mp3 *.avi CONTRIBUTING.md.tmp youtube-dl youtube-dl.exe
|
||||||
|
find . -name "*.pyc" -delete
|
||||||
|
|
||||||
PREFIX ?= /usr/local
|
PREFIX ?= /usr/local
|
||||||
BINDIR ?= $(PREFIX)/bin
|
BINDIR ?= $(PREFIX)/bin
|
||||||
@@ -43,7 +44,7 @@ test:
|
|||||||
ot: offlinetest
|
ot: offlinetest
|
||||||
|
|
||||||
offlinetest: codetest
|
offlinetest: codetest
|
||||||
nosetests --verbose test --exclude test_download --exclude test_age_restriction --exclude test_subtitles --exclude test_write_annotations --exclude test_youtube_lists
|
nosetests --verbose test --exclude test_download.py --exclude test_age_restriction.py --exclude test_subtitles.py --exclude test_write_annotations.py --exclude test_youtube_lists.py
|
||||||
|
|
||||||
tar: youtube-dl.tar.gz
|
tar: youtube-dl.tar.gz
|
||||||
|
|
||||||
@@ -60,34 +61,34 @@ youtube-dl: youtube_dl/*.py youtube_dl/*/*.py
|
|||||||
chmod a+x youtube-dl
|
chmod a+x youtube-dl
|
||||||
|
|
||||||
README.md: youtube_dl/*.py youtube_dl/*/*.py
|
README.md: youtube_dl/*.py youtube_dl/*/*.py
|
||||||
COLUMNS=80 python youtube_dl/__main__.py --help | python devscripts/make_readme.py
|
COLUMNS=80 $(PYTHON) youtube_dl/__main__.py --help | $(PYTHON) devscripts/make_readme.py
|
||||||
|
|
||||||
CONTRIBUTING.md: README.md
|
CONTRIBUTING.md: README.md
|
||||||
python devscripts/make_contributing.py README.md CONTRIBUTING.md
|
$(PYTHON) devscripts/make_contributing.py README.md CONTRIBUTING.md
|
||||||
|
|
||||||
supportedsites:
|
supportedsites:
|
||||||
python devscripts/make_supportedsites.py docs/supportedsites.md
|
$(PYTHON) devscripts/make_supportedsites.py docs/supportedsites.md
|
||||||
|
|
||||||
README.txt: README.md
|
README.txt: README.md
|
||||||
pandoc -f markdown -t plain README.md -o README.txt
|
pandoc -f markdown -t plain README.md -o README.txt
|
||||||
|
|
||||||
youtube-dl.1: README.md
|
youtube-dl.1: README.md
|
||||||
python devscripts/prepare_manpage.py >youtube-dl.1.temp.md
|
$(PYTHON) devscripts/prepare_manpage.py >youtube-dl.1.temp.md
|
||||||
pandoc -s -f markdown -t man youtube-dl.1.temp.md -o youtube-dl.1
|
pandoc -s -f markdown -t man youtube-dl.1.temp.md -o youtube-dl.1
|
||||||
rm -f youtube-dl.1.temp.md
|
rm -f youtube-dl.1.temp.md
|
||||||
|
|
||||||
youtube-dl.bash-completion: youtube_dl/*.py youtube_dl/*/*.py devscripts/bash-completion.in
|
youtube-dl.bash-completion: youtube_dl/*.py youtube_dl/*/*.py devscripts/bash-completion.in
|
||||||
python devscripts/bash-completion.py
|
$(PYTHON) devscripts/bash-completion.py
|
||||||
|
|
||||||
bash-completion: youtube-dl.bash-completion
|
bash-completion: youtube-dl.bash-completion
|
||||||
|
|
||||||
youtube-dl.zsh: youtube_dl/*.py youtube_dl/*/*.py devscripts/zsh-completion.in
|
youtube-dl.zsh: youtube_dl/*.py youtube_dl/*/*.py devscripts/zsh-completion.in
|
||||||
python devscripts/zsh-completion.py
|
$(PYTHON) devscripts/zsh-completion.py
|
||||||
|
|
||||||
zsh-completion: youtube-dl.zsh
|
zsh-completion: youtube-dl.zsh
|
||||||
|
|
||||||
youtube-dl.fish: youtube_dl/*.py youtube_dl/*/*.py devscripts/fish-completion.in
|
youtube-dl.fish: youtube_dl/*.py youtube_dl/*/*.py devscripts/fish-completion.in
|
||||||
python devscripts/fish-completion.py
|
$(PYTHON) devscripts/fish-completion.py
|
||||||
|
|
||||||
fish-completion: youtube-dl.fish
|
fish-completion: youtube-dl.fish
|
||||||
|
|
||||||
|
|||||||
626
README.md
626
README.md
@@ -5,9 +5,11 @@ youtube-dl - download videos from youtube.com or other video platforms
|
|||||||
- [OPTIONS](#options)
|
- [OPTIONS](#options)
|
||||||
- [CONFIGURATION](#configuration)
|
- [CONFIGURATION](#configuration)
|
||||||
- [OUTPUT TEMPLATE](#output-template)
|
- [OUTPUT TEMPLATE](#output-template)
|
||||||
|
- [FORMAT SELECTION](#format-selection)
|
||||||
- [VIDEO SELECTION](#video-selection)
|
- [VIDEO SELECTION](#video-selection)
|
||||||
- [FAQ](#faq)
|
- [FAQ](#faq)
|
||||||
- [DEVELOPER INSTRUCTIONS](#developer-instructions)
|
- [DEVELOPER INSTRUCTIONS](#developer-instructions)
|
||||||
|
- [EMBEDDING YOUTUBE-DL](#embedding-youtube-dl)
|
||||||
- [BUGS](#bugs)
|
- [BUGS](#bugs)
|
||||||
- [COPYRIGHT](#copyright)
|
- [COPYRIGHT](#copyright)
|
||||||
|
|
||||||
@@ -16,12 +18,12 @@ youtube-dl - download videos from youtube.com or other video platforms
|
|||||||
To install it right away for all UNIX users (Linux, OS X, etc.), type:
|
To install it right away for all UNIX users (Linux, OS X, etc.), type:
|
||||||
|
|
||||||
sudo curl https://yt-dl.org/latest/youtube-dl -o /usr/local/bin/youtube-dl
|
sudo curl https://yt-dl.org/latest/youtube-dl -o /usr/local/bin/youtube-dl
|
||||||
sudo chmod a+x /usr/local/bin/youtube-dl
|
sudo chmod a+rx /usr/local/bin/youtube-dl
|
||||||
|
|
||||||
If you do not have curl, you can alternatively use a recent wget:
|
If you do not have curl, you can alternatively use a recent wget:
|
||||||
|
|
||||||
sudo wget https://yt-dl.org/downloads/latest/youtube-dl -O /usr/local/bin/youtube-dl
|
sudo wget https://yt-dl.org/downloads/latest/youtube-dl -O /usr/local/bin/youtube-dl
|
||||||
sudo chmod a+x /usr/local/bin/youtube-dl
|
sudo chmod a+rx /usr/local/bin/youtube-dl
|
||||||
|
|
||||||
Windows users can [download a .exe file](https://yt-dl.org/latest/youtube-dl.exe) and place it in their home directory or any other location on their [PATH](http://en.wikipedia.org/wiki/PATH_%28variable%29).
|
Windows users can [download a .exe file](https://yt-dl.org/latest/youtube-dl.exe) and place it in their home directory or any other location on their [PATH](http://en.wikipedia.org/wiki/PATH_%28variable%29).
|
||||||
|
|
||||||
@@ -33,7 +35,7 @@ You can also use pip:
|
|||||||
|
|
||||||
sudo pip install youtube-dl
|
sudo pip install youtube-dl
|
||||||
|
|
||||||
Alternatively, refer to the developer instructions below for how to check out and work with the git repository. For further options, including PGP signatures, see https://rg3.github.io/youtube-dl/download.html .
|
Alternatively, refer to the [developer instructions](#developer-instructions) for how to check out and work with the git repository. For further options, including PGP signatures, see the [youtube-dl Download Page](https://rg3.github.io/youtube-dl/download.html).
|
||||||
|
|
||||||
# DESCRIPTION
|
# DESCRIPTION
|
||||||
**youtube-dl** is a small command-line program to download videos from
|
**youtube-dl** is a small command-line program to download videos from
|
||||||
@@ -45,24 +47,25 @@ which means you can modify it, redistribute it or use it however you like.
|
|||||||
youtube-dl [OPTIONS] URL [URL...]
|
youtube-dl [OPTIONS] URL [URL...]
|
||||||
|
|
||||||
# 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. Make
|
||||||
sure that you have sufficient permissions
|
sure that you have sufficient permissions
|
||||||
(run with sudo if needed)
|
(run with sudo if needed)
|
||||||
-i, --ignore-errors continue on download errors, for example to
|
-i, --ignore-errors Continue on download errors, for example to
|
||||||
skip unavailable videos in a playlist
|
skip unavailable videos in a playlist
|
||||||
--abort-on-error Abort downloading of further videos (in the
|
--abort-on-error Abort downloading of further videos (in the
|
||||||
playlist or the command line) if an error
|
playlist or the command line) if an error
|
||||||
occurs
|
occurs
|
||||||
--dump-user-agent display the current browser identification
|
--dump-user-agent Display the current browser identification
|
||||||
--list-extractors List all supported extractors and the URLs
|
--list-extractors List all supported extractors
|
||||||
they would handle
|
|
||||||
--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
|
||||||
|
extractor
|
||||||
--default-search PREFIX Use this prefix for unqualified URLs. For
|
--default-search PREFIX Use this prefix for unqualified URLs. For
|
||||||
example "gvsearch2:" downloads two videos
|
example "gvsearch2:" downloads two videos
|
||||||
from google videos for youtube-dl "large
|
from google videos for youtube-dl "large
|
||||||
apple". Use the value "auto" to let
|
apple". Use the value "auto" to let
|
||||||
youtube-dl guess ("auto_warning" to emit a
|
youtube-dl guess ("auto_warning" to emit a
|
||||||
warning when guessing). "error" just throws
|
warning when guessing). "error" just throws
|
||||||
@@ -77,7 +80,7 @@ which means you can modify it, redistribute it or use it however you like.
|
|||||||
on Windows)
|
on Windows)
|
||||||
--flat-playlist Do not extract the videos of a playlist,
|
--flat-playlist Do not extract the videos of a playlist,
|
||||||
only list them.
|
only list them.
|
||||||
--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 proxy. Pass in
|
--proxy URL Use the specified HTTP/HTTPS proxy. Pass in
|
||||||
@@ -90,37 +93,42 @@ which means you can modify it, redistribute it or use it however you like.
|
|||||||
(experimental)
|
(experimental)
|
||||||
-6, --force-ipv6 Make all connections via IPv6
|
-6, --force-ipv6 Make all connections via IPv6
|
||||||
(experimental)
|
(experimental)
|
||||||
|
--cn-verification-proxy URL Use this proxy to verify the IP address for
|
||||||
|
some Chinese sites. The default proxy
|
||||||
|
specified by --proxy (or none, if the
|
||||||
|
options is not present) is used for the
|
||||||
|
actual downloading. (experimental)
|
||||||
|
|
||||||
## 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 1)
|
||||||
--playlist-end NUMBER playlist video to end at (default is last)
|
--playlist-end NUMBER Playlist video to end at (default is last)
|
||||||
--playlist-items ITEM_SPEC playlist video items to download. Specify
|
--playlist-items ITEM_SPEC Playlist video items to download. Specify
|
||||||
indices of the videos in the playlist
|
indices of the videos in the playlist
|
||||||
seperated by commas like: "--playlist-items
|
separated by commas like: "--playlist-items
|
||||||
1,2,5,8" if you want to download videos
|
1,2,5,8" if you want to download videos
|
||||||
indexed 1, 2, 5, 8 in the playlist. You can
|
indexed 1, 2, 5, 8 in the playlist. You can
|
||||||
specify range: "--playlist-items
|
specify range: "--playlist-items
|
||||||
1-3,7,10-13", it will download the videos
|
1-3,7,10-13", it will download the videos
|
||||||
at index 1, 2, 3, 7, 10, 11, 12 and 13.
|
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 (regex or
|
||||||
caseless sub-string)
|
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 SIZE
|
||||||
(e.g. 50k or 44.6m)
|
(e.g. 50k or 44.6m)
|
||||||
--date DATE download only videos uploaded in this date
|
--date DATE Download only videos uploaded in this date
|
||||||
--datebefore DATE download only videos uploaded on or before
|
--datebefore DATE Download only videos uploaded on or before
|
||||||
this date (i.e. inclusive)
|
this date (i.e. inclusive)
|
||||||
--dateafter DATE download only videos uploaded on or after
|
--dateafter DATE Download only videos uploaded on or after
|
||||||
this date (i.e. inclusive)
|
this date (i.e. inclusive)
|
||||||
--min-views COUNT Do not download any videos with less than
|
--min-views COUNT Do not download any videos with less than
|
||||||
COUNT views
|
COUNT views
|
||||||
--max-views COUNT Do not download any videos with more than
|
--max-views COUNT Do not download any videos with more than
|
||||||
COUNT views
|
COUNT views
|
||||||
--match-filter FILTER (Experimental) Generic video filter.
|
--match-filter FILTER Generic video filter (experimental).
|
||||||
Specify any key (see help for -o for a list
|
Specify any key (see help for -o for a list
|
||||||
of available keys) to match if the key is
|
of available keys) to match if the key is
|
||||||
present, !key to check if the key is not
|
present, !key to check if the key is not
|
||||||
@@ -135,11 +143,13 @@ which means you can modify it, redistribute it or use it however you like.
|
|||||||
less than 50 times (or the dislike
|
less than 50 times (or the dislike
|
||||||
functionality is not available at the given
|
functionality is not available at the given
|
||||||
service), but who also have a description,
|
service), but who also have a description,
|
||||||
use --match-filter "like_count > 100 &
|
use --match-filter "like_count > 100 &
|
||||||
dislike_count <? 50 & description" .
|
dislike_count <? 50 & description" .
|
||||||
--no-playlist If the URL refers to a video and a
|
--no-playlist Download only the video, if the URL refers
|
||||||
playlist, download only the video.
|
to a video and a playlist.
|
||||||
--age-limit YEARS download only videos suitable for the given
|
--yes-playlist Download the playlist, if the URL refers to
|
||||||
|
a video and a playlist.
|
||||||
|
--age-limit YEARS Download only videos suitable for the given
|
||||||
age
|
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
|
||||||
@@ -148,30 +158,36 @@ which means you can modify it, redistribute it or use it however you like.
|
|||||||
(experimental)
|
(experimental)
|
||||||
|
|
||||||
## Download Options:
|
## Download Options:
|
||||||
-r, --rate-limit LIMIT maximum download rate in bytes per second
|
-r, --rate-limit LIMIT Maximum download rate in bytes per second
|
||||||
(e.g. 50K or 4.2M)
|
(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".
|
||||||
--buffer-size SIZE size of download buffer (e.g. 1024 or 16K)
|
--buffer-size SIZE Size of download buffer (e.g. 1024 or 16K)
|
||||||
(default is 1024)
|
(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 value
|
||||||
of SIZE.
|
of SIZE.
|
||||||
--playlist-reverse Download playlist videos in reverse order
|
--playlist-reverse Download playlist videos in reverse order
|
||||||
--xattr-set-filesize (experimental) set file xattribute
|
--xattr-set-filesize Set file xattribute ytdl.filesize with
|
||||||
ytdl.filesize with expected filesize
|
expected filesize (experimental)
|
||||||
--hls-prefer-native (experimental) Use the native HLS
|
--hls-prefer-native Use the native HLS downloader instead of
|
||||||
downloader instead of ffmpeg.
|
ffmpeg (experimental)
|
||||||
--external-downloader COMMAND (experimental) Use the specified external
|
--hls-use-mpegts Use the mpegts container for HLS videos,
|
||||||
downloader. Currently supports
|
allowing to play the video while
|
||||||
aria2c,curl,wget
|
downloading (some players may not be able
|
||||||
|
to play it)
|
||||||
|
--external-downloader COMMAND Use the specified external downloader.
|
||||||
|
Currently supports
|
||||||
|
aria2c,axel,curl,httpie,wget
|
||||||
|
--external-downloader-args ARGS Give these arguments to the external
|
||||||
|
downloader
|
||||||
|
|
||||||
## Filesystem Options:
|
## Filesystem Options:
|
||||||
-a, --batch-file FILE file containing URLs to download ('-' for
|
-a, --batch-file FILE File containing URLs to download ('-' for
|
||||||
stdin)
|
stdin)
|
||||||
--id use only video ID in file name
|
--id Use only video ID in file name
|
||||||
-o, --output TEMPLATE output filename template. Use %(title)s to
|
-o, --output TEMPLATE Output filename template. Use %(title)s to
|
||||||
get the title, %(uploader)s for the
|
get the title, %(uploader)s for the
|
||||||
uploader name, %(uploader_id)s for the
|
uploader name, %(uploader_id)s for the
|
||||||
uploader nickname if different,
|
uploader nickname if different,
|
||||||
@@ -180,7 +196,7 @@ which means you can modify it, redistribute it or use it however you like.
|
|||||||
filename extension, %(format)s for the
|
filename extension, %(format)s for the
|
||||||
format description (like "22 - 1280x720" or
|
format description (like "22 - 1280x720" or
|
||||||
"HD"), %(format_id)s for the unique id of
|
"HD"), %(format_id)s for the unique id of
|
||||||
the format (like Youtube's itags: "137"),
|
the format (like YouTube's itags: "137"),
|
||||||
%(upload_date)s for the upload date
|
%(upload_date)s for the upload date
|
||||||
(YYYYMMDD), %(extractor)s for the provider
|
(YYYYMMDD), %(extractor)s for the provider
|
||||||
(youtube, metacafe, etc), %(id)s for the
|
(youtube, metacafe, etc), %(id)s for the
|
||||||
@@ -197,37 +213,38 @@ which means you can modify it, redistribute it or use it however you like.
|
|||||||
download to a different directory, for
|
download to a different directory, for
|
||||||
example with -o '/my/downloads/%(uploader)s
|
example with -o '/my/downloads/%(uploader)s
|
||||||
/%(title)s-%(id)s.%(ext)s' .
|
/%(title)s-%(id)s.%(ext)s' .
|
||||||
--autonumber-size NUMBER Specifies the number of digits in
|
--autonumber-size NUMBER Specify the number of digits in
|
||||||
%(autonumber)s when it is present in output
|
%(autonumber)s when it is present in output
|
||||||
filename template or --auto-number option
|
filename template or --auto-number option
|
||||||
is given
|
is given
|
||||||
--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
|
||||||
-A, --auto-number [deprecated; use -o
|
-A, --auto-number [deprecated; use -o
|
||||||
"%(autonumber)s-%(title)s.%(ext)s" ] number
|
"%(autonumber)s-%(title)s.%(ext)s" ] Number
|
||||||
downloaded files starting from 00000
|
downloaded files starting from 00000
|
||||||
-t, --title [deprecated] use title in file name
|
-t, --title [deprecated] Use title in file name
|
||||||
(default)
|
(default)
|
||||||
-l, --literal [deprecated] alias of --title
|
-l, --literal [deprecated] Alias of --title
|
||||||
-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 files.
|
||||||
By default, youtube-dl will resume
|
By default, youtube-dl will resume
|
||||||
downloads if possible.
|
downloads if possible.
|
||||||
--no-continue do not resume partially downloaded files
|
--no-continue Do not resume partially downloaded files
|
||||||
(restart from beginning)
|
(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 set
|
||||||
the file modification time
|
the file modification time
|
||||||
--write-description write video description to a .description
|
--write-description Write video description to a .description
|
||||||
file
|
file
|
||||||
--write-info-json write video metadata to a .info.json file
|
--write-info-json Write video metadata to a .info.json file
|
||||||
--write-annotations write video annotations to a .annotation
|
--write-annotations Write video annotations to a
|
||||||
file
|
.annotations.xml file
|
||||||
--load-info FILE json file containing the video information
|
--load-info FILE JSON file containing the video information
|
||||||
(created with the "--write-json" option)
|
(created with the "--write-info-json"
|
||||||
--cookies FILE file to read cookies from and dump cookie
|
option)
|
||||||
|
--cookies FILE File to read cookies from and dump cookie
|
||||||
jar in
|
jar in
|
||||||
--cache-dir DIR Location in the filesystem where youtube-dl
|
--cache-dir DIR Location in the filesystem where youtube-dl
|
||||||
can store some downloaded information
|
can store some downloaded information
|
||||||
@@ -240,59 +257,58 @@ which means you can modify it, redistribute it or use it however you like.
|
|||||||
--rm-cache-dir Delete all filesystem cache files
|
--rm-cache-dir Delete all filesystem cache files
|
||||||
|
|
||||||
## Thumbnail images:
|
## Thumbnail images:
|
||||||
--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 disk
|
||||||
--list-thumbnails Simulate and list all available thumbnail
|
--list-thumbnails Simulate and list all available thumbnail
|
||||||
formats
|
formats
|
||||||
|
|
||||||
## Verbosity / Simulation Options:
|
## Verbosity / Simulation Options:
|
||||||
-q, --quiet activates 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 write
|
||||||
anything to disk
|
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 information.
|
||||||
See --output for a description of available
|
See --output for a description of available
|
||||||
keys.
|
keys.
|
||||||
-J, --dump-single-json simulate, quiet but print JSON information
|
-J, --dump-single-json Simulate, quiet but print JSON information
|
||||||
for each command-line argument. If the URL
|
for each command-line argument. If the URL
|
||||||
refers to a playlist, dump the whole
|
refers to a playlist, dump the whole
|
||||||
playlist information in a single line.
|
playlist information in a single line.
|
||||||
--print-json Be quiet and print the video information as
|
--print-json Be quiet and print the video information as
|
||||||
JSON (video is still being downloaded).
|
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-intermediate-pages print downloaded pages to debug problems
|
--dump-pages Print downloaded pages encoded using base64
|
||||||
(very verbose)
|
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
|
-C, --call-home Contact the youtube-dl server for debugging
|
||||||
debugging.
|
|
||||||
--no-call-home Do NOT contact the youtube-dl server for
|
--no-call-home Do NOT contact the youtube-dl server for
|
||||||
debugging.
|
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 retrieve
|
||||||
information about the video. (Currently
|
information about the video. (Currently
|
||||||
supported only for YouTube)
|
supported only for YouTube)
|
||||||
--user-agent UA specify a custom user agent
|
--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 video
|
||||||
access is restricted to one domain
|
access is restricted to one domain
|
||||||
--add-header FIELD:VALUE specify a custom HTTP header and its value,
|
--add-header FIELD:VALUE Specify a custom HTTP header and its value,
|
||||||
separated by a colon ':'. You can use this
|
separated by a colon ':'. You can use this
|
||||||
option multiple times
|
option multiple times
|
||||||
--bidi-workaround Work around terminals that lack
|
--bidi-workaround Work around terminals that lack
|
||||||
@@ -302,97 +318,79 @@ which means you can modify it, redistribute it or use it however you like.
|
|||||||
download.
|
download.
|
||||||
|
|
||||||
## Video Format Options:
|
## Video Format Options:
|
||||||
-f, --format FORMAT video format code, specify the order of
|
-f, --format FORMAT Video format code, see the "FORMAT
|
||||||
preference using slashes, as in -f 22/17/18
|
SELECTION" for all the info
|
||||||
. Instead of format codes, you can select
|
--all-formats Download all available video formats
|
||||||
by extension for the extensions aac, m4a,
|
--prefer-free-formats Prefer free video formats unless a specific
|
||||||
mp3, mp4, ogg, wav, webm. You can also use
|
|
||||||
the special names "best", "bestvideo",
|
|
||||||
"bestaudio", "worst". You can filter the
|
|
||||||
video results by putting a condition in
|
|
||||||
brackets, as in -f "best[height=720]" (or
|
|
||||||
-f "[filesize>10M]"). This works for
|
|
||||||
filesize, height, width, tbr, abr, vbr,
|
|
||||||
asr, and fps and the comparisons <, <=, >,
|
|
||||||
>=, =, != and for ext, acodec, vcodec,
|
|
||||||
container, and protocol and the comparisons
|
|
||||||
=, != . Formats for which the value is not
|
|
||||||
known are excluded unless you put a
|
|
||||||
question mark (?) after the operator. You
|
|
||||||
can combine format filters, so -f "[height
|
|
||||||
<=? 720][tbr>500]" selects up to 720p
|
|
||||||
videos (or videos where the height is not
|
|
||||||
known) with a bitrate of at least 500
|
|
||||||
KBit/s. By default, youtube-dl will pick
|
|
||||||
the best quality. Use commas to download
|
|
||||||
multiple audio formats, such as -f
|
|
||||||
136/137/mp4/bestvideo,140/m4a/bestaudio.
|
|
||||||
You can merge the video and audio of two
|
|
||||||
formats into a single file using -f <video-
|
|
||||||
format>+<audio-format> (requires ffmpeg or
|
|
||||||
avconv), for example -f
|
|
||||||
bestvideo+bestaudio.
|
|
||||||
--all-formats download all available video formats
|
|
||||||
--prefer-free-formats prefer free video formats unless a specific
|
|
||||||
one is requested
|
one is requested
|
||||||
--max-quality FORMAT highest quality format to download
|
-F, --list-formats List all available formats of requested
|
||||||
-F, --list-formats list all available formats
|
videos
|
||||||
--youtube-skip-dash-manifest Do not download the DASH manifest on
|
--youtube-skip-dash-manifest Do not download the DASH manifests and
|
||||||
YouTube videos
|
related data on YouTube videos
|
||||||
--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 automatic subtitle file (youtube
|
--write-auto-sub Write automatically generated subtitle file
|
||||||
only)
|
(YouTube only)
|
||||||
--all-subs downloads all the available subtitles of
|
--all-subs Download all the available subtitles of the
|
||||||
the video
|
video
|
||||||
--list-subs lists all available subtitles for the video
|
--list-subs List all available subtitles for the video
|
||||||
--sub-format FORMAT subtitle format (default=srt) ([sbv/vtt]
|
--sub-format FORMAT Subtitle format, accepts formats
|
||||||
youtube only)
|
preference, for example: "srt" or
|
||||||
--sub-lang LANGS languages of the subtitles to download
|
"ass/srt/best"
|
||||||
(optional) separated by commas, use IETF
|
--sub-lang LANGS Languages of the subtitles to download
|
||||||
language tags like 'en,pt'
|
(optional) separated by commas, use --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 left
|
||||||
out, youtube-dl will ask interactively.
|
out, youtube-dl will ask interactively.
|
||||||
-2, --twofactor TWOFACTOR two-factor auth code
|
-2, --twofactor TWOFACTOR Two-factor auth code
|
||||||
-n, --netrc use .netrc authentication data
|
-n, --netrc Use .netrc authentication data
|
||||||
--video-password PASSWORD video password (vimeo, smotri)
|
--video-password PASSWORD Video password (vimeo, smotri, youku)
|
||||||
|
|
||||||
## 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 or avconv and ffprobe or
|
||||||
avprobe)
|
avprobe)
|
||||||
--audio-format FORMAT "best", "aac", "vorbis", "mp3", "m4a",
|
--audio-format FORMAT Specify audio format: "best", "aac",
|
||||||
"opus", or "wav"; "best" by default
|
"vorbis", "mp3", "m4a", "opus", or "wav";
|
||||||
--audio-quality QUALITY ffmpeg/avconv audio quality specification,
|
"best" by default
|
||||||
insert a value between 0 (better) and 9
|
--audio-quality QUALITY Specify ffmpeg/avconv audio quality, insert
|
||||||
(worse) for VBR or a specific bitrate like
|
a value between 0 (better) and 9 (worse)
|
||||||
128K (default 5)
|
for VBR or a specific bitrate 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)
|
mp4|flv|ogg|webm|mkv|avi)
|
||||||
-k, --keep-video keeps the video file on disk after the
|
--postprocessor-args ARGS Give these arguments to the postprocessor
|
||||||
post-processing; the video is erased by
|
-k, --keep-video Keep the video file on disk after the post-
|
||||||
default
|
processing; the video is erased by default
|
||||||
--no-post-overwrites do not overwrite post-processed files; the
|
--no-post-overwrites Do not overwrite post-processed files; the
|
||||||
post-processed files are overwritten by
|
post-processed files are overwritten by
|
||||||
default
|
default
|
||||||
--embed-subs embed subtitles in the video (only for mp4
|
--embed-subs Embed subtitles in the video (only for mkv
|
||||||
videos)
|
and mp4 videos)
|
||||||
--embed-thumbnail embed thumbnail in the audio as cover art
|
--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
|
||||||
--xattrs write metadata to the video file's xattrs
|
--metadata-from-title FORMAT Parse additional metadata like song title /
|
||||||
|
artist from the video title. The format
|
||||||
|
syntax is the same as --output, the parsed
|
||||||
|
parameters replace existing values.
|
||||||
|
Additional templates: %(album)s,
|
||||||
|
%(artist)s. Example: --metadata-from-title
|
||||||
|
"%(artist)s - %(title)s" matches a title
|
||||||
|
like "Coldplay - Paradise"
|
||||||
|
--xattrs Write metadata to the video file's xattrs
|
||||||
(using dublin core and xdg standards)
|
(using dublin core and xdg standards)
|
||||||
--fixup POLICY Automatically correct known faults of the
|
--fixup POLICY Automatically correct known faults of the
|
||||||
file. One of never (do nothing), warn (only
|
file. One of never (do nothing), warn (only
|
||||||
emit a warning), detect_or_warn(the
|
emit a warning), detect_or_warn (the
|
||||||
default; fix file if we can, warn
|
default; fix file if we can, warn
|
||||||
otherwise)
|
otherwise)
|
||||||
--prefer-avconv Prefer avconv over ffmpeg for running the
|
--prefer-avconv Prefer avconv over ffmpeg for running the
|
||||||
@@ -406,40 +404,213 @@ which means you can modify it, redistribute it or use it however you like.
|
|||||||
downloading, similar to find's -exec
|
downloading, similar to find's -exec
|
||||||
syntax. Example: --exec 'adb push {}
|
syntax. Example: --exec 'adb push {}
|
||||||
/sdcard/Music/ && rm {}'
|
/sdcard/Music/ && rm {}'
|
||||||
|
--convert-subs FORMAT Convert the subtitles to other format
|
||||||
|
(currently supported: srt|ass|vtt)
|
||||||
|
|
||||||
# CONFIGURATION
|
# CONFIGURATION
|
||||||
|
|
||||||
You can configure youtube-dl by placing default arguments (such as `--extract-audio --no-mtime` to always extract the audio and not copy the mtime) into `/etc/youtube-dl.conf` and/or `~/.config/youtube-dl/config`. On Windows, the configuration file locations are `%APPDATA%\youtube-dl\config.txt` and `C:\Users\<user name>\youtube-dl.conf`.
|
You can configure youtube-dl by placing any supported command line option to a configuration file. On Linux, the system wide configuration file is located at `/etc/youtube-dl.conf` and the user wide configuration file at `~/.config/youtube-dl/config`. On Windows, the user wide configuration file locations are `%APPDATA%\youtube-dl\config.txt` or `C:\Users\<user name>\youtube-dl.conf`. For example, with the following configuration file youtube-dl will always extract the audio, not copy the mtime and use a proxy:
|
||||||
|
```
|
||||||
|
--extract-audio
|
||||||
|
--no-mtime
|
||||||
|
--proxy 127.0.0.1:3128
|
||||||
|
```
|
||||||
|
|
||||||
|
You can use `--ignore-config` if you want to disable the configuration file for a particular youtube-dl run.
|
||||||
|
|
||||||
|
### Authentication with `.netrc` file
|
||||||
|
|
||||||
|
You may also want to configure automatic credentials storage for extractors that support authentication (by providing login and password with `--username` and `--password`) in order not to pass credentials as command line arguments on every youtube-dl execution and prevent tracking plain text passwords in the shell command history. You can achieve this using a [`.netrc` file](http://stackoverflow.com/tags/.netrc/info) on per extractor basis. For that you will need to create a`.netrc` file in your `$HOME` and restrict permissions to read/write by you only:
|
||||||
|
```
|
||||||
|
touch $HOME/.netrc
|
||||||
|
chmod a-rwx,u+rw $HOME/.netrc
|
||||||
|
```
|
||||||
|
After that you can add credentials for extractor in the following format, where *extractor* is the name of extractor in lowercase:
|
||||||
|
```
|
||||||
|
machine <extractor> login <login> password <password>
|
||||||
|
```
|
||||||
|
For example:
|
||||||
|
```
|
||||||
|
machine youtube login myaccount@gmail.com password my_youtube_password
|
||||||
|
machine twitch login my_twitch_account_name password my_twitch_password
|
||||||
|
```
|
||||||
|
To activate authentication with the `.netrc` file you should pass `--netrc` to youtube-dl or place it in the [configuration file](#configuration).
|
||||||
|
|
||||||
|
On Windows you may also need to setup the `%HOME%` environment variable manually.
|
||||||
|
|
||||||
# OUTPUT TEMPLATE
|
# OUTPUT TEMPLATE
|
||||||
|
|
||||||
The `-o` option allows users to indicate a template for the output file names. The basic usage is not to set any template arguments when downloading a single file, like in `youtube-dl -o funny_video.flv "http://some/video"`. However, it may contain special sequences that will be replaced when downloading each video. The special sequences have the format `%(NAME)s`. To clarify, that is a percent symbol followed by a name in parenthesis, followed by a lowercase S. Allowed names are:
|
The `-o` option allows users to indicate a template for the output file names. The basic usage is not to set any template arguments when downloading a single file, like in `youtube-dl -o funny_video.flv "http://some/video"`. However, it may contain special sequences that will be replaced when downloading each video. The special sequences have the format `%(NAME)s`. To clarify, that is a percent symbol followed by a name in parentheses, followed by a lowercase S. Allowed names are:
|
||||||
|
|
||||||
- `id`: The sequence will be replaced by the video identifier.
|
- `id`: Video identifier
|
||||||
- `url`: The sequence will be replaced by the video URL.
|
- `title`: Video title
|
||||||
- `uploader`: The sequence will be replaced by the nickname of the person who uploaded the video.
|
- `url`: Video URL
|
||||||
- `upload_date`: The sequence will be replaced by the upload date in YYYYMMDD format.
|
- `ext`: Video filename extension
|
||||||
- `title`: The sequence will be replaced by the video title.
|
- `alt_title`: A secondary title of the video
|
||||||
- `ext`: The sequence will be replaced by the appropriate extension (like flv or mp4).
|
- `display_id`: An alternative identifier for the video
|
||||||
- `epoch`: The sequence will be replaced by the Unix epoch when creating the file.
|
- `uploader`: Full name of the video uploader
|
||||||
- `autonumber`: The sequence will be replaced by a five-digit number that will be increased with each download, starting at zero.
|
- `creator`: The main artist who created the video
|
||||||
- `playlist`: The name or the id of the playlist that contains the video.
|
- `release_date`: The date (YYYYMMDD) when the video was released
|
||||||
- `playlist_index`: The index of the video in the playlist, a five-digit number.
|
- `timestamp`: UNIX timestamp of the moment the video became available
|
||||||
|
- `upload_date`: Video upload date (YYYYMMDD)
|
||||||
|
- `uploader_id`: Nickname or id of the video uploader
|
||||||
|
- `location`: Physical location where the video was filmed
|
||||||
|
- `duration`: Length of the video in seconds
|
||||||
|
- `view_count`: How many users have watched the video on the platform
|
||||||
|
- `like_count`: Number of positive ratings of the video
|
||||||
|
- `dislike_count`: Number of negative ratings of the video
|
||||||
|
- `repost_count`: Number of reposts of the video
|
||||||
|
- `average_rating`: Average rating give by users, the scale used depends on the webpage
|
||||||
|
- `comment_count`: Number of comments on the video
|
||||||
|
- `age_limit`: Age restriction for the video (years)
|
||||||
|
- `format`: A human-readable description of the format
|
||||||
|
- `format_id`: Format code specified by `--format`
|
||||||
|
- `format_note`: Additional info about the format
|
||||||
|
- `width`: Width of the video
|
||||||
|
- `height`: Height of the video
|
||||||
|
- `resolution`: Textual description of width and height
|
||||||
|
- `tbr`: Average bitrate of audio and video in KBit/s
|
||||||
|
- `abr`: Average audio bitrate in KBit/s
|
||||||
|
- `acodec`: Name of the audio codec in use
|
||||||
|
- `asr`: Audio sampling rate in Hertz
|
||||||
|
- `vbr`: Average video bitrate in KBit/s
|
||||||
|
- `fps`: Frame rate
|
||||||
|
- `vcodec`: Name of the video codec in use
|
||||||
|
- `container`: Name of the container format
|
||||||
|
- `filesize`: The number of bytes, if known in advance
|
||||||
|
- `filesize_approx`: An estimate for the number of bytes
|
||||||
|
- `protocol`: The protocol that will be used for the actual download
|
||||||
|
- `extractor`: Name of the extractor
|
||||||
|
- `extractor_key`: Key name of the extractor
|
||||||
|
- `epoch`: Unix epoch when creating the file
|
||||||
|
- `autonumber`: Five-digit number that will be increased with each download, starting at zero
|
||||||
|
- `playlist`: Name or id of the playlist that contains the video
|
||||||
|
- `playlist_index`: Index of the video in the playlist padded with leading zeros according to the total length of the playlist
|
||||||
|
|
||||||
|
Available for the video that belongs to some logical chapter or section:
|
||||||
|
- `chapter`: Name or title of the chapter the video belongs to
|
||||||
|
- `chapter_number`: Number of the chapter the video belongs to
|
||||||
|
- `chapter_id`: Id of the chapter the video belongs to
|
||||||
|
|
||||||
|
Available for the video that is an episode of some series or programme:
|
||||||
|
- `series`: Title of the series or programme the video episode belongs to
|
||||||
|
- `season`: Title of the season the video episode belongs to
|
||||||
|
- `season_number`: Number of the season the video episode belongs to
|
||||||
|
- `season_id`: Id of the season the video episode belongs to
|
||||||
|
- `episode`: Title of the video episode
|
||||||
|
- `episode_number`: Number of the video episode within a season
|
||||||
|
- `episode_id`: Id of the video episode
|
||||||
|
|
||||||
|
Each aforementioned sequence when referenced in 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 particular extractor, such sequences will be replaced with `NA`.
|
||||||
|
|
||||||
|
For example for `-o %(title)s-%(id)s.%(ext)s` and 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.
|
||||||
|
|
||||||
|
Output template can also contain arbitrary hierarchical path, e.g. `-o '%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s'` that will result in downloading each video in a directory corresponding to this path template. Any missing directory will be automatically created for you.
|
||||||
|
|
||||||
|
To specify percent literal in output template use `%%`. To output to stdout use `-o -`.
|
||||||
|
|
||||||
The current default template is `%(title)s-%(id)s.%(ext)s`.
|
The current default template is `%(title)s-%(id)s.%(ext)s`.
|
||||||
|
|
||||||
In some cases, you don't want special characters such as 中, spaces, or &, such as when transferring the downloaded filename to a Windows system or the filename through an 8bit-unsafe channel. In these cases, add the `--restrict-filenames` flag to get a shorter title:
|
In some cases, you don't want special characters such as 中, spaces, or &, such as when transferring the downloaded filename to a Windows system or the filename through an 8bit-unsafe channel. In these cases, add the `--restrict-filenames` flag to get a shorter title:
|
||||||
|
|
||||||
|
Examples (note on Windows you may need to use double quotes instead of single):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ youtube-dl --get-filename -o "%(title)s.%(ext)s" BaW_jenozKc
|
$ youtube-dl --get-filename -o '%(title)s.%(ext)s' BaW_jenozKc
|
||||||
youtube-dl test video ''_ä↭𝕐.mp4 # All kinds of weird characters
|
youtube-dl test video ''_ä↭𝕐.mp4 # All kinds of weird characters
|
||||||
$ youtube-dl --get-filename -o "%(title)s.%(ext)s" BaW_jenozKc --restrict-filenames
|
|
||||||
|
$ youtube-dl --get-filename -o '%(title)s.%(ext)s' BaW_jenozKc --restrict-filenames
|
||||||
youtube-dl_test_video_.mp4 # A simple file name
|
youtube-dl_test_video_.mp4 # A simple file name
|
||||||
|
|
||||||
|
# Download YouTube playlist videos in separate directory indexed by video order in a playlist
|
||||||
|
$ youtube-dl -o '%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s' https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re
|
||||||
|
|
||||||
|
# Download Udemy course keeping each chapter in separate directory under MyVideos directory in your home
|
||||||
|
$ youtube-dl -u user -p password -o '~/MyVideos/%(playlist)s/%(chapter_number)s - %(chapter)s/%(title)s.%(ext)s' https://www.udemy.com/java-tutorial/
|
||||||
|
|
||||||
|
# Download entire series season keeping each series and each season in separate directory under C:/MyVideos
|
||||||
|
$ youtube-dl -o "C:/MyVideos/%(series)s/%(season_number)s - %(season)s/%(episode_number)s - %(episode)s.%(ext)s" http://videomore.ru/kino_v_detalayah/5_sezon/367617
|
||||||
|
|
||||||
|
# Stream the video being downloaded to stdout
|
||||||
|
$ youtube-dl -o - BaW_jenozKc
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# FORMAT SELECTION
|
||||||
|
|
||||||
|
By default youtube-dl tries to download the best available quality, i.e. if you want the best quality you **don't need** to pass any special options, youtube-dl will guess it for you by **default**.
|
||||||
|
|
||||||
|
But sometimes you may want to download in a different format, for example when you are on a slow or intermittent connection. The key mechanism for achieving this is so called *format selection* based on which you can explicitly specify desired format, select formats based on some criterion or criteria, setup precedence and much more.
|
||||||
|
|
||||||
|
The general syntax for format selection is `--format FORMAT` or shorter `-f FORMAT` where `FORMAT` is a *selector expression*, i.e. an expression that describes format or formats you would like to download.
|
||||||
|
|
||||||
|
The simplest case is requesting a specific format, for example with `-f 22` you can download the format with format code equal to 22. You can get the list of available format codes for particular video using `--list-formats` or `-F`. Note that these format codes are extractor specific.
|
||||||
|
|
||||||
|
You can also use a file extension (currently `3gp`, `aac`, `flv`, `m4a`, `mp3`, `mp4`, `ogg`, `wav`, `webm` are supported) to download best quality format of particular file extension served as a single file, e.g. `-f webm` will download best quality format with `webm` extension served as a single file.
|
||||||
|
|
||||||
|
You can also use special names to select particular edge case format:
|
||||||
|
- `best`: Select best quality format represented by single file with video and audio
|
||||||
|
- `worst`: Select worst quality format represented by single file with video and audio
|
||||||
|
- `bestvideo`: Select best quality video only format (e.g. DASH video), may not be available
|
||||||
|
- `worstvideo`: Select worst quality video only format, may not be available
|
||||||
|
- `bestaudio`: Select best quality audio only format, may not be available
|
||||||
|
- `worstaudio`: Select worst quality audio only format, may not be available
|
||||||
|
|
||||||
|
For example, to download worst quality video only format you can use `-f worstvideo`.
|
||||||
|
|
||||||
|
If you want to download multiple videos and they don't have the same formats available, you can specify the order of preference using slashes. Note that slash is left-associative, i.e. formats on the left hand side are preferred, for example `-f 22/17/18` will download format 22 if it's available, otherwise it will download format 17 if it's available, otherwise it will download format 18 if it's available, otherwise it will complain that no suitable formats are available for download.
|
||||||
|
|
||||||
|
If you want to download several formats of the same video use comma as a separator, e.g. `-f 22,17,18` will download all these three formats, of course if they are available. Or more sophisticated example combined with precedence feature `-f 136/137/mp4/bestvideo,140/m4a/bestaudio`.
|
||||||
|
|
||||||
|
You can also filter the video formats by putting a condition in brackets, as in `-f "best[height=720]"` (or `-f "[filesize>10M]"`).
|
||||||
|
|
||||||
|
The following numeric meta fields can be used with comparisons `<`, `<=`, `>`, `>=`, `=` (equals), `!=` (not equals):
|
||||||
|
- `filesize`: The number of bytes, if known in advance
|
||||||
|
- `width`: Width of the video, if known
|
||||||
|
- `height`: Height of the video, if known
|
||||||
|
- `tbr`: Average bitrate of audio and video in KBit/s
|
||||||
|
- `abr`: Average audio bitrate in KBit/s
|
||||||
|
- `vbr`: Average video bitrate in KBit/s
|
||||||
|
- `asr`: Audio sampling rate in Hertz
|
||||||
|
- `fps`: Frame rate
|
||||||
|
|
||||||
|
Also filtering work for comparisons `=` (equals), `!=` (not equals), `^=` (begins with), `$=` (ends with), `*=` (contains) and following string meta fields:
|
||||||
|
- `ext`: File extension
|
||||||
|
- `acodec`: Name of the audio codec in use
|
||||||
|
- `vcodec`: Name of the video codec in use
|
||||||
|
- `container`: Name of the container format
|
||||||
|
- `protocol`: The protocol that will be used for the actual download, lower-case. `http`, `https`, `rtsp`, `rtmp`, `rtmpe`, `m3u8`, or `m3u8_native`
|
||||||
|
|
||||||
|
Note that none of the aforementioned meta fields are guaranteed to be present since this solely depends on the metadata obtained by particular extractor, i.e. the metadata offered by video hoster.
|
||||||
|
|
||||||
|
Formats for which the value is not known are excluded unless you put a question mark (`?`) after the operator. You can combine format filters, so `-f "[height <=? 720][tbr>500]"` selects up to 720p videos (or videos where the height is not known) with a bitrate of at least 500 KBit/s.
|
||||||
|
|
||||||
|
You can merge the video and audio of two formats into a single file using `-f <video-format>+<audio-format>` (requires ffmpeg or avconv installed), for example `-f bestvideo+bestaudio` will download best video only format, best audio only format and mux them together with ffmpeg/avconv.
|
||||||
|
|
||||||
|
Format selectors can also be grouped using parentheses, for example if you want to download the best mp4 and webm formats with a height lower than 480 you can use `-f '(mp4,webm)[height<480]'`.
|
||||||
|
|
||||||
|
Since the end of April 2015 and version 2015.04.26 youtube-dl uses `-f bestvideo+bestaudio/best` as default format selection (see #5447, #5456). If ffmpeg or avconv are installed this results in downloading `bestvideo` and `bestaudio` separately and muxing them together into a single file giving the best overall quality available. Otherwise it falls back to `best` and results in downloading the best available quality served as a single file. `best` is also needed for videos that don't come from YouTube because they don't provide the audio and video in two different files. If you want to only download some DASH formats (for example if you are not interested in getting videos with a resolution higher than 1080p), you can add `-f bestvideo[height<=?1080]+bestaudio/best` to your configuration file. Note that if you use youtube-dl to stream to `stdout` (and most likely to pipe it to your media player then), i.e. you explicitly specify output template as `-o -`, youtube-dl still uses `-f best` format selection in order to start content delivery immediately to your player and not to wait until `bestvideo` and `bestaudio` are downloaded and muxed.
|
||||||
|
|
||||||
|
If you want to preserve the old format selection behavior (prior to youtube-dl 2015.04.26), i.e. you want to download the best available quality media served as a single file, you should explicitly specify your choice with `-f best`. You may want to add it to the [configuration file](#configuration) in order not to type it every time you run youtube-dl.
|
||||||
|
|
||||||
|
Examples (note on Windows you may need to use double quotes instead of single):
|
||||||
|
```bash
|
||||||
|
# Download best mp4 format available or any other best if no mp4 available
|
||||||
|
$ youtube-dl -f 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best'
|
||||||
|
|
||||||
|
# Download best format available but not better that 480p
|
||||||
|
$ youtube-dl -f 'bestvideo[height<=480]+bestaudio/best[height<=480]'
|
||||||
|
|
||||||
|
# Download best video only format but no bigger that 50 MB
|
||||||
|
$ youtube-dl -f 'best[filesize<50M]'
|
||||||
|
|
||||||
|
# Download best format available via direct link over HTTP/HTTPS protocol
|
||||||
|
$ youtube-dl -f '(bestvideo+bestaudio/best)[protocol^=http]'
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
# VIDEO SELECTION
|
# VIDEO SELECTION
|
||||||
|
|
||||||
Videos can be filtered by their upload date using the options `--date`, `--datebefore` or `--dateafter`, they accept dates in two formats:
|
Videos can be filtered by their upload date using the options `--date`, `--datebefore` or `--dateafter`. They accept dates in two formats:
|
||||||
|
|
||||||
- Absolute dates: Dates in the format `YYYYMMDD`.
|
- Absolute dates: Dates in the format `YYYYMMDD`.
|
||||||
- Relative dates: Dates in the format `(now|today)[+-][0-9](day|week|month|year)(s)?`
|
- Relative dates: Dates in the format `(now|today)[+-][0-9](day|week|month|year)(s)?`
|
||||||
@@ -453,7 +624,7 @@ $ youtube-dl --dateafter now-6months
|
|||||||
# Download only the videos uploaded on January 1, 1970
|
# Download only the videos uploaded on January 1, 1970
|
||||||
$ youtube-dl --date 19700101
|
$ youtube-dl --date 19700101
|
||||||
|
|
||||||
$ # will only download the videos uploaded in the 200x decade
|
$ # Download only the videos uploaded in the 200x decade
|
||||||
$ youtube-dl --dateafter 20000101 --datebefore 20091231
|
$ youtube-dl --dateafter 20000101 --datebefore 20091231
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -465,7 +636,7 @@ If you've followed [our manual installation instructions](http://rg3.github.io/y
|
|||||||
|
|
||||||
If you have used pip, a simple `sudo pip install -U youtube-dl` is sufficient to update.
|
If you have used pip, a simple `sudo pip install -U youtube-dl` is sufficient to update.
|
||||||
|
|
||||||
If you have installed youtube-dl using a package manager like *apt-get* or *yum*, use the standard system update mechanism to update. Note that distribution packages are often outdated. As a rule of thumb, youtube-dl releases at least once a month, and often weekly or even daily. Simply go to http://yt-dl.org/ to find out the current version. Unfortunately, there is nothing we youtube-dl developers can do if your distributions serves a really outdated version. You can (and should) complain to your distribution in their bugtracker or support forum.
|
If you have installed youtube-dl using a package manager like *apt-get* or *yum*, use the standard system update mechanism to update. Note that distribution packages are often outdated. As a rule of thumb, youtube-dl releases at least once a month, and often weekly or even daily. Simply go to http://yt-dl.org/ to find out the current version. Unfortunately, there is nothing we youtube-dl developers can do if your distribution serves a really outdated version. You can (and should) complain to your distribution in their bugtracker or support forum.
|
||||||
|
|
||||||
As a last resort, you can also uninstall the version installed by your package manager and follow our manual installation instructions. For that, remove the distribution's package, with a line like
|
As a last resort, you can also uninstall the version installed by your package manager and follow our manual installation instructions. For that, remove the distribution's package, with a line like
|
||||||
|
|
||||||
@@ -487,11 +658,11 @@ YouTube changed their playlist format in March 2014 and later on, so you'll need
|
|||||||
|
|
||||||
If you have installed youtube-dl with a package manager, pip, setup.py or a tarball, please use that to update. Note that Ubuntu packages do not seem to get updated anymore. Since we are not affiliated with Ubuntu, there is little we can do. Feel free to [report bugs](https://bugs.launchpad.net/ubuntu/+source/youtube-dl/+filebug) to the [Ubuntu packaging guys](mailto:ubuntu-motu@lists.ubuntu.com?subject=outdated%20version%20of%20youtube-dl) - all they have to do is update the package to a somewhat recent version. See above for a way to update.
|
If you have installed youtube-dl with a package manager, pip, setup.py or a tarball, please use that to update. Note that Ubuntu packages do not seem to get updated anymore. Since we are not affiliated with Ubuntu, there is little we can do. Feel free to [report bugs](https://bugs.launchpad.net/ubuntu/+source/youtube-dl/+filebug) to the [Ubuntu packaging guys](mailto:ubuntu-motu@lists.ubuntu.com?subject=outdated%20version%20of%20youtube-dl) - all they have to do is update the package to a somewhat recent version. See above for a way to update.
|
||||||
|
|
||||||
### Do I always have to pass in `--max-quality FORMAT`, or `-citw`?
|
### Do I always have to pass `-citw`?
|
||||||
|
|
||||||
By default, youtube-dl intends to have the best options (incidentally, if you have a convincing case that these should be different, [please file an issue where you explain that](https://yt-dl.org/bug)). Therefore, it is unnecessary and sometimes harmful to copy long option strings from webpages. In particular, `--max-quality` *limits* the video quality (so if you want the best quality, do NOT pass it in), and the only option out of `-citw` that is regularly useful is `-i`.
|
By default, youtube-dl intends to have the best options (incidentally, if you have a convincing case that these should be different, [please file an issue where you explain that](https://yt-dl.org/bug)). Therefore, it is unnecessary and sometimes harmful to copy long option strings from webpages. In particular, the only option out of `-citw` that is regularly useful is `-i`.
|
||||||
|
|
||||||
### Can you please put the -b option back?
|
### Can you please put the `-b` option back?
|
||||||
|
|
||||||
Most people asking this question are not aware that youtube-dl now defaults to downloading the highest available quality as reported by YouTube, which will be 1080p or 720p in some cases, so you no longer need the `-b` option. For some specific videos, maybe YouTube does not report them to be available in a specific high quality format you're interested in. In that case, simply request it with the `-f` option and youtube-dl will try to download it.
|
Most people asking this question are not aware that youtube-dl now defaults to downloading the highest available quality as reported by YouTube, which will be 1080p or 720p in some cases, so you no longer need the `-b` option. For some specific videos, maybe YouTube does not report them to be available in a specific high quality format you're interested in. In that case, simply request it with the `-f` option and youtube-dl will try to download it.
|
||||||
|
|
||||||
@@ -499,17 +670,23 @@ Most people asking this question are not aware that youtube-dl now defaults to d
|
|||||||
|
|
||||||
Apparently YouTube requires you to pass a CAPTCHA test if you download too much. We're [considering to provide a way to let you solve the CAPTCHA](https://github.com/rg3/youtube-dl/issues/154), but at the moment, your best course of action is pointing a webbrowser to the youtube URL, solving the CAPTCHA, and restart youtube-dl.
|
Apparently YouTube requires you to pass a CAPTCHA test if you download too much. We're [considering to provide a way to let you solve the CAPTCHA](https://github.com/rg3/youtube-dl/issues/154), but at the moment, your best course of action is pointing a webbrowser to the youtube URL, solving the CAPTCHA, and restart youtube-dl.
|
||||||
|
|
||||||
|
### Do I need any other programs?
|
||||||
|
|
||||||
|
youtube-dl works fine on its own on most sites. However, if you want to convert video/audio, you'll need [avconv](https://libav.org/) or [ffmpeg](https://www.ffmpeg.org/). On some sites - most notably YouTube - videos can be retrieved in a higher quality format without sound. youtube-dl will detect whether avconv/ffmpeg is present and automatically pick the best option.
|
||||||
|
|
||||||
|
Videos or video formats streamed via RTMP protocol can only be downloaded when [rtmpdump](https://rtmpdump.mplayerhq.hu/) is installed. Downloading MMS and RTSP videos requires either [mplayer](http://mplayerhq.hu/) or [mpv](https://mpv.io/) to be installed.
|
||||||
|
|
||||||
### I have downloaded a video but how can I play it?
|
### I have downloaded a video but how can I play it?
|
||||||
|
|
||||||
Once the video is fully downloaded, use any video player, such as [vlc](http://www.videolan.org) or [mplayer](http://www.mplayerhq.hu/).
|
Once the video is fully downloaded, use any video player, such as [vlc](http://www.videolan.org) or [mplayer](http://www.mplayerhq.hu/).
|
||||||
|
|
||||||
### I extracted a video URL with -g, but it does not play on another machine / in my webbrowser.
|
### I extracted a video URL with `-g`, but it does not play on another machine / in my webbrowser.
|
||||||
|
|
||||||
It depends a lot on the service. In many cases, requests for the video (to download/play it) must come from the same IP address and with the same cookies. Use the `--cookies` option to write the required cookies into a file, and advise your downloader to read cookies from that file. Some sites also require a common user agent to be used, use `--dump-user-agent` to see the one in use by youtube-dl.
|
It depends a lot on the service. In many cases, requests for the video (to download/play it) must come from the same IP address and with the same cookies. Use the `--cookies` option to write the required cookies into a file, and advise your downloader to read cookies from that file. Some sites also require a common user agent to be used, use `--dump-user-agent` to see the one in use by youtube-dl.
|
||||||
|
|
||||||
It may be beneficial to use IPv6; in some cases, the restrictions are only applied to IPv4. Some services (sometimes only for a subset of videos) do not restrict the video URL by IP address, cookie, or user-agent, but these are the exception rather than the rule.
|
It may be beneficial to use IPv6; in some cases, the restrictions are only applied to IPv4. Some services (sometimes only for a subset of videos) do not restrict the video URL by IP address, cookie, or user-agent, but these are the exception rather than the rule.
|
||||||
|
|
||||||
Please bear in mind that some URL protocols are **not** supported by browsers out of the box, including RTMP. If you are using -g, your own downloader must support these as well.
|
Please bear in mind that some URL protocols are **not** supported by browsers out of the box, including RTMP. If you are using `-g`, your own downloader must support these as well.
|
||||||
|
|
||||||
If you want to play the video on a machine that is not running youtube-dl, you can relay the video content from the machine that runs youtube-dl. You can use `-o -` to let youtube-dl stream a video to stdout, or simply allow the player to download the files written by youtube-dl in turn.
|
If you want to play the video on a machine that is not running youtube-dl, you can relay the video content from the machine that runs youtube-dl. You can use `-o -` to let youtube-dl stream a video to stdout, or simply allow the player to download the files written by youtube-dl in turn.
|
||||||
|
|
||||||
@@ -517,15 +694,35 @@ If you want to play the video on a machine that is not running youtube-dl, you c
|
|||||||
|
|
||||||
YouTube has switched to a new video info format in July 2011 which is not supported by old versions of youtube-dl. See [above](#how-do-i-update-youtube-dl) for how to update youtube-dl.
|
YouTube has switched to a new video info format in July 2011 which is not supported by old versions of youtube-dl. See [above](#how-do-i-update-youtube-dl) for how to update youtube-dl.
|
||||||
|
|
||||||
### ERROR: unable to download video ###
|
### ERROR: unable to download video
|
||||||
|
|
||||||
YouTube requires an additional signature since September 2012 which is not supported by old versions of youtube-dl. See [above](#how-do-i-update-youtube-dl) for how to update youtube-dl.
|
YouTube requires an additional signature since September 2012 which is not supported by old versions of youtube-dl. See [above](#how-do-i-update-youtube-dl) for how to update youtube-dl.
|
||||||
|
|
||||||
|
### Video URL contains an ampersand and I'm getting some strange output `[1] 2839` or `'v' is not recognized as an internal or external command`
|
||||||
|
|
||||||
|
That's actually the output from your shell. Since ampersand is one of the special shell characters it's interpreted by the shell preventing you from passing the whole URL to youtube-dl. To disable your shell from interpreting the ampersands (or any other special characters) you have to either put the whole URL in quotes or escape them with a backslash (which approach will work depends on your shell).
|
||||||
|
|
||||||
|
For example if your URL is https://www.youtube.com/watch?t=4&v=BaW_jenozKc you should end up with following command:
|
||||||
|
|
||||||
|
```youtube-dl 'https://www.youtube.com/watch?t=4&v=BaW_jenozKc'```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```youtube-dl https://www.youtube.com/watch?t=4\&v=BaW_jenozKc```
|
||||||
|
|
||||||
|
For Windows you have to use the double quotes:
|
||||||
|
|
||||||
|
```youtube-dl "https://www.youtube.com/watch?t=4&v=BaW_jenozKc"```
|
||||||
|
|
||||||
### ExtractorError: Could not find JS function u'OF'
|
### ExtractorError: Could not find JS function u'OF'
|
||||||
|
|
||||||
In February 2015, the new YouTube player contained a character sequence in a string that was misinterpreted by old versions of youtube-dl. See [above](#how-do-i-update-youtube-dl) for how to update youtube-dl.
|
In February 2015, the new YouTube player contained a character sequence in a string that was misinterpreted by old versions of youtube-dl. See [above](#how-do-i-update-youtube-dl) for how to update youtube-dl.
|
||||||
|
|
||||||
### SyntaxError: Non-ASCII character ###
|
### 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.
|
||||||
|
|
||||||
|
### SyntaxError: Non-ASCII character
|
||||||
|
|
||||||
The error
|
The error
|
||||||
|
|
||||||
@@ -554,13 +751,19 @@ From then on, after restarting your shell, you will be able to access both youtu
|
|||||||
|
|
||||||
Use the `-o` to specify an [output template](#output-template), for example `-o "/home/user/videos/%(title)s-%(id)s.%(ext)s"`. If you want this for all of your downloads, put the option into your [configuration file](#configuration).
|
Use the `-o` to specify an [output template](#output-template), for example `-o "/home/user/videos/%(title)s-%(id)s.%(ext)s"`. If you want this for all of your downloads, put the option into your [configuration file](#configuration).
|
||||||
|
|
||||||
### How do I download a video starting with a `-` ?
|
### How do I download a video starting with a `-`?
|
||||||
|
|
||||||
Either prepend `http://www.youtube.com/watch?v=` or separate the ID from the options with `--`:
|
Either prepend `http://www.youtube.com/watch?v=` or separate the ID from the options with `--`:
|
||||||
|
|
||||||
youtube-dl -- -wNyEUrxzFU
|
youtube-dl -- -wNyEUrxzFU
|
||||||
youtube-dl "http://www.youtube.com/watch?v=-wNyEUrxzFU"
|
youtube-dl "http://www.youtube.com/watch?v=-wNyEUrxzFU"
|
||||||
|
|
||||||
|
### How do I pass cookies to youtube-dl?
|
||||||
|
|
||||||
|
Use the `--cookies` option, for example `--cookies /path/to/cookies/file.txt`. 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, `LF` (`\n`) for Linux and `CR` (`\r`) for Mac OS. `HTTP Error 400: Bad Request` when using `--cookies` is a good sign of invalid newline format.
|
||||||
|
|
||||||
|
Passing cookies to youtube-dl is a good way to workaround login when a particular extractor does not implement it explicitly. Another use case is working around [CAPTCHA](https://en.wikipedia.org/wiki/CAPTCHA) some websites require you to solve in particular cases in order to get access (e.g. YouTube, CloudFlare).
|
||||||
|
|
||||||
### Can you add support for this anime video site, or site which shows current movies for free?
|
### Can you add support for this anime video site, or site which shows current movies for free?
|
||||||
|
|
||||||
As a matter of policy (as well as legality), youtube-dl does not include support for services that specialize in infringing copyright. As a rule of thumb, if you cannot easily find a video that the service is quite obviously allowed to distribute (i.e. that has been uploaded by the creator, the creator's distributor, or is published under a free license), the service is probably unfit for inclusion to youtube-dl.
|
As a matter of policy (as well as legality), youtube-dl does not include support for services that specialize in infringing copyright. As a rule of thumb, if you cannot easily find a video that the service is quite obviously allowed to distribute (i.e. that has been uploaded by the creator, the creator's distributor, or is published under a free license), the service is probably unfit for inclusion to youtube-dl.
|
||||||
@@ -569,9 +772,21 @@ A note on the service that they don't host the infringing content, but just link
|
|||||||
|
|
||||||
Support requests for services that **do** purchase the rights to distribute their content are perfectly fine though. If in doubt, you can simply include a source that mentions the legitimate purchase of content.
|
Support requests for services that **do** purchase the rights to distribute their content are perfectly fine though. If in doubt, you can simply include a source that mentions the legitimate purchase of content.
|
||||||
|
|
||||||
|
### How can I speed up work on my issue?
|
||||||
|
|
||||||
|
(Also known as: Help, my important issue not being solved!) The youtube-dl core developer team is quite small. While we do our best to solve as many issues as possible, sometimes that can take quite a while. To speed up your issue, here's what you can do:
|
||||||
|
|
||||||
|
First of all, please do report the issue [at our issue tracker](https://yt-dl.org/bugs). That allows us to coordinate all efforts by users and developers, and serves as a unified point. Unfortunately, the youtube-dl project has grown too large to use personal email as an effective communication channel.
|
||||||
|
|
||||||
|
Please read the [bug reporting instructions](#bugs) below. A lot of bugs lack all the necessary information. If you can, offer proxy, VPN, or shell access to the youtube-dl developers. If you are able to, test the issue from multiple computers in multiple countries to exclude local censorship or misconfiguration issues.
|
||||||
|
|
||||||
|
If nobody is interested in solving your issue, you are welcome to take matters into your own hands and submit a pull request (or coerce/pay somebody else to do so).
|
||||||
|
|
||||||
|
Feel free to bump the issue from time to time by writing a small comment ("Issue is still present in youtube-dl version ...from France, but fixed from Belgium"), but please not more than once a month. Please do not declare your issue as `important` or `urgent`.
|
||||||
|
|
||||||
### How can I detect whether a given URL is supported by youtube-dl?
|
### How can I detect whether a given URL is supported by youtube-dl?
|
||||||
|
|
||||||
For one, have a look at the [list of supported sites](docs/supportedsites.md). Note that it can sometimes happen that the site changes its URL scheme (say, from http://example.com/v/1234567 to http://example.com/v/1234567 ) and youtube-dl reports an URL of a service in that list as unsupported. In that case, simply report a bug.
|
For one, have a look at the [list of supported sites](docs/supportedsites.md). Note that it can sometimes happen that the site changes its URL scheme (say, from http://example.com/video/1234567 to http://example.com/v/1234567 ) and youtube-dl reports an URL of a service in that list as unsupported. In that case, simply report a bug.
|
||||||
|
|
||||||
It is *not* possible to detect whether a URL is supported or not. That's because youtube-dl contains a generic extractor which matches **all** URLs. You may be tempted to disable, exclude, or remove the generic extractor, but the generic extractor not only allows users to extract videos from lots of websites that embed a video from another service, but may also be used to extract video from a service that it's hosting itself. Therefore, we neither recommend nor support disabling, excluding, or removing the generic extractor.
|
It is *not* possible to detect whether a URL is supported or not. That's because youtube-dl contains a generic extractor which matches **all** URLs. You may be tempted to disable, exclude, or remove the generic extractor, but the generic extractor not only allows users to extract videos from lots of websites that embed a video from another service, but may also be used to extract video from a service that it's hosting itself. Therefore, we neither recommend nor support disabling, excluding, or removing the generic extractor.
|
||||||
|
|
||||||
@@ -637,18 +852,19 @@ If you want to add support for a new site, you can follow this quick list (assum
|
|||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
# TODO more code goes here, for example ...
|
# TODO more code goes here, for example ...
|
||||||
title = self._html_search_regex(r'<h1>(.*?)</h1>', webpage, 'title')
|
title = self._html_search_regex(r'<h1>(.+?)</h1>', webpage, 'title')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': self._og_search_description(webpage),
|
'description': self._og_search_description(webpage),
|
||||||
|
'uploader': self._search_regex(r'<div[^>]+id="uploader"[^>]*>([^<]+)<', webpage, 'uploader', fatal=False),
|
||||||
# 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/__init__.py`](https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/__init__.py).
|
5. Add an import in [`youtube_dl/extractor/__init__.py`](https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/__init__.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 be then be named `TestDownload.test_YourExtractor`, `TestDownload.test_YourExtractor_1`, `TestDownload.test_YourExtractor_2`, etc.
|
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.
|
||||||
7. Have a look at [`youtube_dl/common/extractor/common.py`](https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/common.py) for possible helper methods and a [detailed description of what your extractor should return](https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/common.py#L38). Add tests and code for as many as you want.
|
7. Have a look at [`youtube_dl/extractor/common.py`](https://github.com/rg3/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/rg3/youtube-dl/blob/master/youtube_dl/extractor/common.py#L62-L200). Add tests and code for as many as you want.
|
||||||
8. If you can, check the code with [flake8](https://pypi.python.org/pypi/flake8).
|
8. If you can, check the code with [flake8](https://pypi.python.org/pypi/flake8).
|
||||||
9. When the tests pass, [add](http://git-scm.com/docs/git-add) the new files and [commit](http://git-scm.com/docs/git-commit) them and [push](http://git-scm.com/docs/git-push) the result, like this:
|
9. When the tests pass, [add](http://git-scm.com/docs/git-add) the new files and [commit](http://git-scm.com/docs/git-commit) them and [push](http://git-scm.com/docs/git-push) the result, like this:
|
||||||
|
|
||||||
@@ -668,6 +884,7 @@ youtube-dl makes the best effort to be a good command-line program, and thus sho
|
|||||||
From a Python program, you can embed youtube-dl in a more powerful fashion, like this:
|
From a Python program, you can embed youtube-dl in a more powerful fashion, like this:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
from __future__ import unicode_literals
|
||||||
import youtube_dl
|
import youtube_dl
|
||||||
|
|
||||||
ydl_opts = {}
|
ydl_opts = {}
|
||||||
@@ -675,11 +892,12 @@ with youtube_dl.YoutubeDL(ydl_opts) as ydl:
|
|||||||
ydl.download(['http://www.youtube.com/watch?v=BaW_jenozKc'])
|
ydl.download(['http://www.youtube.com/watch?v=BaW_jenozKc'])
|
||||||
```
|
```
|
||||||
|
|
||||||
Most likely, you'll want to use various options. For a list of what can be done, have a look at [youtube_dl/YoutubeDL.py](https://github.com/rg3/youtube-dl/blob/master/youtube_dl/YoutubeDL.py#L69). For a start, if you want to intercept youtube-dl's output, set a `logger` object.
|
Most likely, you'll want to use various options. For a list of what can be done, have a look at [`youtube_dl/YoutubeDL.py`](https://github.com/rg3/youtube-dl/blob/master/youtube_dl/YoutubeDL.py#L121-L269). For a start, if you want to intercept youtube-dl's output, set a `logger` object.
|
||||||
|
|
||||||
Here's a more complete example of a program that outputs only errors (and a short message after the download is finished), and downloads/converts the video to an mp3 file:
|
Here's a more complete example of a program that outputs only errors (and a short message after the download is finished), and downloads/converts the video to an mp3 file:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
from __future__ import unicode_literals
|
||||||
import youtube_dl
|
import youtube_dl
|
||||||
|
|
||||||
|
|
||||||
@@ -715,11 +933,25 @@ with youtube_dl.YoutubeDL(ydl_opts) as ydl:
|
|||||||
|
|
||||||
# BUGS
|
# BUGS
|
||||||
|
|
||||||
Bugs and suggestions should be reported at: <https://github.com/rg3/youtube-dl/issues> . Unless you were prompted so or there is another pertinent reason (e.g. GitHub fails to accept the bug report), please do not send bug reports via personal email. For discussions, join us in the irc channel #youtube-dl on freenode.
|
Bugs and suggestions should be reported at: <https://github.com/rg3/youtube-dl/issues>. Unless you were prompted so or there is another pertinent reason (e.g. GitHub fails to accept the bug report), please do not send bug reports via personal email. For discussions, join us in the IRC channel [#youtube-dl](irc://chat.freenode.net/#youtube-dl) on freenode ([webchat](http://webchat.freenode.net/?randomnick=1&channels=youtube-dl)).
|
||||||
|
|
||||||
**Please include the full output of youtube-dl when run with `-v`**.
|
**Please include the full output of youtube-dl when run with `-v`**, i.e. add `-v` flag to your command line, copy the **whole** output and post it in the issue body wrapped in \`\`\` for better formatting. It should look similar to this:
|
||||||
|
```
|
||||||
|
$ youtube-dl -v http://www.youtube.com/watch?v=BaW_jenozKcj
|
||||||
|
[debug] System config: []
|
||||||
|
[debug] User config: []
|
||||||
|
[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] youtube-dl version 2015.12.06
|
||||||
|
[debug] Git HEAD: 135392e
|
||||||
|
[debug] Python version 2.6.6 - Windows-2003Server-5.2.3790-SP2
|
||||||
|
[debug] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4
|
||||||
|
[debug] Proxy map: {}
|
||||||
|
...
|
||||||
|
```
|
||||||
|
**Do not post screenshots of verbose log only plain text is acceptable.**
|
||||||
|
|
||||||
The output (including the first lines) contain important debugging information. Issues without the full output are often not reproducible and therefore do not get solved in short order, if ever.
|
The output (including the first lines) contains important debugging information. Issues without the full output are often not reproducible and therefore do not get solved in short order, if ever.
|
||||||
|
|
||||||
Please re-read your issue once again to avoid a couple of common mistakes (you can and should use this as a checklist):
|
Please re-read your issue once again to avoid a couple of common mistakes (you can and should use this as a checklist):
|
||||||
|
|
||||||
@@ -733,19 +965,21 @@ So please elaborate on what feature you are requesting, or what bug you want to
|
|||||||
- How it could be fixed
|
- How it could be fixed
|
||||||
- How your proposed solution would look like
|
- How your proposed solution would look like
|
||||||
|
|
||||||
If your report is shorter than two lines, it is almost certainly missing some of these, which makes it hard for us to respond to it. We're often too polite to close the issue outright, but the missing info makes misinterpretation likely. As a commiter myself, I often get frustrated by these issues, since the only possible way for me to move forward on them is to ask for clarification over and over.
|
If your report is shorter than two lines, it is almost certainly missing some of these, which makes it hard for us to respond to it. We're often too polite to close the issue outright, but the missing info makes misinterpretation likely. As a committer myself, I often get frustrated by these issues, since the only possible way for me to move forward on them is to ask for clarification over and over.
|
||||||
|
|
||||||
For bug reports, this means that your report should contain the *complete* output of youtube-dl when called with the -v flag. The error message you get for (most) bugs even says so, but you would not believe how many of our bug reports do not contain this information.
|
For bug reports, this means that your report should contain the *complete* output of youtube-dl when called with the `-v` flag. The error message you get for (most) bugs even says so, but you would not believe how many of our bug reports do not contain this information.
|
||||||
|
|
||||||
Site support requests **must contain an example URL**. An example URL is a URL you might want to download, like http://www.youtube.com/watch?v=BaW_jenozKc . There should be an obvious video present. Except under very special circumstances, the main page of a video service (e.g. http://www.youtube.com/ ) is *not* an example URL.
|
If your server has multiple IPs or you suspect censorship, adding `--call-home` may be a good idea to get more diagnostics. If the error is `ERROR: Unable to extract ...` and you cannot reproduce it from multiple countries, add `--dump-pages` (warning: this will yield a rather large output, redirect it to the file `log.txt` by adding `>log.txt 2>&1` to your command-line) or upload the `.dump` files you get when you add `--write-pages` [somewhere](https://gist.github.com/).
|
||||||
|
|
||||||
|
**Site support requests must contain an example URL**. An example URL is a URL you might want to download, like `http://www.youtube.com/watch?v=BaW_jenozKc`. There should be an obvious video present. Except under very special circumstances, the main page of a video service (e.g. `http://www.youtube.com/`) is *not* an example URL.
|
||||||
|
|
||||||
### Are you using the latest version?
|
### Are you using the latest version?
|
||||||
|
|
||||||
Before reporting any issue, type youtube-dl -U. This should report that you're up-to-date. About 20% of the reports we receive are already fixed, but people are using outdated versions. This goes for feature requests as well.
|
Before reporting any issue, type `youtube-dl -U`. This should report that you're up-to-date. About 20% of the reports we receive are already fixed, but people are using outdated versions. This goes for feature requests as well.
|
||||||
|
|
||||||
### Is the issue already documented?
|
### Is the issue already documented?
|
||||||
|
|
||||||
Make sure that someone has not already opened the issue you're trying to open. Search at the top of the window or at https://github.com/rg3/youtube-dl/search?type=Issues . If there is an issue, feel free to write something along the lines of "This affects me as well, with version 2015.01.01. Here is some more information on the issue: ...". While some issues may be old, a new post into them often spurs rapid activity.
|
Make sure that someone has not already opened the issue you're trying to open. Search at the top of the window or browse the [GitHub Issues](https://github.com/rg3/youtube-dl/search?type=Issues) of this repository. If there is an issue, feel free to write something along the lines of "This affects me as well, with version 2015.01.01. Here is some more information on the issue: ...". While some issues may be old, a new post into them often spurs rapid activity.
|
||||||
|
|
||||||
### Why are existing options not enough?
|
### Why are existing options not enough?
|
||||||
|
|
||||||
@@ -775,4 +1009,4 @@ It may sound strange, but some bug reports we receive are completely unrelated t
|
|||||||
|
|
||||||
youtube-dl is released into the public domain by the copyright holders.
|
youtube-dl is released into the public domain by the copyright holders.
|
||||||
|
|
||||||
This README file was originally written by Daniel Bolton (<https://github.com/dbbolton>) and is likewise released into the public domain.
|
This README file was originally written by [Daniel Bolton](https://github.com/dbbolton) and is likewise released into the public domain.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import os
|
|||||||
from os.path import dirname as dirn
|
from os.path import dirname as dirn
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
sys.path.append(dirn(dirn((os.path.abspath(__file__)))))
|
sys.path.insert(0, dirn(dirn((os.path.abspath(__file__)))))
|
||||||
import youtube_dl
|
import youtube_dl
|
||||||
|
|
||||||
BASH_COMPLETION_FILE = "youtube-dl.bash-completion"
|
BASH_COMPLETION_FILE = "youtube-dl.bash-completion"
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ for test in get_testcases():
|
|||||||
if METHOD == 'EURISTIC':
|
if METHOD == 'EURISTIC':
|
||||||
try:
|
try:
|
||||||
webpage = compat_urllib_request.urlopen(test['url'], timeout=10).read()
|
webpage = compat_urllib_request.urlopen(test['url'], timeout=10).read()
|
||||||
except:
|
except Exception:
|
||||||
print('\nFail: {0}'.format(test['name']))
|
print('\nFail: {0}'.format(test['name']))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -45,12 +45,12 @@ for test in get_testcases():
|
|||||||
|
|
||||||
RESULT = ('.' + domain + '\n' in LIST or '\n' + domain + '\n' in LIST)
|
RESULT = ('.' + domain + '\n' in LIST or '\n' + domain + '\n' in LIST)
|
||||||
|
|
||||||
if RESULT and ('info_dict' not in test or 'age_limit' not in test['info_dict']
|
if RESULT and ('info_dict' not in test or 'age_limit' not in test['info_dict'] or
|
||||||
or test['info_dict']['age_limit'] != 18):
|
test['info_dict']['age_limit'] != 18):
|
||||||
print('\nPotential missing age_limit check: {0}'.format(test['name']))
|
print('\nPotential missing age_limit check: {0}'.format(test['name']))
|
||||||
|
|
||||||
elif not RESULT and ('info_dict' in test and 'age_limit' in test['info_dict']
|
elif not RESULT and ('info_dict' in test and 'age_limit' in test['info_dict'] and
|
||||||
and test['info_dict']['age_limit'] == 18):
|
test['info_dict']['age_limit'] == 18):
|
||||||
print('\nPotential false negative: {0}'.format(test['name']))
|
print('\nPotential false negative: {0}'.format(test['name']))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import os
|
|||||||
from os.path import dirname as dirn
|
from os.path import dirname as dirn
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
sys.path.append(dirn(dirn((os.path.abspath(__file__)))))
|
sys.path.insert(0, dirn(dirn((os.path.abspath(__file__)))))
|
||||||
import youtube_dl
|
import youtube_dl
|
||||||
from youtube_dl.utils import shell_quote
|
from youtube_dl.utils import shell_quote
|
||||||
|
|
||||||
|
|||||||
42
devscripts/generate_aes_testdata.py
Normal file
42
devscripts/generate_aes_testdata.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import codecs
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from youtube_dl.utils import intlist_to_bytes
|
||||||
|
from youtube_dl.aes import aes_encrypt, key_expansion
|
||||||
|
|
||||||
|
secret_msg = b'Secret message goes here'
|
||||||
|
|
||||||
|
|
||||||
|
def hex_str(int_list):
|
||||||
|
return codecs.encode(intlist_to_bytes(int_list), 'hex')
|
||||||
|
|
||||||
|
|
||||||
|
def openssl_encode(algo, key, iv):
|
||||||
|
cmd = ['openssl', 'enc', '-e', '-' + algo, '-K', hex_str(key), '-iv', hex_str(iv)]
|
||||||
|
prog = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||||
|
out, _ = prog.communicate(secret_msg)
|
||||||
|
return out
|
||||||
|
|
||||||
|
iv = key = [0x20, 0x15] + 14 * [0]
|
||||||
|
|
||||||
|
r = openssl_encode('aes-128-cbc', key, iv)
|
||||||
|
print('aes_cbc_decrypt')
|
||||||
|
print(repr(r))
|
||||||
|
|
||||||
|
password = key
|
||||||
|
new_key = aes_encrypt(password, key_expansion(password))
|
||||||
|
r = openssl_encode('aes-128-ctr', new_key, iv)
|
||||||
|
print('aes_decrypt_text 16')
|
||||||
|
print(repr(r))
|
||||||
|
|
||||||
|
password = key + 16 * [0]
|
||||||
|
new_key = aes_encrypt(password, key_expansion(password)) * (32 // 16)
|
||||||
|
r = openssl_encode('aes-256-ctr', new_key, iv)
|
||||||
|
print('aes_decrypt_text 32')
|
||||||
|
print(repr(r))
|
||||||
@@ -5,7 +5,7 @@ from __future__ import with_statement, unicode_literals
|
|||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import glob
|
import glob
|
||||||
import io # For Python 2 compatibilty
|
import io # For Python 2 compatibility
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import os
|
|||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
# We must be able to import youtube_dl
|
# We must be able to import youtube_dl
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||||
|
|
||||||
import youtube_dl
|
import youtube_dl
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import sys
|
|||||||
|
|
||||||
# Import youtube_dl
|
# Import youtube_dl
|
||||||
ROOT_DIR = os.path.join(os.path.dirname(__file__), '..')
|
ROOT_DIR = os.path.join(os.path.dirname(__file__), '..')
|
||||||
sys.path.append(ROOT_DIR)
|
sys.path.insert(0, ROOT_DIR)
|
||||||
import youtube_dl
|
import youtube_dl
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,35 @@ import re
|
|||||||
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
README_FILE = os.path.join(ROOT_DIR, 'README.md')
|
README_FILE = os.path.join(ROOT_DIR, 'README.md')
|
||||||
|
|
||||||
|
|
||||||
|
def filter_options(readme):
|
||||||
|
ret = ''
|
||||||
|
in_options = False
|
||||||
|
for line in readme.split('\n'):
|
||||||
|
if line.startswith('# '):
|
||||||
|
if line[2:].startswith('OPTIONS'):
|
||||||
|
in_options = True
|
||||||
|
else:
|
||||||
|
in_options = False
|
||||||
|
|
||||||
|
if in_options:
|
||||||
|
if line.lstrip().startswith('-'):
|
||||||
|
option, description = re.split(r'\s{2,}', line.lstrip())
|
||||||
|
split_option = option.split(' ')
|
||||||
|
|
||||||
|
if not split_option[-1].startswith('-'): # metavar
|
||||||
|
option = ' '.join(split_option[:-1] + ['*%s*' % split_option[-1]])
|
||||||
|
|
||||||
|
# Pandoc's definition_lists. See http://pandoc.org/README.html
|
||||||
|
# for more information.
|
||||||
|
ret += '\n%s\n: %s\n' % (option, description)
|
||||||
|
else:
|
||||||
|
ret += line.lstrip() + '\n'
|
||||||
|
else:
|
||||||
|
ret += line + '\n'
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
with io.open(README_FILE, encoding='utf-8') as f:
|
with io.open(README_FILE, encoding='utf-8') as f:
|
||||||
readme = f.read()
|
readme = f.read()
|
||||||
|
|
||||||
@@ -26,6 +55,8 @@ readme = re.sub(r'(?s)^.*?(?=# DESCRIPTION)', '', readme)
|
|||||||
readme = re.sub(r'\s+youtube-dl \[OPTIONS\] URL \[URL\.\.\.\]', '', readme)
|
readme = re.sub(r'\s+youtube-dl \[OPTIONS\] URL \[URL\.\.\.\]', '', readme)
|
||||||
readme = PREFIX + readme
|
readme = PREFIX + readme
|
||||||
|
|
||||||
|
readme = filter_options(readme)
|
||||||
|
|
||||||
if sys.version_info < (3, 0):
|
if sys.version_info < (3, 0):
|
||||||
print(readme.encode('utf-8'))
|
print(readme.encode('utf-8'))
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import os
|
|||||||
from os.path import dirname as dirn
|
from os.path import dirname as dirn
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
sys.path.append(dirn(dirn((os.path.abspath(__file__)))))
|
sys.path.insert(0, dirn(dirn((os.path.abspath(__file__)))))
|
||||||
import youtube_dl
|
import youtube_dl
|
||||||
|
|
||||||
ZSH_COMPLETION_FILE = "youtube-dl.zsh"
|
ZSH_COMPLETION_FILE = "youtube-dl.zsh"
|
||||||
|
|||||||
@@ -1,36 +1,49 @@
|
|||||||
# Supported sites
|
# Supported sites
|
||||||
- **1tv**: Первый канал
|
- **1tv**: Первый канал
|
||||||
- **1up.com**
|
- **1up.com**
|
||||||
|
- **20min**
|
||||||
- **220.ro**
|
- **220.ro**
|
||||||
|
- **22tracks:genre**
|
||||||
|
- **22tracks:track**
|
||||||
- **24video**
|
- **24video**
|
||||||
- **3sat**
|
- **3sat**
|
||||||
- **4tube**
|
- **4tube**
|
||||||
- **56.com**
|
- **56.com**
|
||||||
- **5min**
|
- **5min**
|
||||||
- **8tracks**
|
- **8tracks**
|
||||||
|
- **91porn**
|
||||||
- **9gag**
|
- **9gag**
|
||||||
- **abc.net.au**
|
- **abc.net.au**
|
||||||
- **Abc7News**
|
- **Abc7News**
|
||||||
- **AcademicEarth:Course**
|
- **AcademicEarth:Course**
|
||||||
|
- **acast**
|
||||||
|
- **acast:channel**
|
||||||
- **AddAnime**
|
- **AddAnime**
|
||||||
- **AdobeTV**
|
- **AdobeTV**
|
||||||
|
- **AdobeTVChannel**
|
||||||
|
- **AdobeTVShow**
|
||||||
|
- **AdobeTVVideo**
|
||||||
- **AdultSwim**
|
- **AdultSwim**
|
||||||
- **Aftenposten**
|
- **aenetworks**: A+E Networks: A&E, Lifetime, History.com, FYI Network
|
||||||
- **Aftonbladet**
|
- **Aftonbladet**
|
||||||
|
- **AirMozilla**
|
||||||
- **AlJazeera**
|
- **AlJazeera**
|
||||||
- **Allocine**
|
- **Allocine**
|
||||||
- **AlphaPorno**
|
- **AlphaPorno**
|
||||||
- **anitube.se**
|
- **anitube.se**
|
||||||
- **AnySex**
|
- **AnySex**
|
||||||
- **Aparat**
|
- **Aparat**
|
||||||
- **AppleDailyAnimationNews**
|
- **AppleConnect**
|
||||||
- **AppleDailyRealtimeNews**
|
- **AppleDaily**: 臺灣蘋果日報
|
||||||
- **AppleTrailers**
|
- **appletrailers**
|
||||||
|
- **appletrailers:section**
|
||||||
- **archive.org**: archive.org videos
|
- **archive.org**: archive.org videos
|
||||||
- **ARD**
|
- **ARD**
|
||||||
|
- **ARD:mediathek**: Saarländischer Rundfunk
|
||||||
- **ARD:mediathek**
|
- **ARD:mediathek**
|
||||||
- **arte.tv**
|
- **arte.tv**
|
||||||
- **arte.tv:+7**
|
- **arte.tv:+7**
|
||||||
|
- **arte.tv:cinema**
|
||||||
- **arte.tv:concert**
|
- **arte.tv:concert**
|
||||||
- **arte.tv:creative**
|
- **arte.tv:creative**
|
||||||
- **arte.tv:ddc**
|
- **arte.tv:ddc**
|
||||||
@@ -38,46 +51,63 @@
|
|||||||
- **arte.tv:future**
|
- **arte.tv:future**
|
||||||
- **AtresPlayer**
|
- **AtresPlayer**
|
||||||
- **ATTTechChannel**
|
- **ATTTechChannel**
|
||||||
|
- **AudiMedia**
|
||||||
- **audiomack**
|
- **audiomack**
|
||||||
- **audiomack:album**
|
- **audiomack:album**
|
||||||
- **Azubu**
|
- **Azubu**
|
||||||
|
- **AzubuLive**
|
||||||
|
- **BaiduVideo**: 百度视频
|
||||||
- **bambuser**
|
- **bambuser**
|
||||||
- **bambuser:channel**
|
- **bambuser:channel**
|
||||||
- **Bandcamp**
|
- **Bandcamp**
|
||||||
- **Bandcamp:album**
|
- **Bandcamp:album**
|
||||||
|
- **bbc**: BBC
|
||||||
- **bbc.co.uk**: BBC iPlayer
|
- **bbc.co.uk**: BBC iPlayer
|
||||||
|
- **bbc.co.uk:article**: BBC articles
|
||||||
|
- **BeatportPro**
|
||||||
- **Beeg**
|
- **Beeg**
|
||||||
- **BehindKink**
|
- **BehindKink**
|
||||||
- **Bet**
|
- **Bet**
|
||||||
|
- **Bigflix**
|
||||||
- **Bild**: Bild.de
|
- **Bild**: Bild.de
|
||||||
- **BiliBili**
|
- **BiliBili**
|
||||||
|
- **BleacherReport**
|
||||||
|
- **BleacherReportCMS**
|
||||||
- **blinkx**
|
- **blinkx**
|
||||||
- **blip.tv:user**
|
|
||||||
- **BlipTV**
|
|
||||||
- **Bloomberg**
|
- **Bloomberg**
|
||||||
- **Bpb**: Bundeszentrale für politische Bildung
|
- **Bpb**: Bundeszentrale für politische Bildung
|
||||||
- **BR**: Bayerischer Rundfunk Mediathek
|
- **BR**: Bayerischer Rundfunk Mediathek
|
||||||
- **Break**
|
- **Break**
|
||||||
- **Brightcove**
|
- **brightcove:legacy**
|
||||||
|
- **brightcove:new**
|
||||||
|
- **bt:article**: Bergens Tidende Articles
|
||||||
|
- **bt:vestlendingen**: Bergens Tidende - Vestlendingen
|
||||||
- **BuzzFeed**
|
- **BuzzFeed**
|
||||||
- **BYUtv**
|
- **BYUtv**
|
||||||
- **Camdemy**
|
- **Camdemy**
|
||||||
- **CamdemyFolder**
|
- **CamdemyFolder**
|
||||||
- **Canal13cl**
|
|
||||||
- **canalc2.tv**
|
- **canalc2.tv**
|
||||||
- **Canalplus**: canalplus.fr, piwiplus.fr and d8.tv
|
- **Canalplus**: canalplus.fr, piwiplus.fr and d8.tv
|
||||||
|
- **Canvas**
|
||||||
- **CBS**
|
- **CBS**
|
||||||
- **CBSNews**: CBS News
|
- **CBSNews**: CBS News
|
||||||
|
- **CBSNewsLiveVideo**: CBS News Live Videos
|
||||||
|
- **CBSSports**
|
||||||
- **CeskaTelevize**
|
- **CeskaTelevize**
|
||||||
- **channel9**: Channel 9
|
- **channel9**: Channel 9
|
||||||
|
- **Chaturbate**
|
||||||
- **Chilloutzone**
|
- **Chilloutzone**
|
||||||
|
- **chirbit**
|
||||||
|
- **chirbit:profile**
|
||||||
- **Cinchcast**
|
- **Cinchcast**
|
||||||
- **Cinemassacre**
|
- **Cinemassacre**
|
||||||
- **clipfish**
|
- **Clipfish**
|
||||||
- **cliphunter**
|
- **cliphunter**
|
||||||
- **Clipsyndicate**
|
- **Clipsyndicate**
|
||||||
|
- **cloudtime**: CloudTime
|
||||||
- **Cloudy**
|
- **Cloudy**
|
||||||
- **Clubic**
|
- **Clubic**
|
||||||
|
- **Clyp**
|
||||||
- **cmt.com**
|
- **cmt.com**
|
||||||
- **CNET**
|
- **CNET**
|
||||||
- **CNN**
|
- **CNN**
|
||||||
@@ -88,31 +118,50 @@
|
|||||||
- **ComCarCoff**
|
- **ComCarCoff**
|
||||||
- **ComedyCentral**
|
- **ComedyCentral**
|
||||||
- **ComedyCentralShows**: The Daily Show / The Colbert Report
|
- **ComedyCentralShows**: The Daily Show / The Colbert Report
|
||||||
- **CondeNast**: Condé Nast media group: Condé Nast, GQ, Glamour, 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
|
||||||
- **Cracked**
|
- **Cracked**
|
||||||
- **Criterion**
|
- **Criterion**
|
||||||
|
- **CrooksAndLiars**
|
||||||
- **Crunchyroll**
|
- **Crunchyroll**
|
||||||
- **crunchyroll:playlist**
|
- **crunchyroll:playlist**
|
||||||
- **CSpan**: C-SPAN
|
- **CSpan**: C-SPAN
|
||||||
- **CtsNews**
|
- **CtsNews**: 華視新聞
|
||||||
- **culturebox.francetvinfo.fr**
|
- **culturebox.francetvinfo.fr**
|
||||||
|
- **CultureUnplugged**
|
||||||
|
- **CWTV**
|
||||||
- **dailymotion**
|
- **dailymotion**
|
||||||
- **dailymotion:playlist**
|
- **dailymotion:playlist**
|
||||||
- **dailymotion:user**
|
- **dailymotion:user**
|
||||||
|
- **DailymotionCloud**
|
||||||
- **daum.net**
|
- **daum.net**
|
||||||
|
- **daum.net:clip**
|
||||||
|
- **daum.net:playlist**
|
||||||
|
- **daum.net:user**
|
||||||
- **DBTV**
|
- **DBTV**
|
||||||
|
- **DCN**
|
||||||
|
- **dcn:live**
|
||||||
|
- **dcn:season**
|
||||||
|
- **dcn:video**
|
||||||
- **DctpTv**
|
- **DctpTv**
|
||||||
- **DeezerPlaylist**
|
- **DeezerPlaylist**
|
||||||
- **defense.gouv.fr**
|
- **defense.gouv.fr**
|
||||||
|
- **democracynow**
|
||||||
|
- **DHM**: Filmarchiv - Deutsches Historisches Museum
|
||||||
|
- **Digiteka**
|
||||||
- **Discovery**
|
- **Discovery**
|
||||||
- **divxstage**: DivxStage
|
|
||||||
- **Dotsub**
|
- **Dotsub**
|
||||||
|
- **DouyuTV**: 斗鱼
|
||||||
|
- **DPlay**
|
||||||
|
- **dramafever**
|
||||||
|
- **dramafever:series**
|
||||||
- **DRBonanza**
|
- **DRBonanza**
|
||||||
- **Dropbox**
|
- **Dropbox**
|
||||||
- **DrTuber**
|
- **DrTuber**
|
||||||
- **DRTV**
|
- **DRTV**
|
||||||
- **Dump**
|
- **Dump**
|
||||||
|
- **Dumpert**
|
||||||
- **dvtv**: http://video.aktualne.cz/
|
- **dvtv**: http://video.aktualne.cz/
|
||||||
|
- **EaglePlatform**
|
||||||
- **EbaumsWorld**
|
- **EbaumsWorld**
|
||||||
- **EchoMsk**
|
- **EchoMsk**
|
||||||
- **eHow**
|
- **eHow**
|
||||||
@@ -127,48 +176,62 @@
|
|||||||
- **Eporner**
|
- **Eporner**
|
||||||
- **EroProfile**
|
- **EroProfile**
|
||||||
- **Escapist**
|
- **Escapist**
|
||||||
|
- **ESPN**
|
||||||
|
- **EsriVideo**
|
||||||
|
- **Europa**
|
||||||
- **EveryonesMixtape**
|
- **EveryonesMixtape**
|
||||||
- **exfm**: ex.fm
|
- **exfm**: ex.fm
|
||||||
- **ExpoTV**
|
- **ExpoTV**
|
||||||
- **ExtremeTube**
|
- **ExtremeTube**
|
||||||
- **facebook**
|
- **facebook**
|
||||||
|
- **facebook:post**
|
||||||
- **faz.net**
|
- **faz.net**
|
||||||
- **fc2**
|
- **fc2**
|
||||||
|
- **Fczenit**
|
||||||
- **fernsehkritik.tv**
|
- **fernsehkritik.tv**
|
||||||
- **fernsehkritik.tv:postecke**
|
|
||||||
- **Firedrive**
|
|
||||||
- **Firstpost**
|
- **Firstpost**
|
||||||
|
- **FiveTV**
|
||||||
- **Flickr**
|
- **Flickr**
|
||||||
- **Folketinget**: Folketinget (ft.dk; Danish parliament)
|
- **Folketinget**: Folketinget (ft.dk; Danish parliament)
|
||||||
|
- **FootyRoom**
|
||||||
|
- **FOX**
|
||||||
- **Foxgay**
|
- **Foxgay**
|
||||||
- **FoxNews**
|
- **FoxNews**: Fox News and Fox Business Video
|
||||||
|
- **FoxSports**
|
||||||
- **france2.fr:generation-quoi**
|
- **france2.fr:generation-quoi**
|
||||||
- **FranceCulture**
|
- **FranceCulture**
|
||||||
|
- **FranceCultureEmission**
|
||||||
- **FranceInter**
|
- **FranceInter**
|
||||||
- **francetv**: France 2, 3, 4, 5 and Ô
|
- **francetv**: France 2, 3, 4, 5 and Ô
|
||||||
- **francetvinfo.fr**
|
- **francetvinfo.fr**
|
||||||
- **Freesound**
|
- **Freesound**
|
||||||
- **freespeech.org**
|
- **freespeech.org**
|
||||||
- **FreeVideo**
|
- **FreeVideo**
|
||||||
|
- **Funimation**
|
||||||
- **FunnyOrDie**
|
- **FunnyOrDie**
|
||||||
|
- **GameInformer**
|
||||||
- **Gamekings**
|
- **Gamekings**
|
||||||
- **GameOne**
|
- **GameOne**
|
||||||
- **gameone:playlist**
|
- **gameone:playlist**
|
||||||
|
- **Gamersyde**
|
||||||
- **GameSpot**
|
- **GameSpot**
|
||||||
- **GameStar**
|
- **GameStar**
|
||||||
- **Gametrailers**
|
- **Gametrailers**
|
||||||
|
- **Gazeta**
|
||||||
- **GDCVault**
|
- **GDCVault**
|
||||||
- **generic**: Generic downloader that works on some sites
|
- **generic**: Generic downloader that works on some sites
|
||||||
|
- **Gfycat**
|
||||||
- **GiantBomb**
|
- **GiantBomb**
|
||||||
- **Giga**
|
- **Giga**
|
||||||
- **Glide**: Glide mobile video messages (glide.me)
|
- **Glide**: Glide mobile video messages (glide.me)
|
||||||
- **Globo**
|
- **Globo**
|
||||||
|
- **GloboArticle**
|
||||||
- **GodTube**
|
- **GodTube**
|
||||||
- **GoldenMoustache**
|
- **GoldenMoustache**
|
||||||
- **Golem**
|
- **Golem**
|
||||||
- **GorillaVid**: GorillaVid.in, daclips.in, movpod.in and fastvideo.in
|
- **GoogleDrive**
|
||||||
- **Goshgay**
|
- **Goshgay**
|
||||||
- **Grooveshark**
|
- **GPUTechConf**
|
||||||
- **Groupon**
|
- **Groupon**
|
||||||
- **Hark**
|
- **Hark**
|
||||||
- **HearThisAt**
|
- **HearThisAt**
|
||||||
@@ -177,12 +240,11 @@
|
|||||||
- **Helsinki**: helsinki.fi
|
- **Helsinki**: helsinki.fi
|
||||||
- **HentaiStigma**
|
- **HentaiStigma**
|
||||||
- **HistoricFilms**
|
- **HistoricFilms**
|
||||||
- **History**
|
|
||||||
- **hitbox**
|
- **hitbox**
|
||||||
- **hitbox:live**
|
- **hitbox:live**
|
||||||
- **HornBunny**
|
- **HornBunny**
|
||||||
- **HostingBulk**
|
|
||||||
- **HotNewHipHop**
|
- **HotNewHipHop**
|
||||||
|
- **HotStar**
|
||||||
- **Howcast**
|
- **Howcast**
|
||||||
- **HowStuffWorks**
|
- **HowStuffWorks**
|
||||||
- **HuffPost**: Huffington Post
|
- **HuffPost**: Huffington Post
|
||||||
@@ -191,51 +253,83 @@
|
|||||||
- **ign.com**
|
- **ign.com**
|
||||||
- **imdb**: Internet Movie Database trailers
|
- **imdb**: Internet Movie Database trailers
|
||||||
- **imdb:list**: Internet Movie Database lists
|
- **imdb:list**: Internet Movie Database lists
|
||||||
|
- **Imgur**
|
||||||
|
- **ImgurAlbum**
|
||||||
- **Ina**
|
- **Ina**
|
||||||
|
- **Indavideo**
|
||||||
|
- **IndavideoEmbed**
|
||||||
- **InfoQ**
|
- **InfoQ**
|
||||||
- **Instagram**
|
- **Instagram**
|
||||||
- **instagram:user**: Instagram user profile
|
- **instagram:user**: Instagram user profile
|
||||||
- **InternetVideoArchive**
|
- **InternetVideoArchive**
|
||||||
- **IPrima**
|
- **IPrima**
|
||||||
|
- **iqiyi**: 爱奇艺
|
||||||
|
- **Ir90Tv**
|
||||||
- **ivi**: ivi.ru
|
- **ivi**: ivi.ru
|
||||||
- **ivi:compilation**: ivi.ru compilations
|
- **ivi:compilation**: ivi.ru compilations
|
||||||
|
- **ivideon**: Ivideon TV
|
||||||
- **Izlesene**
|
- **Izlesene**
|
||||||
- **JadoreCettePub**
|
- **JadoreCettePub**
|
||||||
- **JeuxVideo**
|
- **JeuxVideo**
|
||||||
- **Jove**
|
- **Jove**
|
||||||
- **jpopsuki.tv**
|
- **jpopsuki.tv**
|
||||||
- **Jukebox**
|
- **JWPlatform**
|
||||||
|
- **Kaltura**
|
||||||
|
- **KanalPlay**: Kanal 5/9/11 Play
|
||||||
- **Kankan**
|
- **Kankan**
|
||||||
- **Karaoketv**
|
- **Karaoketv**
|
||||||
|
- **KarriereVideos**
|
||||||
- **keek**
|
- **keek**
|
||||||
- **KeezMovies**
|
- **KeezMovies**
|
||||||
- **KhanAcademy**
|
- **KhanAcademy**
|
||||||
- **KickStarter**
|
- **KickStarter**
|
||||||
|
- **KonserthusetPlay**
|
||||||
- **kontrtube**: KontrTube.ru - Труба зовёт
|
- **kontrtube**: KontrTube.ru - Труба зовёт
|
||||||
- **KrasView**: Красвью
|
- **KrasView**: Красвью
|
||||||
- **Ku6**
|
- **Ku6**
|
||||||
|
- **kuwo:album**: 酷我音乐 - 专辑
|
||||||
|
- **kuwo:category**: 酷我音乐 - 分类
|
||||||
|
- **kuwo:chart**: 酷我音乐 - 排行榜
|
||||||
|
- **kuwo:mv**: 酷我音乐 - MV
|
||||||
|
- **kuwo:singer**: 酷我音乐 - 歌手
|
||||||
|
- **kuwo:song**: 酷我音乐
|
||||||
- **la7.tv**
|
- **la7.tv**
|
||||||
- **Laola1Tv**
|
- **Laola1Tv**
|
||||||
|
- **Lecture2Go**
|
||||||
|
- **Lemonde**
|
||||||
|
- **Letv**: 乐视网
|
||||||
|
- **LetvCloud**: 乐视云
|
||||||
|
- **LetvPlaylist**
|
||||||
|
- **LetvTv**
|
||||||
|
- **Libsyn**
|
||||||
|
- **life:embed**
|
||||||
- **lifenews**: LIFE | NEWS
|
- **lifenews**: LIFE | NEWS
|
||||||
|
- **limelight**
|
||||||
|
- **limelight:channel**
|
||||||
|
- **limelight:channel_list**
|
||||||
- **LiveLeak**
|
- **LiveLeak**
|
||||||
- **livestream**
|
- **livestream**
|
||||||
- **livestream:original**
|
- **livestream:original**
|
||||||
- **LnkGo**
|
- **LnkGo**
|
||||||
|
- **LoveHomePorn**
|
||||||
- **lrt.lt**
|
- **lrt.lt**
|
||||||
- **lynda**: lynda.com videos
|
- **lynda**: lynda.com videos
|
||||||
- **lynda:course**: lynda.com online courses
|
- **lynda:course**: lynda.com online courses
|
||||||
- **m6**
|
- **m6**
|
||||||
- **macgamestore**: MacGameStore trailers
|
- **macgamestore**: MacGameStore trailers
|
||||||
- **mailru**: Видео@Mail.Ru
|
- **mailru**: Видео@Mail.Ru
|
||||||
|
- **MakerTV**
|
||||||
- **Malemotion**
|
- **Malemotion**
|
||||||
- **MDR**
|
- **MatchTV**
|
||||||
|
- **MDR**: MDR.DE and KiKA
|
||||||
- **media.ccc.de**
|
- **media.ccc.de**
|
||||||
- **metacafe**
|
- **metacafe**
|
||||||
- **Metacritic**
|
- **Metacritic**
|
||||||
- **Mgoon**
|
- **Mgoon**
|
||||||
- **Minhateca**
|
- **Minhateca**
|
||||||
- **MinistryGrid**
|
- **MinistryGrid**
|
||||||
- **mitele.es**
|
- **miomio.tv**
|
||||||
|
- **MiTele**: mitele.es
|
||||||
- **mixcloud**
|
- **mixcloud**
|
||||||
- **MLB**
|
- **MLB**
|
||||||
- **MoeVideo**: LetitBit video services: moevideo.net, playreplay.net and videochart.net
|
- **MoeVideo**: LetitBit video services: moevideo.net, playreplay.net and videochart.net
|
||||||
@@ -247,144 +341,218 @@
|
|||||||
- **Motherless**
|
- **Motherless**
|
||||||
- **Motorsport**: motorsport.com
|
- **Motorsport**: motorsport.com
|
||||||
- **MovieClips**
|
- **MovieClips**
|
||||||
|
- **MovieFap**
|
||||||
- **Moviezine**
|
- **Moviezine**
|
||||||
- **movshare**: MovShare
|
|
||||||
- **MPORA**
|
- **MPORA**
|
||||||
|
- **MSNBC**
|
||||||
- **MTV**
|
- **MTV**
|
||||||
|
- **mtv.de**
|
||||||
- **mtviggy.com**
|
- **mtviggy.com**
|
||||||
- **mtvservices:embedded**
|
- **mtvservices:embedded**
|
||||||
- **MuenchenTV**: münchen.tv
|
- **MuenchenTV**: münchen.tv
|
||||||
- **MusicPlayOn**
|
- **MusicPlayOn**
|
||||||
- **MusicVault**
|
|
||||||
- **muzu.tv**
|
- **muzu.tv**
|
||||||
|
- **Mwave**
|
||||||
- **MySpace**
|
- **MySpace**
|
||||||
- **MySpace:album**
|
- **MySpace:album**
|
||||||
- **MySpass**
|
- **MySpass**
|
||||||
|
- **Myvi**
|
||||||
- **myvideo**
|
- **myvideo**
|
||||||
- **MyVidster**
|
- **MyVidster**
|
||||||
- **n-tv.de**
|
- **n-tv.de**
|
||||||
|
- **NationalGeographic**
|
||||||
- **Naver**
|
- **Naver**
|
||||||
- **NBA**
|
- **NBA**
|
||||||
- **NBC**
|
- **NBC**
|
||||||
- **NBCNews**
|
- **NBCNews**
|
||||||
- **ndr**: NDR.de - Mediathek
|
- **NBCSports**
|
||||||
|
- **NBCSportsVPlayer**
|
||||||
|
- **ndr**: NDR.de - Norddeutscher Rundfunk
|
||||||
|
- **ndr:embed**
|
||||||
|
- **ndr:embed:base**
|
||||||
- **NDTV**
|
- **NDTV**
|
||||||
- **NerdCubedFeed**
|
- **NerdCubedFeed**
|
||||||
- **Nerdist**
|
- **Nerdist**
|
||||||
|
- **netease:album**: 网易云音乐 - 专辑
|
||||||
|
- **netease:djradio**: 网易云音乐 - 电台
|
||||||
|
- **netease:mv**: 网易云音乐 - MV
|
||||||
|
- **netease:playlist**: 网易云音乐 - 歌单
|
||||||
|
- **netease:program**: 网易云音乐 - 电台节目
|
||||||
|
- **netease:singer**: 网易云音乐 - 歌手
|
||||||
|
- **netease:song**: 网易云音乐
|
||||||
- **Netzkino**
|
- **Netzkino**
|
||||||
- **Newgrounds**
|
- **Newgrounds**
|
||||||
- **Newstube**
|
- **Newstube**
|
||||||
- **NextMedia**
|
- **NextMedia**: 蘋果日報
|
||||||
- **NextMediaActionNews**
|
- **NextMediaActionNews**: 蘋果日報 - 動新聞
|
||||||
|
- **nextmovie.com**
|
||||||
- **nfb**: National Film Board of Canada
|
- **nfb**: National Film Board of Canada
|
||||||
- **nfl.com**
|
- **nfl.com**
|
||||||
- **nhl.com**
|
- **nhl.com**
|
||||||
- **nhl.com:news**: NHL news
|
- **nhl.com:news**: NHL news
|
||||||
- **nhl.com:videocenter**: NHL videocenter category
|
- **nhl.com:videocenter**: NHL videocenter category
|
||||||
|
- **nick.com**
|
||||||
- **niconico**: ニコニコ動画
|
- **niconico**: ニコニコ動画
|
||||||
- **NiconicoPlaylist**
|
- **NiconicoPlaylist**
|
||||||
|
- **njoy**: N-JOY
|
||||||
|
- **njoy:embed**
|
||||||
- **Noco**
|
- **Noco**
|
||||||
- **Normalboots**
|
- **Normalboots**
|
||||||
- **NosVideo**
|
- **NosVideo**
|
||||||
|
- **Nova**: TN.cz, Prásk.tv, Nova.cz, Novaplus.cz, FANDA.tv, Krásná.cz and Doma.cz
|
||||||
- **novamov**: NovaMov
|
- **novamov**: NovaMov
|
||||||
- **Nowness**
|
- **nowness**
|
||||||
|
- **nowness:playlist**
|
||||||
|
- **nowness:series**
|
||||||
|
- **NowTV** (Currently broken)
|
||||||
|
- **NowTVList**
|
||||||
- **nowvideo**: NowVideo
|
- **nowvideo**: NowVideo
|
||||||
- **npo.nl**
|
- **npo**: npo.nl and ntr.nl
|
||||||
- **npo.nl:live**
|
- **npo.nl:live**
|
||||||
- **npo.nl:radio**
|
- **npo.nl:radio**
|
||||||
- **npo.nl:radio:fragment**
|
- **npo.nl:radio:fragment**
|
||||||
|
- **Npr**
|
||||||
- **NRK**
|
- **NRK**
|
||||||
- **NRKTV**
|
- **NRKPlaylist**
|
||||||
|
- **NRKTV**: NRK TV and NRK Radio
|
||||||
- **ntv.ru**
|
- **ntv.ru**
|
||||||
- **Nuvid**
|
- **Nuvid**
|
||||||
- **NYTimes**
|
- **NYTimes**
|
||||||
|
- **NYTimesArticle**
|
||||||
- **ocw.mit.edu**
|
- **ocw.mit.edu**
|
||||||
|
- **Odnoklassniki**
|
||||||
- **OktoberfestTV**
|
- **OktoberfestTV**
|
||||||
- **on.aol.com**
|
- **on.aol.com**
|
||||||
|
- **OnionStudios**
|
||||||
- **Ooyala**
|
- **Ooyala**
|
||||||
- **OpenFilm**
|
- **OoyalaExternal**
|
||||||
|
- **OraTV**
|
||||||
- **orf:fm4**: radio FM4
|
- **orf:fm4**: radio FM4
|
||||||
|
- **orf:iptv**: iptv.ORF.at
|
||||||
- **orf:oe1**: Radio Österreich 1
|
- **orf:oe1**: Radio Österreich 1
|
||||||
- **orf:tvthek**: ORF TVthek
|
- **orf:tvthek**: ORF TVthek
|
||||||
|
- **pandora.tv**: 판도라TV
|
||||||
- **parliamentlive.tv**: UK parliament videos
|
- **parliamentlive.tv**: UK parliament videos
|
||||||
- **Patreon**
|
- **Patreon**
|
||||||
- **PBS**
|
- **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)
|
||||||
- **Phoenix**
|
- **pcmag**
|
||||||
|
- **Periscope**: Periscope
|
||||||
|
- **PhilharmonieDeParis**: Philharmonie de Paris
|
||||||
|
- **phoenix.de**
|
||||||
- **Photobucket**
|
- **Photobucket**
|
||||||
|
- **Pinkbike**
|
||||||
|
- **Pladform**
|
||||||
- **PlanetaPlay**
|
- **PlanetaPlay**
|
||||||
- **play.fm**
|
- **play.fm**
|
||||||
- **played.to**
|
- **played.to**
|
||||||
|
- **Playtvak**: Playtvak.cz, iDNES.cz and Lidovky.cz
|
||||||
- **Playvid**
|
- **Playvid**
|
||||||
|
- **Playwire**
|
||||||
|
- **pluralsight**
|
||||||
|
- **pluralsight:course**
|
||||||
- **plus.google**: Google Plus
|
- **plus.google**: Google Plus
|
||||||
- **pluzz.francetv.fr**
|
- **pluzz.francetv.fr**
|
||||||
- **podomatic**
|
- **podomatic**
|
||||||
- **PornHd**
|
- **PornHd**
|
||||||
- **PornHub**
|
- **PornHub**
|
||||||
|
- **PornHubPlaylist**
|
||||||
- **Pornotube**
|
- **Pornotube**
|
||||||
|
- **PornoVoisines**
|
||||||
- **PornoXO**
|
- **PornoXO**
|
||||||
|
- **PrimeShareTV**
|
||||||
- **PromptFile**
|
- **PromptFile**
|
||||||
- **prosiebensat1**: ProSiebenSat.1 Digital
|
- **prosiebensat1**: ProSiebenSat.1 Digital
|
||||||
|
- **Puls4**
|
||||||
- **Pyvideo**
|
- **Pyvideo**
|
||||||
|
- **qqmusic**: QQ音乐
|
||||||
|
- **qqmusic:album**: QQ音乐 - 专辑
|
||||||
|
- **qqmusic:playlist**: QQ音乐 - 歌单
|
||||||
|
- **qqmusic:singer**: QQ音乐 - 歌手
|
||||||
|
- **qqmusic:toplist**: QQ音乐 - 排行榜
|
||||||
- **QuickVid**
|
- **QuickVid**
|
||||||
|
- **R7**
|
||||||
- **radio.de**
|
- **radio.de**
|
||||||
- **radiobremen**
|
- **radiobremen**
|
||||||
- **radiofrance**
|
- **radiofrance**
|
||||||
|
- **RadioJavan**
|
||||||
- **Rai**
|
- **Rai**
|
||||||
|
- **RaiTV**
|
||||||
- **RBMARadio**
|
- **RBMARadio**
|
||||||
|
- **RDS**: RDS.ca
|
||||||
- **RedTube**
|
- **RedTube**
|
||||||
|
- **RegioTV**
|
||||||
- **Restudy**
|
- **Restudy**
|
||||||
- **ReverbNation**
|
- **ReverbNation**
|
||||||
|
- **Revision3**
|
||||||
- **RingTV**
|
- **RingTV**
|
||||||
- **RottenTomatoes**
|
- **RottenTomatoes**
|
||||||
- **Roxwel**
|
- **Roxwel**
|
||||||
- **RTBF**
|
- **RTBF**
|
||||||
- **Rte**
|
- **rte**: Raidió Teilifís Éireann TV
|
||||||
|
- **rte:radio**: Raidió Teilifís Éireann radio
|
||||||
- **rtl.nl**: rtl.nl and rtlxl.nl
|
- **rtl.nl**: rtl.nl and rtlxl.nl
|
||||||
- **RTL2**
|
- **RTL2**
|
||||||
- **RTLnow**
|
|
||||||
- **RTP**
|
- **RTP**
|
||||||
- **RTS**: RTS.ch
|
- **RTS**: RTS.ch
|
||||||
- **rtve.es:alacarta**: RTVE a la carta
|
- **rtve.es:alacarta**: RTVE a la carta
|
||||||
|
- **rtve.es:infantil**: RTVE infantil
|
||||||
- **rtve.es:live**: RTVE.es live streams
|
- **rtve.es:live**: RTVE.es live streams
|
||||||
|
- **RTVNH**
|
||||||
- **RUHD**
|
- **RUHD**
|
||||||
|
- **RulePorn**
|
||||||
- **rutube**: Rutube videos
|
- **rutube**: Rutube videos
|
||||||
- **rutube:channel**: Rutube channels
|
- **rutube:channel**: Rutube channels
|
||||||
- **rutube:embed**: Rutube embedded videos
|
- **rutube:embed**: Rutube embedded videos
|
||||||
- **rutube:movie**: Rutube movies
|
- **rutube:movie**: Rutube movies
|
||||||
- **rutube:person**: Rutube person videos
|
- **rutube:person**: Rutube person videos
|
||||||
- **RUTV**: RUTV.RU
|
- **RUTV**: RUTV.RU
|
||||||
|
- **Ruutu**
|
||||||
|
- **safari**: safaribooksonline.com online video
|
||||||
|
- **safari:course**: safaribooksonline.com online courses
|
||||||
- **Sandia**: Sandia National Laboratories
|
- **Sandia**: Sandia National Laboratories
|
||||||
- **Sapo**: SAPO Vídeos
|
- **Sapo**: SAPO Vídeos
|
||||||
- **savefrom.net**
|
- **savefrom.net**
|
||||||
- **SBS**: sbs.com.au
|
- **SBS**: sbs.com.au
|
||||||
|
- **schooltv**
|
||||||
- **SciVee**
|
- **SciVee**
|
||||||
- **screen.yahoo:search**: Yahoo screen search
|
- **screen.yahoo:search**: Yahoo screen search
|
||||||
- **Screencast**
|
- **Screencast**
|
||||||
- **ScreencastOMatic**
|
- **ScreencastOMatic**
|
||||||
- **ScreenwaveMedia**
|
- **ScreenwaveMedia**
|
||||||
|
- **SenateISVP**
|
||||||
- **ServingSys**
|
- **ServingSys**
|
||||||
- **Sexu**
|
- **Sexu**
|
||||||
- **SexyKarma**: Sexy Karma and Watch Indian Porn
|
- **SexyKarma**: Sexy Karma and Watch Indian Porn
|
||||||
- **Shared**
|
- **Shahid**
|
||||||
|
- **Shared**: shared.sx and vivo.sx
|
||||||
- **ShareSix**
|
- **ShareSix**
|
||||||
- **Sina**
|
- **Sina**
|
||||||
|
- **skynewsarabia:video**
|
||||||
|
- **skynewsarabia:video**
|
||||||
- **Slideshare**
|
- **Slideshare**
|
||||||
- **Slutload**
|
- **Slutload**
|
||||||
- **smotri**: Smotri.com
|
- **smotri**: Smotri.com
|
||||||
- **smotri:broadcast**: Smotri.com broadcasts
|
- **smotri:broadcast**: Smotri.com broadcasts
|
||||||
- **smotri:community**: Smotri.com community videos
|
- **smotri:community**: Smotri.com community videos
|
||||||
- **smotri:user**: Smotri.com user videos
|
- **smotri:user**: Smotri.com user videos
|
||||||
|
- **SnagFilms**
|
||||||
|
- **SnagFilmsEmbed**
|
||||||
- **Snotr**
|
- **Snotr**
|
||||||
- **Sockshare**
|
|
||||||
- **Sohu**
|
- **Sohu**
|
||||||
- **soundcloud**
|
- **soundcloud**
|
||||||
- **soundcloud:playlist**
|
- **soundcloud:playlist**
|
||||||
|
- **soundcloud:search**: Soundcloud search
|
||||||
- **soundcloud:set**
|
- **soundcloud:set**
|
||||||
- **soundcloud:user**
|
- **soundcloud:user**
|
||||||
- **Soundgasm**
|
- **soundgasm**
|
||||||
|
- **soundgasm:profile**
|
||||||
- **southpark.cc.com**
|
- **southpark.cc.com**
|
||||||
|
- **southpark.cc.com:español**
|
||||||
- **southpark.de**
|
- **southpark.de**
|
||||||
|
- **southpark.nl**
|
||||||
|
- **southparkstudios.dk**
|
||||||
- **Space**
|
- **Space**
|
||||||
|
- **SpankBang**
|
||||||
- **Spankwire**
|
- **Spankwire**
|
||||||
- **Spiegel**
|
- **Spiegel**
|
||||||
- **Spiegel:Article**: Articles on spiegel.de
|
- **Spiegel:Article**: Articles on spiegel.de
|
||||||
@@ -392,15 +560,21 @@
|
|||||||
- **Spike**
|
- **Spike**
|
||||||
- **Sport5**
|
- **Sport5**
|
||||||
- **SportBox**
|
- **SportBox**
|
||||||
|
- **SportBoxEmbed**
|
||||||
- **SportDeutschland**
|
- **SportDeutschland**
|
||||||
- **SRMediathek**: Saarländischer Rundfunk
|
- **Sportschau**
|
||||||
|
- **SRGSSR**
|
||||||
|
- **SRGSSRPlay**: srf.ch, rts.ch, rsi.ch, rtr.ch and swissinfo.ch play sites
|
||||||
|
- **SSA**
|
||||||
- **stanfordoc**: Stanford Open ClassRoom
|
- **stanfordoc**: Stanford Open ClassRoom
|
||||||
- **Steam**
|
- **Steam**
|
||||||
|
- **Stitcher**
|
||||||
- **streamcloud.eu**
|
- **streamcloud.eu**
|
||||||
- **StreamCZ**
|
- **StreamCZ**
|
||||||
- **StreetVoice**
|
- **StreetVoice**
|
||||||
- **SunPorno**
|
- **SunPorno**
|
||||||
- **SVTPlay**
|
- **SVT**
|
||||||
|
- **SVTPlay**: SVT Play and Öppet arkiv
|
||||||
- **SWRMediathek**
|
- **SWRMediathek**
|
||||||
- **Syfy**
|
- **Syfy**
|
||||||
- **SztvHu**
|
- **SztvHu**
|
||||||
@@ -414,40 +588,57 @@
|
|||||||
- **TeamFour**
|
- **TeamFour**
|
||||||
- **TechTalks**
|
- **TechTalks**
|
||||||
- **techtv.mit.edu**
|
- **techtv.mit.edu**
|
||||||
- **TED**
|
- **ted**
|
||||||
- **tegenlicht.vpro.nl**
|
- **Tele13**
|
||||||
- **TeleBruxelles**
|
- **TeleBruxelles**
|
||||||
- **telecinco.es**
|
- **Telecinco**: telecinco.es, cuatro.com and mediaset.es
|
||||||
|
- **Telegraaf**
|
||||||
- **TeleMB**
|
- **TeleMB**
|
||||||
- **TeleTask**
|
- **TeleTask**
|
||||||
- **TenPlay**
|
- **TenPlay**
|
||||||
- **TestTube**
|
|
||||||
- **TF1**
|
- **TF1**
|
||||||
|
- **TheIntercept**
|
||||||
- **TheOnion**
|
- **TheOnion**
|
||||||
- **ThePlatform**
|
- **ThePlatform**
|
||||||
|
- **ThePlatformFeed**
|
||||||
- **TheSixtyOne**
|
- **TheSixtyOne**
|
||||||
|
- **ThisAmericanLife**
|
||||||
- **ThisAV**
|
- **ThisAV**
|
||||||
- **THVideo**
|
- **THVideo**
|
||||||
- **THVideoPlaylist**
|
- **THVideoPlaylist**
|
||||||
- **tinypic**: tinypic.com videos
|
- **tinypic**: tinypic.com videos
|
||||||
- **tlc.com**
|
|
||||||
- **tlc.de**
|
- **tlc.de**
|
||||||
- **TMZ**
|
- **TMZ**
|
||||||
|
- **TMZArticle**
|
||||||
- **TNAFlix**
|
- **TNAFlix**
|
||||||
|
- **toggle**
|
||||||
- **tou.tv**
|
- **tou.tv**
|
||||||
- **Toypics**: Toypics user profile
|
- **Toypics**: Toypics user profile
|
||||||
- **ToypicsUser**: Toypics user profile
|
- **ToypicsUser**: Toypics user profile
|
||||||
- **TrailerAddict** (Currently broken)
|
- **TrailerAddict** (Currently broken)
|
||||||
- **Trilulilu**
|
- **Trilulilu**
|
||||||
|
- **trollvids**
|
||||||
- **TruTube**
|
- **TruTube**
|
||||||
- **Tube8**
|
- **Tube8**
|
||||||
- **Tudou**
|
- **TubiTv**
|
||||||
|
- **tudou**
|
||||||
|
- **tudou:album**
|
||||||
|
- **tudou:playlist**
|
||||||
- **Tumblr**
|
- **Tumblr**
|
||||||
- **TuneIn**
|
- **tunein:clip**
|
||||||
|
- **tunein:program**
|
||||||
|
- **tunein:station**
|
||||||
|
- **tunein:topic**
|
||||||
- **Turbo**
|
- **Turbo**
|
||||||
- **Tutv**
|
- **Tutv**
|
||||||
- **tv.dfb.de**
|
- **tv.dfb.de**
|
||||||
|
- **TV2**
|
||||||
|
- **TV2Article**
|
||||||
|
- **TV4**: tv4.se and tv4play.se
|
||||||
|
- **TVC**
|
||||||
|
- **TVCArticle**
|
||||||
- **tvigle**: Интернет-телевидение Tvigle.ru
|
- **tvigle**: Интернет-телевидение Tvigle.ru
|
||||||
|
- **tvland.com**
|
||||||
- **tvp.pl**
|
- **tvp.pl**
|
||||||
- **tvp.pl:Series**
|
- **tvp.pl:Series**
|
||||||
- **TVPlay**: TV3Play and related services
|
- **TVPlay**: TV3Play and related services
|
||||||
@@ -459,37 +650,48 @@
|
|||||||
- **twitch:stream**
|
- **twitch:stream**
|
||||||
- **twitch:video**
|
- **twitch:video**
|
||||||
- **twitch:vod**
|
- **twitch:vod**
|
||||||
|
- **twitter**
|
||||||
|
- **twitter:card**
|
||||||
- **Ubu**
|
- **Ubu**
|
||||||
- **udemy**
|
- **udemy**
|
||||||
- **udemy:course**
|
- **udemy:course**
|
||||||
|
- **UDNEmbed**: 聯合影音
|
||||||
- **Unistra**
|
- **Unistra**
|
||||||
- **Urort**: NRK P3 Urørt
|
- **Urort**: NRK P3 Urørt
|
||||||
- **ustream**
|
- **ustream**
|
||||||
- **ustream:channel**
|
- **ustream:channel**
|
||||||
|
- **Varzesh3**
|
||||||
- **Vbox7**
|
- **Vbox7**
|
||||||
- **VeeHD**
|
- **VeeHD**
|
||||||
- **Veoh**
|
- **Veoh**
|
||||||
|
- **Vessel**
|
||||||
- **Vesti**: Вести.Ru
|
- **Vesti**: Вести.Ru
|
||||||
- **Vevo**
|
- **Vevo**
|
||||||
- **VGTV**
|
- **VGTV**: VGTV, BTTV, FTV, Aftenposten and Aftonbladet
|
||||||
- **vh1.com**
|
- **vh1.com**
|
||||||
- **Vice**
|
- **Vice**
|
||||||
- **Viddler**
|
- **Viddler**
|
||||||
- **video.google:search**: Google Video search
|
- **video.google:search**: Google Video search
|
||||||
- **video.mit.edu**
|
- **video.mit.edu**
|
||||||
- **VideoBam**
|
|
||||||
- **VideoDetective**
|
- **VideoDetective**
|
||||||
- **videofy.me**
|
- **videofy.me**
|
||||||
- **videolectures.net**
|
- **VideoMega** (Currently broken)
|
||||||
- **VideoMega**
|
- **videomore**
|
||||||
|
- **videomore:season**
|
||||||
|
- **videomore:video**
|
||||||
- **VideoPremium**
|
- **VideoPremium**
|
||||||
- **VideoTt**: video.tt - Your True Tube
|
- **VideoTt**: video.tt - Your True Tube (Currently broken)
|
||||||
- **videoweed**: VideoWeed
|
- **videoweed**: VideoWeed
|
||||||
- **Vidme**
|
- **vidme**
|
||||||
|
- **vidme:user**
|
||||||
|
- **vidme:user:likes**
|
||||||
- **Vidzi**
|
- **Vidzi**
|
||||||
- **vier**
|
- **vier**
|
||||||
- **vier:videos**
|
- **vier:videos**
|
||||||
|
- **Viewster**
|
||||||
|
- **Viidea**
|
||||||
- **viki**
|
- **viki**
|
||||||
|
- **viki:channel**
|
||||||
- **vimeo**
|
- **vimeo**
|
||||||
- **vimeo:album**
|
- **vimeo:album**
|
||||||
- **vimeo:channel**
|
- **vimeo:channel**
|
||||||
@@ -498,13 +700,16 @@
|
|||||||
- **vimeo:review**: Review pages on vimeo
|
- **vimeo:review**: Review pages on vimeo
|
||||||
- **vimeo:user**
|
- **vimeo:user**
|
||||||
- **vimeo:watchlater**: Vimeo watch later list, "vimeowatchlater" keyword (requires authentication)
|
- **vimeo:watchlater**: Vimeo watch later list, "vimeowatchlater" keyword (requires authentication)
|
||||||
- **Vimple**: Vimple.ru
|
- **Vimple**: Vimple - one-click video hosting
|
||||||
- **Vine**
|
- **Vine**
|
||||||
- **vine:user**
|
- **vine:user**
|
||||||
- **vk.com**
|
- **vk**: VK
|
||||||
- **vk.com:user-videos**: vk.com:All of a user's videos
|
- **vk:uservideos**: VK - User's Videos
|
||||||
|
- **vlive**
|
||||||
- **Vodlocker**
|
- **Vodlocker**
|
||||||
|
- **VoiceRepublic**
|
||||||
- **Vporn**
|
- **Vporn**
|
||||||
|
- **vpro**: npo.nl and ntr.nl
|
||||||
- **VRT**
|
- **VRT**
|
||||||
- **vube**: Vube.com
|
- **vube**: Vube.com
|
||||||
- **VuClip**
|
- **VuClip**
|
||||||
@@ -517,28 +722,39 @@
|
|||||||
- **wdr:mobile**
|
- **wdr:mobile**
|
||||||
- **WDRMaus**: Sendung mit der Maus
|
- **WDRMaus**: Sendung mit der Maus
|
||||||
- **WebOfStories**
|
- **WebOfStories**
|
||||||
|
- **WebOfStoriesPlaylist**
|
||||||
- **Weibo**
|
- **Weibo**
|
||||||
|
- **WeiqiTV**: WQTV
|
||||||
|
- **wholecloud**: WholeCloud
|
||||||
- **Wimp**
|
- **Wimp**
|
||||||
- **Wistia**
|
- **Wistia**
|
||||||
|
- **WNL**
|
||||||
- **WorldStarHipHop**
|
- **WorldStarHipHop**
|
||||||
- **wrzuta.pl**
|
- **wrzuta.pl**
|
||||||
- **WSJ**: Wall Street Journal
|
- **WSJ**: Wall Street Journal
|
||||||
- **XBef**
|
- **XBef**
|
||||||
- **XboxClips**
|
- **XboxClips**
|
||||||
|
- **XFileShare**: XFileShare based sites: GorillaVid.in, daclips.in, movpod.in, fastvideo.in, realvid.net, filehoot.com and vidto.me
|
||||||
- **XHamster**
|
- **XHamster**
|
||||||
|
- **XHamsterEmbed**
|
||||||
- **XMinus**
|
- **XMinus**
|
||||||
- **XNXX**
|
- **XNXX**
|
||||||
|
- **Xstream**
|
||||||
- **XTube**
|
- **XTube**
|
||||||
- **XTubeUser**: XTube user profile
|
- **XTubeUser**: XTube user profile
|
||||||
- **Xuite**
|
- **Xuite**: 隨意窩Xuite影音
|
||||||
- **XVideos**
|
- **XVideos**
|
||||||
- **XXXYMovies**
|
- **XXXYMovies**
|
||||||
- **Yahoo**: Yahoo screen and movies
|
- **Yahoo**: Yahoo screen and movies
|
||||||
- **Yam**
|
- **Yam**: 蕃薯藤yam天空部落
|
||||||
|
- **yandexmusic:album**: Яндекс.Музыка - Альбом
|
||||||
|
- **yandexmusic:playlist**: Яндекс.Музыка - Плейлист
|
||||||
|
- **yandexmusic:track**: Яндекс.Музыка - Трек
|
||||||
- **YesJapan**
|
- **YesJapan**
|
||||||
|
- **yinyuetai:video**: 音悦Tai
|
||||||
- **Ynet**
|
- **Ynet**
|
||||||
- **YouJizz**
|
- **YouJizz**
|
||||||
- **Youku**
|
- **youku**: 优酷
|
||||||
- **YouPorn**
|
- **YouPorn**
|
||||||
- **YourUpload**
|
- **YourUpload**
|
||||||
- **youtube**: YouTube.com
|
- **youtube**: YouTube.com
|
||||||
@@ -546,6 +762,7 @@
|
|||||||
- **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: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
|
||||||
@@ -553,8 +770,10 @@
|
|||||||
- **youtube:show**: YouTube.com (multi-season) shows
|
- **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:user**: YouTube.com user videos (URL or "ytuser" keyword)
|
||||||
- **youtube:watch_later**: Youtube watch later list, ":ytwatchlater" for short (requires authentication)
|
- **youtube:watchlater**: Youtube watch later list, ":ytwatchlater" for short (requires authentication)
|
||||||
|
- **Zapiks**
|
||||||
- **ZDF**
|
- **ZDF**
|
||||||
- **ZDFChannel**
|
- **ZDFChannel**
|
||||||
- **zingmp3:album**: mp3.zing.vn albums
|
- **zingmp3:album**: mp3.zing.vn albums
|
||||||
- **zingmp3:song**: mp3.zing.vn songs
|
- **zingmp3:song**: mp3.zing.vn songs
|
||||||
|
- **ZippCast**
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -28,7 +28,7 @@ py2exe_options = {
|
|||||||
"compressed": 1,
|
"compressed": 1,
|
||||||
"optimize": 2,
|
"optimize": 2,
|
||||||
"dist_dir": '.',
|
"dist_dir": '.',
|
||||||
"dll_excludes": ['w9xpopen.exe'],
|
"dll_excludes": ['w9xpopen.exe', 'crypt32.dll'],
|
||||||
}
|
}
|
||||||
|
|
||||||
py2exe_console = [{
|
py2exe_console = [{
|
||||||
|
|||||||
133
test/helper.py
133
test/helper.py
@@ -89,68 +89,83 @@ def gettestcases(include_onlymatching=False):
|
|||||||
md5 = lambda s: hashlib.md5(s.encode('utf-8')).hexdigest()
|
md5 = lambda s: hashlib.md5(s.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def expect_info_dict(self, got_dict, expected_dict):
|
def expect_value(self, got, expected, field):
|
||||||
|
if isinstance(expected, compat_str) and expected.startswith('re:'):
|
||||||
|
match_str = expected[len('re:'):]
|
||||||
|
match_rex = re.compile(match_str)
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
isinstance(got, compat_str),
|
||||||
|
'Expected a %s object, but got %s for field %s' % (
|
||||||
|
compat_str.__name__, type(got).__name__, field))
|
||||||
|
self.assertTrue(
|
||||||
|
match_rex.match(got),
|
||||||
|
'field %s (value: %r) should match %r' % (field, got, match_str))
|
||||||
|
elif isinstance(expected, compat_str) and expected.startswith('startswith:'):
|
||||||
|
start_str = expected[len('startswith:'):]
|
||||||
|
self.assertTrue(
|
||||||
|
isinstance(got, compat_str),
|
||||||
|
'Expected a %s object, but got %s for field %s' % (
|
||||||
|
compat_str.__name__, type(got).__name__, field))
|
||||||
|
self.assertTrue(
|
||||||
|
got.startswith(start_str),
|
||||||
|
'field %s (value: %r) should start with %r' % (field, got, start_str))
|
||||||
|
elif isinstance(expected, compat_str) and expected.startswith('contains:'):
|
||||||
|
contains_str = expected[len('contains:'):]
|
||||||
|
self.assertTrue(
|
||||||
|
isinstance(got, compat_str),
|
||||||
|
'Expected a %s object, but got %s for field %s' % (
|
||||||
|
compat_str.__name__, type(got).__name__, field))
|
||||||
|
self.assertTrue(
|
||||||
|
contains_str in got,
|
||||||
|
'field %s (value: %r) should contain %r' % (field, got, contains_str))
|
||||||
|
elif isinstance(expected, type):
|
||||||
|
self.assertTrue(
|
||||||
|
isinstance(got, expected),
|
||||||
|
'Expected type %r for field %s, but got value %r of type %r' % (expected, field, got, type(got)))
|
||||||
|
elif isinstance(expected, dict) and isinstance(got, dict):
|
||||||
|
expect_dict(self, got, expected)
|
||||||
|
elif isinstance(expected, list) and isinstance(got, list):
|
||||||
|
self.assertEqual(
|
||||||
|
len(expected), len(got),
|
||||||
|
'Expect a list of length %d, but got a list of length %d for field %s' % (
|
||||||
|
len(expected), len(got), field))
|
||||||
|
for index, (item_got, item_expected) in enumerate(zip(got, expected)):
|
||||||
|
type_got = type(item_got)
|
||||||
|
type_expected = type(item_expected)
|
||||||
|
self.assertEqual(
|
||||||
|
type_expected, type_got,
|
||||||
|
'Type mismatch for list item at index %d for field %s, expected %r, got %r' % (
|
||||||
|
index, field, type_expected, type_got))
|
||||||
|
expect_value(self, item_got, item_expected, field)
|
||||||
|
else:
|
||||||
|
if isinstance(expected, compat_str) and expected.startswith('md5:'):
|
||||||
|
got = 'md5:' + md5(got)
|
||||||
|
elif isinstance(expected, compat_str) and expected.startswith('mincount:'):
|
||||||
|
self.assertTrue(
|
||||||
|
isinstance(got, (list, dict)),
|
||||||
|
'Expected field %s to be a list or a dict, but it is of type %s' % (
|
||||||
|
field, type(got).__name__))
|
||||||
|
expected_num = int(expected.partition(':')[2])
|
||||||
|
assertGreaterEqual(
|
||||||
|
self, len(got), expected_num,
|
||||||
|
'Expected %d items in field %s, but only got %d' % (expected_num, field, len(got)))
|
||||||
|
return
|
||||||
|
self.assertEqual(
|
||||||
|
expected, got,
|
||||||
|
'Invalid value for field %s, expected %r, got %r' % (field, expected, got))
|
||||||
|
|
||||||
|
|
||||||
|
def expect_dict(self, got_dict, expected_dict):
|
||||||
for info_field, expected in expected_dict.items():
|
for info_field, expected in expected_dict.items():
|
||||||
if isinstance(expected, compat_str) and expected.startswith('re:'):
|
got = got_dict.get(info_field)
|
||||||
got = got_dict.get(info_field)
|
expect_value(self, got, expected, info_field)
|
||||||
match_str = expected[len('re:'):]
|
|
||||||
match_rex = re.compile(match_str)
|
|
||||||
|
|
||||||
self.assertTrue(
|
|
||||||
isinstance(got, compat_str),
|
|
||||||
'Expected a %s object, but got %s for field %s' % (
|
|
||||||
compat_str.__name__, type(got).__name__, info_field))
|
|
||||||
self.assertTrue(
|
|
||||||
match_rex.match(got),
|
|
||||||
'field %s (value: %r) should match %r' % (info_field, got, match_str))
|
|
||||||
elif isinstance(expected, compat_str) and expected.startswith('startswith:'):
|
|
||||||
got = got_dict.get(info_field)
|
|
||||||
start_str = expected[len('startswith:'):]
|
|
||||||
self.assertTrue(
|
|
||||||
isinstance(got, compat_str),
|
|
||||||
'Expected a %s object, but got %s for field %s' % (
|
|
||||||
compat_str.__name__, type(got).__name__, info_field))
|
|
||||||
self.assertTrue(
|
|
||||||
got.startswith(start_str),
|
|
||||||
'field %s (value: %r) should start with %r' % (info_field, got, start_str))
|
|
||||||
elif isinstance(expected, compat_str) and expected.startswith('contains:'):
|
|
||||||
got = got_dict.get(info_field)
|
|
||||||
contains_str = expected[len('contains:'):]
|
|
||||||
self.assertTrue(
|
|
||||||
isinstance(got, compat_str),
|
|
||||||
'Expected a %s object, but got %s for field %s' % (
|
|
||||||
compat_str.__name__, type(got).__name__, info_field))
|
|
||||||
self.assertTrue(
|
|
||||||
contains_str in got,
|
|
||||||
'field %s (value: %r) should contain %r' % (info_field, got, contains_str))
|
|
||||||
elif isinstance(expected, type):
|
|
||||||
got = got_dict.get(info_field)
|
|
||||||
self.assertTrue(isinstance(got, expected),
|
|
||||||
'Expected type %r for field %s, but got value %r of type %r' % (expected, info_field, got, type(got)))
|
|
||||||
else:
|
|
||||||
if isinstance(expected, compat_str) and expected.startswith('md5:'):
|
|
||||||
got = 'md5:' + md5(got_dict.get(info_field))
|
|
||||||
elif isinstance(expected, compat_str) and expected.startswith('mincount:'):
|
|
||||||
got = got_dict.get(info_field)
|
|
||||||
self.assertTrue(
|
|
||||||
isinstance(got, list),
|
|
||||||
'Expected field %s to be a list, but it is of type %s' % (
|
|
||||||
info_field, type(got).__name__))
|
|
||||||
expected_num = int(expected.partition(':')[2])
|
|
||||||
assertGreaterEqual(
|
|
||||||
self, len(got), expected_num,
|
|
||||||
'Expected %d items in field %s, but only got %d' % (
|
|
||||||
expected_num, info_field, len(got)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
got = got_dict.get(info_field)
|
|
||||||
self.assertEqual(expected, got,
|
|
||||||
'invalid value for field %s, expected %r, got %r' % (info_field, expected, got))
|
|
||||||
|
|
||||||
|
def expect_info_dict(self, got_dict, expected_dict):
|
||||||
|
expect_dict(self, got_dict, expected_dict)
|
||||||
# Check for the presence of mandatory fields
|
# Check for the presence of mandatory fields
|
||||||
if got_dict.get('_type') != 'playlist':
|
if got_dict.get('_type') not in ('playlist', 'multi_video'):
|
||||||
for key in ('id', 'url', 'title', 'ext'):
|
for key in ('id', 'url', 'title', 'ext'):
|
||||||
self.assertTrue(got_dict.get(key), 'Missing mandatory field %s' % key)
|
self.assertTrue(got_dict.get(key), 'Missing mandatory field %s' % key)
|
||||||
# Check for mandatory fields that are automatically set by YoutubeDL
|
# Check for mandatory fields that are automatically set by YoutubeDL
|
||||||
@@ -160,7 +175,7 @@ def expect_info_dict(self, got_dict, expected_dict):
|
|||||||
# Are checkable fields missing from the test case definition?
|
# Are checkable fields missing from the test case definition?
|
||||||
test_info_dict = dict((key, value if not isinstance(value, compat_str) or len(value) < 250 else 'md5:' + md5(value))
|
test_info_dict = dict((key, value if not isinstance(value, compat_str) or len(value) < 250 else 'md5:' + md5(value))
|
||||||
for key, value in got_dict.items()
|
for key, value in got_dict.items()
|
||||||
if value and key in ('id', 'title', 'description', 'uploader', 'upload_date', 'timestamp', 'uploader_id', 'location'))
|
if value and key in ('id', 'title', 'description', 'uploader', 'upload_date', 'timestamp', 'uploader_id', 'location', 'age_limit'))
|
||||||
missing_keys = set(test_info_dict.keys()) - set(expected_dict.keys())
|
missing_keys = set(test_info_dict.keys()) - set(expected_dict.keys())
|
||||||
if missing_keys:
|
if missing_keys:
|
||||||
def _repr(v):
|
def _repr(v):
|
||||||
|
|||||||
@@ -7,8 +7,7 @@
|
|||||||
"forcethumbnail": false,
|
"forcethumbnail": false,
|
||||||
"forcetitle": false,
|
"forcetitle": false,
|
||||||
"forceurl": false,
|
"forceurl": false,
|
||||||
"format": null,
|
"format": "best",
|
||||||
"format_limit": null,
|
|
||||||
"ignoreerrors": false,
|
"ignoreerrors": false,
|
||||||
"listformats": null,
|
"listformats": null,
|
||||||
"logtostderr": false,
|
"logtostderr": false,
|
||||||
@@ -28,7 +27,7 @@
|
|||||||
"retries": 10,
|
"retries": 10,
|
||||||
"simulate": false,
|
"simulate": false,
|
||||||
"subtitleslang": null,
|
"subtitleslang": null,
|
||||||
"subtitlesformat": "srt",
|
"subtitlesformat": "best",
|
||||||
"test": true,
|
"test": true,
|
||||||
"updatetime": true,
|
"updatetime": true,
|
||||||
"usenetrc": false,
|
"usenetrc": false,
|
||||||
|
|||||||
@@ -35,10 +35,18 @@ class TestInfoExtractor(unittest.TestCase):
|
|||||||
<meta name="og:title" content='Foo'/>
|
<meta name="og:title" content='Foo'/>
|
||||||
<meta content="Some video's description " name="og:description"/>
|
<meta content="Some video's description " name="og:description"/>
|
||||||
<meta property='og:image' content='http://domain.com/pic.jpg?key1=val1&key2=val2'/>
|
<meta property='og:image' content='http://domain.com/pic.jpg?key1=val1&key2=val2'/>
|
||||||
|
<meta content='application/x-shockwave-flash' property='og:video:type'>
|
||||||
|
<meta content='Foo' property=og:foobar>
|
||||||
|
<meta name="og:test1" content='foo > < bar'/>
|
||||||
|
<meta name="og:test2" content="foo >//< bar"/>
|
||||||
'''
|
'''
|
||||||
self.assertEqual(ie._og_search_title(html), 'Foo')
|
self.assertEqual(ie._og_search_title(html), 'Foo')
|
||||||
self.assertEqual(ie._og_search_description(html), 'Some video\'s description ')
|
self.assertEqual(ie._og_search_description(html), 'Some video\'s description ')
|
||||||
self.assertEqual(ie._og_search_thumbnail(html), 'http://domain.com/pic.jpg?key1=val1&key2=val2')
|
self.assertEqual(ie._og_search_thumbnail(html), 'http://domain.com/pic.jpg?key1=val1&key2=val2')
|
||||||
|
self.assertEqual(ie._og_search_video_url(html, default=None), None)
|
||||||
|
self.assertEqual(ie._og_search_property('foobar', html), 'Foo')
|
||||||
|
self.assertEqual(ie._og_search_property('test1', html), 'foo > < bar')
|
||||||
|
self.assertEqual(ie._og_search_property('test2', html), 'foo >//< bar')
|
||||||
|
|
||||||
def test_html_search_meta(self):
|
def test_html_search_meta(self):
|
||||||
ie = self.ie
|
ie = self.ie
|
||||||
|
|||||||
@@ -12,8 +12,13 @@ import copy
|
|||||||
|
|
||||||
from test.helper import FakeYDL, assertRegexpMatches
|
from test.helper import FakeYDL, assertRegexpMatches
|
||||||
from youtube_dl import YoutubeDL
|
from youtube_dl import YoutubeDL
|
||||||
|
from youtube_dl.compat import compat_str, compat_urllib_error
|
||||||
from youtube_dl.extractor import YoutubeIE
|
from youtube_dl.extractor import YoutubeIE
|
||||||
|
from youtube_dl.extractor.common import InfoExtractor
|
||||||
from youtube_dl.postprocessor.common import PostProcessor
|
from youtube_dl.postprocessor.common import PostProcessor
|
||||||
|
from youtube_dl.utils import ExtractorError, match_filter_func
|
||||||
|
|
||||||
|
TEST_URL = 'http://localhost/sample.mp4'
|
||||||
|
|
||||||
|
|
||||||
class YDL(FakeYDL):
|
class YDL(FakeYDL):
|
||||||
@@ -46,8 +51,8 @@ class TestFormatSelection(unittest.TestCase):
|
|||||||
ydl = YDL()
|
ydl = YDL()
|
||||||
ydl.params['prefer_free_formats'] = True
|
ydl.params['prefer_free_formats'] = True
|
||||||
formats = [
|
formats = [
|
||||||
{'ext': 'webm', 'height': 460, 'url': 'x'},
|
{'ext': 'webm', 'height': 460, 'url': TEST_URL},
|
||||||
{'ext': 'mp4', 'height': 460, 'url': 'y'},
|
{'ext': 'mp4', 'height': 460, 'url': TEST_URL},
|
||||||
]
|
]
|
||||||
info_dict = _make_result(formats)
|
info_dict = _make_result(formats)
|
||||||
yie = YoutubeIE(ydl)
|
yie = YoutubeIE(ydl)
|
||||||
@@ -60,8 +65,8 @@ class TestFormatSelection(unittest.TestCase):
|
|||||||
ydl = YDL()
|
ydl = YDL()
|
||||||
ydl.params['prefer_free_formats'] = True
|
ydl.params['prefer_free_formats'] = True
|
||||||
formats = [
|
formats = [
|
||||||
{'ext': 'webm', 'height': 720, 'url': 'a'},
|
{'ext': 'webm', 'height': 720, 'url': TEST_URL},
|
||||||
{'ext': 'mp4', 'height': 1080, 'url': 'b'},
|
{'ext': 'mp4', 'height': 1080, 'url': TEST_URL},
|
||||||
]
|
]
|
||||||
info_dict['formats'] = formats
|
info_dict['formats'] = formats
|
||||||
yie = YoutubeIE(ydl)
|
yie = YoutubeIE(ydl)
|
||||||
@@ -74,9 +79,9 @@ class TestFormatSelection(unittest.TestCase):
|
|||||||
ydl = YDL()
|
ydl = YDL()
|
||||||
ydl.params['prefer_free_formats'] = False
|
ydl.params['prefer_free_formats'] = False
|
||||||
formats = [
|
formats = [
|
||||||
{'ext': 'webm', 'height': 720, 'url': '_'},
|
{'ext': 'webm', 'height': 720, 'url': TEST_URL},
|
||||||
{'ext': 'mp4', 'height': 720, 'url': '_'},
|
{'ext': 'mp4', 'height': 720, 'url': TEST_URL},
|
||||||
{'ext': 'flv', 'height': 720, 'url': '_'},
|
{'ext': 'flv', 'height': 720, 'url': TEST_URL},
|
||||||
]
|
]
|
||||||
info_dict['formats'] = formats
|
info_dict['formats'] = formats
|
||||||
yie = YoutubeIE(ydl)
|
yie = YoutubeIE(ydl)
|
||||||
@@ -88,8 +93,8 @@ class TestFormatSelection(unittest.TestCase):
|
|||||||
ydl = YDL()
|
ydl = YDL()
|
||||||
ydl.params['prefer_free_formats'] = False
|
ydl.params['prefer_free_formats'] = False
|
||||||
formats = [
|
formats = [
|
||||||
{'ext': 'flv', 'height': 720, 'url': '_'},
|
{'ext': 'flv', 'height': 720, 'url': TEST_URL},
|
||||||
{'ext': 'webm', 'height': 720, 'url': '_'},
|
{'ext': 'webm', 'height': 720, 'url': TEST_URL},
|
||||||
]
|
]
|
||||||
info_dict['formats'] = formats
|
info_dict['formats'] = formats
|
||||||
yie = YoutubeIE(ydl)
|
yie = YoutubeIE(ydl)
|
||||||
@@ -98,45 +103,13 @@ class TestFormatSelection(unittest.TestCase):
|
|||||||
downloaded = ydl.downloaded_info_dicts[0]
|
downloaded = ydl.downloaded_info_dicts[0]
|
||||||
self.assertEqual(downloaded['ext'], 'flv')
|
self.assertEqual(downloaded['ext'], 'flv')
|
||||||
|
|
||||||
def test_format_limit(self):
|
|
||||||
formats = [
|
|
||||||
{'format_id': 'meh', 'url': 'http://example.com/meh', 'preference': 1},
|
|
||||||
{'format_id': 'good', 'url': 'http://example.com/good', 'preference': 2},
|
|
||||||
{'format_id': 'great', 'url': 'http://example.com/great', 'preference': 3},
|
|
||||||
{'format_id': 'excellent', 'url': 'http://example.com/exc', 'preference': 4},
|
|
||||||
]
|
|
||||||
info_dict = _make_result(formats)
|
|
||||||
|
|
||||||
ydl = YDL()
|
|
||||||
ydl.process_ie_result(info_dict)
|
|
||||||
downloaded = ydl.downloaded_info_dicts[0]
|
|
||||||
self.assertEqual(downloaded['format_id'], 'excellent')
|
|
||||||
|
|
||||||
ydl = YDL({'format_limit': 'good'})
|
|
||||||
assert ydl.params['format_limit'] == 'good'
|
|
||||||
ydl.process_ie_result(info_dict.copy())
|
|
||||||
downloaded = ydl.downloaded_info_dicts[0]
|
|
||||||
self.assertEqual(downloaded['format_id'], 'good')
|
|
||||||
|
|
||||||
ydl = YDL({'format_limit': 'great', 'format': 'all'})
|
|
||||||
ydl.process_ie_result(info_dict.copy())
|
|
||||||
self.assertEqual(ydl.downloaded_info_dicts[0]['format_id'], 'meh')
|
|
||||||
self.assertEqual(ydl.downloaded_info_dicts[1]['format_id'], 'good')
|
|
||||||
self.assertEqual(ydl.downloaded_info_dicts[2]['format_id'], 'great')
|
|
||||||
self.assertTrue('3' in ydl.msgs[0])
|
|
||||||
|
|
||||||
ydl = YDL()
|
|
||||||
ydl.params['format_limit'] = 'excellent'
|
|
||||||
ydl.process_ie_result(info_dict.copy())
|
|
||||||
downloaded = ydl.downloaded_info_dicts[0]
|
|
||||||
self.assertEqual(downloaded['format_id'], 'excellent')
|
|
||||||
|
|
||||||
def test_format_selection(self):
|
def test_format_selection(self):
|
||||||
formats = [
|
formats = [
|
||||||
{'format_id': '35', 'ext': 'mp4', 'preference': 1, 'url': '_'},
|
{'format_id': '35', 'ext': 'mp4', 'preference': 1, 'url': TEST_URL},
|
||||||
{'format_id': '45', 'ext': 'webm', 'preference': 2, 'url': '_'},
|
{'format_id': 'example-with-dashes', 'ext': 'webm', 'preference': 1, 'url': TEST_URL},
|
||||||
{'format_id': '47', 'ext': 'webm', 'preference': 3, 'url': '_'},
|
{'format_id': '45', 'ext': 'webm', 'preference': 2, 'url': TEST_URL},
|
||||||
{'format_id': '2', 'ext': 'flv', 'preference': 4, 'url': '_'},
|
{'format_id': '47', 'ext': 'webm', 'preference': 3, 'url': TEST_URL},
|
||||||
|
{'format_id': '2', 'ext': 'flv', 'preference': 4, 'url': TEST_URL},
|
||||||
]
|
]
|
||||||
info_dict = _make_result(formats)
|
info_dict = _make_result(formats)
|
||||||
|
|
||||||
@@ -165,12 +138,17 @@ class TestFormatSelection(unittest.TestCase):
|
|||||||
downloaded = ydl.downloaded_info_dicts[0]
|
downloaded = ydl.downloaded_info_dicts[0]
|
||||||
self.assertEqual(downloaded['format_id'], '35')
|
self.assertEqual(downloaded['format_id'], '35')
|
||||||
|
|
||||||
|
ydl = YDL({'format': 'example-with-dashes'})
|
||||||
|
ydl.process_ie_result(info_dict.copy())
|
||||||
|
downloaded = ydl.downloaded_info_dicts[0]
|
||||||
|
self.assertEqual(downloaded['format_id'], 'example-with-dashes')
|
||||||
|
|
||||||
def test_format_selection_audio(self):
|
def test_format_selection_audio(self):
|
||||||
formats = [
|
formats = [
|
||||||
{'format_id': 'audio-low', 'ext': 'webm', 'preference': 1, 'vcodec': 'none', 'url': '_'},
|
{'format_id': 'audio-low', 'ext': 'webm', 'preference': 1, 'vcodec': 'none', 'url': TEST_URL},
|
||||||
{'format_id': 'audio-mid', 'ext': 'webm', 'preference': 2, 'vcodec': 'none', 'url': '_'},
|
{'format_id': 'audio-mid', 'ext': 'webm', 'preference': 2, 'vcodec': 'none', 'url': TEST_URL},
|
||||||
{'format_id': 'audio-high', 'ext': 'flv', 'preference': 3, 'vcodec': 'none', 'url': '_'},
|
{'format_id': 'audio-high', 'ext': 'flv', 'preference': 3, 'vcodec': 'none', 'url': TEST_URL},
|
||||||
{'format_id': 'vid', 'ext': 'mp4', 'preference': 4, 'url': '_'},
|
{'format_id': 'vid', 'ext': 'mp4', 'preference': 4, 'url': TEST_URL},
|
||||||
]
|
]
|
||||||
info_dict = _make_result(formats)
|
info_dict = _make_result(formats)
|
||||||
|
|
||||||
@@ -185,8 +163,8 @@ class TestFormatSelection(unittest.TestCase):
|
|||||||
self.assertEqual(downloaded['format_id'], 'audio-low')
|
self.assertEqual(downloaded['format_id'], 'audio-low')
|
||||||
|
|
||||||
formats = [
|
formats = [
|
||||||
{'format_id': 'vid-low', 'ext': 'mp4', 'preference': 1, 'url': '_'},
|
{'format_id': 'vid-low', 'ext': 'mp4', 'preference': 1, 'url': TEST_URL},
|
||||||
{'format_id': 'vid-high', 'ext': 'mp4', 'preference': 2, 'url': '_'},
|
{'format_id': 'vid-high', 'ext': 'mp4', 'preference': 2, 'url': TEST_URL},
|
||||||
]
|
]
|
||||||
info_dict = _make_result(formats)
|
info_dict = _make_result(formats)
|
||||||
|
|
||||||
@@ -228,9 +206,9 @@ class TestFormatSelection(unittest.TestCase):
|
|||||||
|
|
||||||
def test_format_selection_video(self):
|
def test_format_selection_video(self):
|
||||||
formats = [
|
formats = [
|
||||||
{'format_id': 'dash-video-low', 'ext': 'mp4', 'preference': 1, 'acodec': 'none', 'url': '_'},
|
{'format_id': 'dash-video-low', 'ext': 'mp4', 'preference': 1, 'acodec': 'none', 'url': TEST_URL},
|
||||||
{'format_id': 'dash-video-high', 'ext': 'mp4', 'preference': 2, 'acodec': 'none', 'url': '_'},
|
{'format_id': 'dash-video-high', 'ext': 'mp4', 'preference': 2, 'acodec': 'none', 'url': TEST_URL},
|
||||||
{'format_id': 'vid', 'ext': 'mp4', 'preference': 3, 'url': '_'},
|
{'format_id': 'vid', 'ext': 'mp4', 'preference': 3, 'url': TEST_URL},
|
||||||
]
|
]
|
||||||
info_dict = _make_result(formats)
|
info_dict = _make_result(formats)
|
||||||
|
|
||||||
@@ -244,6 +222,16 @@ class TestFormatSelection(unittest.TestCase):
|
|||||||
downloaded = ydl.downloaded_info_dicts[0]
|
downloaded = ydl.downloaded_info_dicts[0]
|
||||||
self.assertEqual(downloaded['format_id'], 'dash-video-low')
|
self.assertEqual(downloaded['format_id'], 'dash-video-low')
|
||||||
|
|
||||||
|
formats = [
|
||||||
|
{'format_id': 'vid-vcodec-dot', 'ext': 'mp4', 'preference': 1, 'vcodec': 'avc1.123456', 'acodec': 'none', 'url': TEST_URL},
|
||||||
|
]
|
||||||
|
info_dict = _make_result(formats)
|
||||||
|
|
||||||
|
ydl = YDL({'format': 'bestvideo[vcodec=avc1.123456]'})
|
||||||
|
ydl.process_ie_result(info_dict.copy())
|
||||||
|
downloaded = ydl.downloaded_info_dicts[0]
|
||||||
|
self.assertEqual(downloaded['format_id'], 'vid-vcodec-dot')
|
||||||
|
|
||||||
def test_youtube_format_selection(self):
|
def test_youtube_format_selection(self):
|
||||||
order = [
|
order = [
|
||||||
'38', '37', '46', '22', '45', '35', '44', '18', '34', '43', '6', '5', '36', '17', '13',
|
'38', '37', '46', '22', '45', '35', '44', '18', '34', '43', '6', '5', '36', '17', '13',
|
||||||
@@ -258,29 +246,100 @@ class TestFormatSelection(unittest.TestCase):
|
|||||||
'141', '172', '140', '171', '139',
|
'141', '172', '140', '171', '139',
|
||||||
]
|
]
|
||||||
|
|
||||||
for f1id, f2id in zip(order, order[1:]):
|
def format_info(f_id):
|
||||||
f1 = YoutubeIE._formats[f1id].copy()
|
info = YoutubeIE._formats[f_id].copy()
|
||||||
f1['format_id'] = f1id
|
|
||||||
f1['url'] = 'url:' + f1id
|
|
||||||
f2 = YoutubeIE._formats[f2id].copy()
|
|
||||||
f2['format_id'] = f2id
|
|
||||||
f2['url'] = 'url:' + f2id
|
|
||||||
|
|
||||||
|
# XXX: In real cases InfoExtractor._parse_mpd_formats() fills up 'acodec'
|
||||||
|
# and 'vcodec', while in tests such information is incomplete since
|
||||||
|
# commit a6c2c24479e5f4827ceb06f64d855329c0a6f593
|
||||||
|
# test_YoutubeDL.test_youtube_format_selection is broken without
|
||||||
|
# this fix
|
||||||
|
if 'acodec' in info and 'vcodec' not in info:
|
||||||
|
info['vcodec'] = 'none'
|
||||||
|
elif 'vcodec' in info and 'acodec' not in info:
|
||||||
|
info['acodec'] = 'none'
|
||||||
|
|
||||||
|
info['format_id'] = f_id
|
||||||
|
info['url'] = 'url:' + f_id
|
||||||
|
return info
|
||||||
|
formats_order = [format_info(f_id) for f_id in order]
|
||||||
|
|
||||||
|
info_dict = _make_result(list(formats_order), extractor='youtube')
|
||||||
|
ydl = YDL({'format': 'bestvideo+bestaudio'})
|
||||||
|
yie = YoutubeIE(ydl)
|
||||||
|
yie._sort_formats(info_dict['formats'])
|
||||||
|
ydl.process_ie_result(info_dict)
|
||||||
|
downloaded = ydl.downloaded_info_dicts[0]
|
||||||
|
self.assertEqual(downloaded['format_id'], '137+141')
|
||||||
|
self.assertEqual(downloaded['ext'], 'mp4')
|
||||||
|
|
||||||
|
info_dict = _make_result(list(formats_order), extractor='youtube')
|
||||||
|
ydl = YDL({'format': 'bestvideo[height>=999999]+bestaudio/best'})
|
||||||
|
yie = YoutubeIE(ydl)
|
||||||
|
yie._sort_formats(info_dict['formats'])
|
||||||
|
ydl.process_ie_result(info_dict)
|
||||||
|
downloaded = ydl.downloaded_info_dicts[0]
|
||||||
|
self.assertEqual(downloaded['format_id'], '38')
|
||||||
|
|
||||||
|
info_dict = _make_result(list(formats_order), extractor='youtube')
|
||||||
|
ydl = YDL({'format': 'bestvideo/best,bestaudio'})
|
||||||
|
yie = YoutubeIE(ydl)
|
||||||
|
yie._sort_formats(info_dict['formats'])
|
||||||
|
ydl.process_ie_result(info_dict)
|
||||||
|
downloaded_ids = [info['format_id'] for info in ydl.downloaded_info_dicts]
|
||||||
|
self.assertEqual(downloaded_ids, ['137', '141'])
|
||||||
|
|
||||||
|
info_dict = _make_result(list(formats_order), extractor='youtube')
|
||||||
|
ydl = YDL({'format': '(bestvideo[ext=mp4],bestvideo[ext=webm])+bestaudio'})
|
||||||
|
yie = YoutubeIE(ydl)
|
||||||
|
yie._sort_formats(info_dict['formats'])
|
||||||
|
ydl.process_ie_result(info_dict)
|
||||||
|
downloaded_ids = [info['format_id'] for info in ydl.downloaded_info_dicts]
|
||||||
|
self.assertEqual(downloaded_ids, ['137+141', '248+141'])
|
||||||
|
|
||||||
|
info_dict = _make_result(list(formats_order), extractor='youtube')
|
||||||
|
ydl = YDL({'format': '(bestvideo[ext=mp4],bestvideo[ext=webm])[height<=720]+bestaudio'})
|
||||||
|
yie = YoutubeIE(ydl)
|
||||||
|
yie._sort_formats(info_dict['formats'])
|
||||||
|
ydl.process_ie_result(info_dict)
|
||||||
|
downloaded_ids = [info['format_id'] for info in ydl.downloaded_info_dicts]
|
||||||
|
self.assertEqual(downloaded_ids, ['136+141', '247+141'])
|
||||||
|
|
||||||
|
info_dict = _make_result(list(formats_order), extractor='youtube')
|
||||||
|
ydl = YDL({'format': '(bestvideo[ext=none]/bestvideo[ext=webm])+bestaudio'})
|
||||||
|
yie = YoutubeIE(ydl)
|
||||||
|
yie._sort_formats(info_dict['formats'])
|
||||||
|
ydl.process_ie_result(info_dict)
|
||||||
|
downloaded_ids = [info['format_id'] for info in ydl.downloaded_info_dicts]
|
||||||
|
self.assertEqual(downloaded_ids, ['248+141'])
|
||||||
|
|
||||||
|
for f1, f2 in zip(formats_order, formats_order[1:]):
|
||||||
info_dict = _make_result([f1, f2], extractor='youtube')
|
info_dict = _make_result([f1, f2], extractor='youtube')
|
||||||
ydl = YDL()
|
ydl = YDL({'format': 'best/bestvideo'})
|
||||||
yie = YoutubeIE(ydl)
|
yie = YoutubeIE(ydl)
|
||||||
yie._sort_formats(info_dict['formats'])
|
yie._sort_formats(info_dict['formats'])
|
||||||
ydl.process_ie_result(info_dict)
|
ydl.process_ie_result(info_dict)
|
||||||
downloaded = ydl.downloaded_info_dicts[0]
|
downloaded = ydl.downloaded_info_dicts[0]
|
||||||
self.assertEqual(downloaded['format_id'], f1id)
|
self.assertEqual(downloaded['format_id'], f1['format_id'])
|
||||||
|
|
||||||
info_dict = _make_result([f2, f1], extractor='youtube')
|
info_dict = _make_result([f2, f1], extractor='youtube')
|
||||||
ydl = YDL()
|
ydl = YDL({'format': 'best/bestvideo'})
|
||||||
yie = YoutubeIE(ydl)
|
yie = YoutubeIE(ydl)
|
||||||
yie._sort_formats(info_dict['formats'])
|
yie._sort_formats(info_dict['formats'])
|
||||||
ydl.process_ie_result(info_dict)
|
ydl.process_ie_result(info_dict)
|
||||||
downloaded = ydl.downloaded_info_dicts[0]
|
downloaded = ydl.downloaded_info_dicts[0]
|
||||||
self.assertEqual(downloaded['format_id'], f1id)
|
self.assertEqual(downloaded['format_id'], f1['format_id'])
|
||||||
|
|
||||||
|
def test_invalid_format_specs(self):
|
||||||
|
def assert_syntax_error(format_spec):
|
||||||
|
ydl = YDL({'format': format_spec})
|
||||||
|
info_dict = _make_result([{'format_id': 'foo', 'url': TEST_URL}])
|
||||||
|
self.assertRaises(SyntaxError, ydl.process_ie_result, info_dict)
|
||||||
|
|
||||||
|
assert_syntax_error('bestvideo,,best')
|
||||||
|
assert_syntax_error('+bestaudio')
|
||||||
|
assert_syntax_error('bestvideo+')
|
||||||
|
assert_syntax_error('/')
|
||||||
|
|
||||||
def test_format_filtering(self):
|
def test_format_filtering(self):
|
||||||
formats = [
|
formats = [
|
||||||
@@ -337,6 +396,79 @@ class TestFormatSelection(unittest.TestCase):
|
|||||||
downloaded = ydl.downloaded_info_dicts[0]
|
downloaded = ydl.downloaded_info_dicts[0]
|
||||||
self.assertEqual(downloaded['format_id'], 'G')
|
self.assertEqual(downloaded['format_id'], 'G')
|
||||||
|
|
||||||
|
ydl = YDL({'format': 'all[width>=400][width<=600]'})
|
||||||
|
ydl.process_ie_result(info_dict)
|
||||||
|
downloaded_ids = [info['format_id'] for info in ydl.downloaded_info_dicts]
|
||||||
|
self.assertEqual(downloaded_ids, ['B', 'C', 'D'])
|
||||||
|
|
||||||
|
ydl = YDL({'format': 'best[height<40]'})
|
||||||
|
try:
|
||||||
|
ydl.process_ie_result(info_dict)
|
||||||
|
except ExtractorError:
|
||||||
|
pass
|
||||||
|
self.assertEqual(ydl.downloaded_info_dicts, [])
|
||||||
|
|
||||||
|
|
||||||
|
class TestYoutubeDL(unittest.TestCase):
|
||||||
|
def test_subtitles(self):
|
||||||
|
def s_formats(lang, autocaption=False):
|
||||||
|
return [{
|
||||||
|
'ext': ext,
|
||||||
|
'url': 'http://localhost/video.%s.%s' % (lang, ext),
|
||||||
|
'_auto': autocaption,
|
||||||
|
} for ext in ['vtt', 'srt', 'ass']]
|
||||||
|
subtitles = dict((l, s_formats(l)) for l in ['en', 'fr', 'es'])
|
||||||
|
auto_captions = dict((l, s_formats(l, True)) for l in ['it', 'pt', 'es'])
|
||||||
|
info_dict = {
|
||||||
|
'id': 'test',
|
||||||
|
'title': 'Test',
|
||||||
|
'url': 'http://localhost/video.mp4',
|
||||||
|
'subtitles': subtitles,
|
||||||
|
'automatic_captions': auto_captions,
|
||||||
|
'extractor': 'TEST',
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_info(params={}):
|
||||||
|
params.setdefault('simulate', True)
|
||||||
|
ydl = YDL(params)
|
||||||
|
ydl.report_warning = lambda *args, **kargs: None
|
||||||
|
return ydl.process_video_result(info_dict, download=False)
|
||||||
|
|
||||||
|
result = get_info()
|
||||||
|
self.assertFalse(result.get('requested_subtitles'))
|
||||||
|
self.assertEqual(result['subtitles'], subtitles)
|
||||||
|
self.assertEqual(result['automatic_captions'], auto_captions)
|
||||||
|
|
||||||
|
result = get_info({'writesubtitles': True})
|
||||||
|
subs = result['requested_subtitles']
|
||||||
|
self.assertTrue(subs)
|
||||||
|
self.assertEqual(set(subs.keys()), set(['en']))
|
||||||
|
self.assertTrue(subs['en'].get('data') is None)
|
||||||
|
self.assertEqual(subs['en']['ext'], 'ass')
|
||||||
|
|
||||||
|
result = get_info({'writesubtitles': True, 'subtitlesformat': 'foo/srt'})
|
||||||
|
subs = result['requested_subtitles']
|
||||||
|
self.assertEqual(subs['en']['ext'], 'srt')
|
||||||
|
|
||||||
|
result = get_info({'writesubtitles': True, 'subtitleslangs': ['es', 'fr', 'it']})
|
||||||
|
subs = result['requested_subtitles']
|
||||||
|
self.assertTrue(subs)
|
||||||
|
self.assertEqual(set(subs.keys()), set(['es', 'fr']))
|
||||||
|
|
||||||
|
result = get_info({'writesubtitles': True, 'writeautomaticsub': True, 'subtitleslangs': ['es', 'pt']})
|
||||||
|
subs = result['requested_subtitles']
|
||||||
|
self.assertTrue(subs)
|
||||||
|
self.assertEqual(set(subs.keys()), set(['es', 'pt']))
|
||||||
|
self.assertFalse(subs['es']['_auto'])
|
||||||
|
self.assertTrue(subs['pt']['_auto'])
|
||||||
|
|
||||||
|
result = get_info({'writeautomaticsub': True, 'subtitleslangs': ['es', 'pt']})
|
||||||
|
subs = result['requested_subtitles']
|
||||||
|
self.assertTrue(subs)
|
||||||
|
self.assertEqual(set(subs.keys()), set(['es', 'pt']))
|
||||||
|
self.assertTrue(subs['es']['_auto'])
|
||||||
|
self.assertTrue(subs['pt']['_auto'])
|
||||||
|
|
||||||
def test_add_extra_info(self):
|
def test_add_extra_info(self):
|
||||||
test_dict = {
|
test_dict = {
|
||||||
'extractor': 'Foo',
|
'extractor': 'Foo',
|
||||||
@@ -379,27 +511,189 @@ class TestFormatSelection(unittest.TestCase):
|
|||||||
def run(self, info):
|
def run(self, info):
|
||||||
with open(audiofile, 'wt') as f:
|
with open(audiofile, 'wt') as f:
|
||||||
f.write('EXAMPLE')
|
f.write('EXAMPLE')
|
||||||
info['filepath']
|
return [info['filepath']], info
|
||||||
return False, info
|
|
||||||
|
|
||||||
def run_pp(params):
|
def run_pp(params, PP):
|
||||||
with open(filename, 'wt') as f:
|
with open(filename, 'wt') as f:
|
||||||
f.write('EXAMPLE')
|
f.write('EXAMPLE')
|
||||||
ydl = YoutubeDL(params)
|
ydl = YoutubeDL(params)
|
||||||
ydl.add_post_processor(SimplePP())
|
ydl.add_post_processor(PP())
|
||||||
ydl.post_process(filename, {'filepath': filename})
|
ydl.post_process(filename, {'filepath': filename})
|
||||||
|
|
||||||
run_pp({'keepvideo': True})
|
run_pp({'keepvideo': True}, SimplePP)
|
||||||
self.assertTrue(os.path.exists(filename), '%s doesn\'t exist' % filename)
|
self.assertTrue(os.path.exists(filename), '%s doesn\'t exist' % filename)
|
||||||
self.assertTrue(os.path.exists(audiofile), '%s doesn\'t exist' % audiofile)
|
self.assertTrue(os.path.exists(audiofile), '%s doesn\'t exist' % audiofile)
|
||||||
os.unlink(filename)
|
os.unlink(filename)
|
||||||
os.unlink(audiofile)
|
os.unlink(audiofile)
|
||||||
|
|
||||||
run_pp({'keepvideo': False})
|
run_pp({'keepvideo': False}, SimplePP)
|
||||||
self.assertFalse(os.path.exists(filename), '%s exists' % filename)
|
self.assertFalse(os.path.exists(filename), '%s exists' % filename)
|
||||||
self.assertTrue(os.path.exists(audiofile), '%s doesn\'t exist' % audiofile)
|
self.assertTrue(os.path.exists(audiofile), '%s doesn\'t exist' % audiofile)
|
||||||
os.unlink(audiofile)
|
os.unlink(audiofile)
|
||||||
|
|
||||||
|
class ModifierPP(PostProcessor):
|
||||||
|
def run(self, info):
|
||||||
|
with open(info['filepath'], 'wt') as f:
|
||||||
|
f.write('MODIFIED')
|
||||||
|
return [], info
|
||||||
|
|
||||||
|
run_pp({'keepvideo': False}, ModifierPP)
|
||||||
|
self.assertTrue(os.path.exists(filename), '%s doesn\'t exist' % filename)
|
||||||
|
os.unlink(filename)
|
||||||
|
|
||||||
|
def test_match_filter(self):
|
||||||
|
class FilterYDL(YDL):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(FilterYDL, self).__init__(*args, **kwargs)
|
||||||
|
self.params['simulate'] = True
|
||||||
|
|
||||||
|
def process_info(self, info_dict):
|
||||||
|
super(YDL, self).process_info(info_dict)
|
||||||
|
|
||||||
|
def _match_entry(self, info_dict, incomplete):
|
||||||
|
res = super(FilterYDL, self)._match_entry(info_dict, incomplete)
|
||||||
|
if res is None:
|
||||||
|
self.downloaded_info_dicts.append(info_dict)
|
||||||
|
return res
|
||||||
|
|
||||||
|
first = {
|
||||||
|
'id': '1',
|
||||||
|
'url': TEST_URL,
|
||||||
|
'title': 'one',
|
||||||
|
'extractor': 'TEST',
|
||||||
|
'duration': 30,
|
||||||
|
'filesize': 10 * 1024,
|
||||||
|
}
|
||||||
|
second = {
|
||||||
|
'id': '2',
|
||||||
|
'url': TEST_URL,
|
||||||
|
'title': 'two',
|
||||||
|
'extractor': 'TEST',
|
||||||
|
'duration': 10,
|
||||||
|
'description': 'foo',
|
||||||
|
'filesize': 5 * 1024,
|
||||||
|
}
|
||||||
|
videos = [first, second]
|
||||||
|
|
||||||
|
def get_videos(filter_=None):
|
||||||
|
ydl = FilterYDL({'match_filter': filter_})
|
||||||
|
for v in videos:
|
||||||
|
ydl.process_ie_result(v, download=True)
|
||||||
|
return [v['id'] for v in ydl.downloaded_info_dicts]
|
||||||
|
|
||||||
|
res = get_videos()
|
||||||
|
self.assertEqual(res, ['1', '2'])
|
||||||
|
|
||||||
|
def f(v):
|
||||||
|
if v['id'] == '1':
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return 'Video id is not 1'
|
||||||
|
res = get_videos(f)
|
||||||
|
self.assertEqual(res, ['1'])
|
||||||
|
|
||||||
|
f = match_filter_func('duration < 30')
|
||||||
|
res = get_videos(f)
|
||||||
|
self.assertEqual(res, ['2'])
|
||||||
|
|
||||||
|
f = match_filter_func('description = foo')
|
||||||
|
res = get_videos(f)
|
||||||
|
self.assertEqual(res, ['2'])
|
||||||
|
|
||||||
|
f = match_filter_func('description =? foo')
|
||||||
|
res = get_videos(f)
|
||||||
|
self.assertEqual(res, ['1', '2'])
|
||||||
|
|
||||||
|
f = match_filter_func('filesize > 5KiB')
|
||||||
|
res = get_videos(f)
|
||||||
|
self.assertEqual(res, ['1'])
|
||||||
|
|
||||||
|
def test_playlist_items_selection(self):
|
||||||
|
entries = [{
|
||||||
|
'id': compat_str(i),
|
||||||
|
'title': compat_str(i),
|
||||||
|
'url': TEST_URL,
|
||||||
|
} for i in range(1, 5)]
|
||||||
|
playlist = {
|
||||||
|
'_type': 'playlist',
|
||||||
|
'id': 'test',
|
||||||
|
'entries': entries,
|
||||||
|
'extractor': 'test:playlist',
|
||||||
|
'extractor_key': 'test:playlist',
|
||||||
|
'webpage_url': 'http://example.com',
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_ids(params):
|
||||||
|
ydl = YDL(params)
|
||||||
|
# make a copy because the dictionary can be modified
|
||||||
|
ydl.process_ie_result(playlist.copy())
|
||||||
|
return [int(v['id']) for v in ydl.downloaded_info_dicts]
|
||||||
|
|
||||||
|
result = get_ids({})
|
||||||
|
self.assertEqual(result, [1, 2, 3, 4])
|
||||||
|
|
||||||
|
result = get_ids({'playlistend': 10})
|
||||||
|
self.assertEqual(result, [1, 2, 3, 4])
|
||||||
|
|
||||||
|
result = get_ids({'playlistend': 2})
|
||||||
|
self.assertEqual(result, [1, 2])
|
||||||
|
|
||||||
|
result = get_ids({'playliststart': 10})
|
||||||
|
self.assertEqual(result, [])
|
||||||
|
|
||||||
|
result = get_ids({'playliststart': 2})
|
||||||
|
self.assertEqual(result, [2, 3, 4])
|
||||||
|
|
||||||
|
result = get_ids({'playlist_items': '2-4'})
|
||||||
|
self.assertEqual(result, [2, 3, 4])
|
||||||
|
|
||||||
|
result = get_ids({'playlist_items': '2,4'})
|
||||||
|
self.assertEqual(result, [2, 4])
|
||||||
|
|
||||||
|
result = get_ids({'playlist_items': '10'})
|
||||||
|
self.assertEqual(result, [])
|
||||||
|
|
||||||
|
def test_urlopen_no_file_protocol(self):
|
||||||
|
# see https://github.com/rg3/youtube-dl/issues/8227
|
||||||
|
ydl = YDL()
|
||||||
|
self.assertRaises(compat_urllib_error.URLError, ydl.urlopen, 'file:///etc/passwd')
|
||||||
|
|
||||||
|
def test_do_not_override_ie_key_in_url_transparent(self):
|
||||||
|
ydl = YDL()
|
||||||
|
|
||||||
|
class Foo1IE(InfoExtractor):
|
||||||
|
_VALID_URL = r'foo1:'
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
return {
|
||||||
|
'_type': 'url_transparent',
|
||||||
|
'url': 'foo2:',
|
||||||
|
'ie_key': 'Foo2',
|
||||||
|
}
|
||||||
|
|
||||||
|
class Foo2IE(InfoExtractor):
|
||||||
|
_VALID_URL = r'foo2:'
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
return {
|
||||||
|
'_type': 'url',
|
||||||
|
'url': 'foo3:',
|
||||||
|
'ie_key': 'Foo3',
|
||||||
|
}
|
||||||
|
|
||||||
|
class Foo3IE(InfoExtractor):
|
||||||
|
_VALID_URL = r'foo3:'
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
return _make_result([{'url': TEST_URL}])
|
||||||
|
|
||||||
|
ydl.add_info_extractor(Foo1IE(ydl))
|
||||||
|
ydl.add_info_extractor(Foo2IE(ydl))
|
||||||
|
ydl.add_info_extractor(Foo3IE(ydl))
|
||||||
|
ydl.extract_info('foo1:')
|
||||||
|
downloaded = ydl.downloaded_info_dicts[0]
|
||||||
|
self.assertEqual(downloaded['url'], TEST_URL)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
55
test/test_aes.py
Normal file
55
test/test_aes.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/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.aes import aes_decrypt, aes_encrypt, aes_cbc_decrypt, aes_decrypt_text
|
||||||
|
from youtube_dl.utils import bytes_to_intlist, intlist_to_bytes
|
||||||
|
import base64
|
||||||
|
|
||||||
|
# the encrypted data can be generate with 'devscripts/generate_aes_testdata.py'
|
||||||
|
|
||||||
|
|
||||||
|
class TestAES(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.key = self.iv = [0x20, 0x15] + 14 * [0]
|
||||||
|
self.secret_msg = b'Secret message goes here'
|
||||||
|
|
||||||
|
def test_encrypt(self):
|
||||||
|
msg = b'message'
|
||||||
|
key = list(range(16))
|
||||||
|
encrypted = aes_encrypt(bytes_to_intlist(msg), key)
|
||||||
|
decrypted = intlist_to_bytes(aes_decrypt(encrypted, key))
|
||||||
|
self.assertEqual(decrypted, msg)
|
||||||
|
|
||||||
|
def test_cbc_decrypt(self):
|
||||||
|
data = bytes_to_intlist(
|
||||||
|
b"\x97\x92+\xe5\x0b\xc3\x18\x91ky9m&\xb3\xb5@\xe6'\xc2\x96.\xc8u\x88\xab9-[\x9e|\xf1\xcd"
|
||||||
|
)
|
||||||
|
decrypted = intlist_to_bytes(aes_cbc_decrypt(data, self.key, self.iv))
|
||||||
|
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg)
|
||||||
|
|
||||||
|
def test_decrypt_text(self):
|
||||||
|
password = intlist_to_bytes(self.key).decode('utf-8')
|
||||||
|
encrypted = base64.b64encode(
|
||||||
|
intlist_to_bytes(self.iv[:8]) +
|
||||||
|
b'\x17\x15\x93\xab\x8d\x80V\xcdV\xe0\t\xcdo\xc2\xa5\xd8ksM\r\xe27N\xae'
|
||||||
|
).decode('utf-8')
|
||||||
|
decrypted = (aes_decrypt_text(encrypted, password, 16))
|
||||||
|
self.assertEqual(decrypted, self.secret_msg)
|
||||||
|
|
||||||
|
password = intlist_to_bytes(self.key).decode('utf-8')
|
||||||
|
encrypted = base64.b64encode(
|
||||||
|
intlist_to_bytes(self.iv[:8]) +
|
||||||
|
b'\x0b\xe6\xa4\xd9z\x0e\xb8\xb9\xd0\xd4i_\x85\x1d\x99\x98_\xe5\x80\xe7.\xbf\xa5\x83'
|
||||||
|
).decode('utf-8')
|
||||||
|
decrypted = (aes_decrypt_text(encrypted, password, 32))
|
||||||
|
self.assertEqual(decrypted, self.secret_msg)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -56,10 +56,10 @@ class TestAllURLsMatching(unittest.TestCase):
|
|||||||
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('www.youtube.com/NASAgovVideo/videos', ['youtube:user'])
|
self.assertMatch('http://www.youtube.com/NASAgovVideo/videos', ['youtube:user'])
|
||||||
|
|
||||||
def test_youtube_feeds(self):
|
def test_youtube_feeds(self):
|
||||||
self.assertMatch('https://www.youtube.com/feed/watch_later', ['youtube:watch_later'])
|
self.assertMatch('https://www.youtube.com/feed/watch_later', ['youtube:watchlater'])
|
||||||
self.assertMatch('https://www.youtube.com/feed/subscriptions', ['youtube:subscriptions'])
|
self.assertMatch('https://www.youtube.com/feed/subscriptions', ['youtube:subscriptions'])
|
||||||
self.assertMatch('https://www.youtube.com/feed/recommended', ['youtube:recommended'])
|
self.assertMatch('https://www.youtube.com/feed/recommended', ['youtube:recommended'])
|
||||||
self.assertMatch('https://www.youtube.com/my_favorites', ['youtube:favorites'])
|
self.assertMatch('https://www.youtube.com/my_favorites', ['youtube:favorites'])
|
||||||
@@ -104,11 +104,11 @@ class TestAllURLsMatching(unittest.TestCase):
|
|||||||
self.assertMatch(':tds', ['ComedyCentralShows'])
|
self.assertMatch(':tds', ['ComedyCentralShows'])
|
||||||
|
|
||||||
def test_vimeo_matching(self):
|
def test_vimeo_matching(self):
|
||||||
self.assertMatch('http://vimeo.com/channels/tributes', ['vimeo:channel'])
|
self.assertMatch('https://vimeo.com/channels/tributes', ['vimeo:channel'])
|
||||||
self.assertMatch('http://vimeo.com/channels/31259', ['vimeo:channel'])
|
self.assertMatch('https://vimeo.com/channels/31259', ['vimeo:channel'])
|
||||||
self.assertMatch('http://vimeo.com/channels/31259/53576664', ['vimeo'])
|
self.assertMatch('https://vimeo.com/channels/31259/53576664', ['vimeo'])
|
||||||
self.assertMatch('http://vimeo.com/user7108434', ['vimeo:user'])
|
self.assertMatch('https://vimeo.com/user7108434', ['vimeo:user'])
|
||||||
self.assertMatch('http://vimeo.com/user7108434/videos', ['vimeo:user'])
|
self.assertMatch('https://vimeo.com/user7108434/videos', ['vimeo:user'])
|
||||||
self.assertMatch('https://vimeo.com/user21297594/review/75524534/3c257a1b5d', ['vimeo:review'])
|
self.assertMatch('https://vimeo.com/user21297594/review/75524534/3c257a1b5d', ['vimeo:review'])
|
||||||
|
|
||||||
# https://github.com/rg3/youtube-dl/issues/1930
|
# https://github.com/rg3/youtube-dl/issues/1930
|
||||||
@@ -121,8 +121,8 @@ class TestAllURLsMatching(unittest.TestCase):
|
|||||||
|
|
||||||
def test_pbs(self):
|
def test_pbs(self):
|
||||||
# https://github.com/rg3/youtube-dl/issues/2350
|
# https://github.com/rg3/youtube-dl/issues/2350
|
||||||
self.assertMatch('http://video.pbs.org/viralplayer/2365173446/', ['PBS'])
|
self.assertMatch('http://video.pbs.org/viralplayer/2365173446/', ['pbs'])
|
||||||
self.assertMatch('http://video.pbs.org/widget/partnerplayer/980042464/', ['PBS'])
|
self.assertMatch('http://video.pbs.org/widget/partnerplayer/980042464/', ['pbs'])
|
||||||
|
|
||||||
def test_yahoo_https(self):
|
def test_yahoo_https(self):
|
||||||
# https://github.com/rg3/youtube-dl/issues/2701
|
# https://github.com/rg3/youtube-dl/issues/2701
|
||||||
|
|||||||
@@ -13,7 +13,12 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|||||||
from youtube_dl.utils import get_filesystem_encoding
|
from youtube_dl.utils import get_filesystem_encoding
|
||||||
from youtube_dl.compat import (
|
from youtube_dl.compat import (
|
||||||
compat_getenv,
|
compat_getenv,
|
||||||
|
compat_etree_fromstring,
|
||||||
compat_expanduser,
|
compat_expanduser,
|
||||||
|
compat_shlex_split,
|
||||||
|
compat_str,
|
||||||
|
compat_urllib_parse_unquote,
|
||||||
|
compat_urllib_parse_unquote_plus,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -42,5 +47,46 @@ class TestCompat(unittest.TestCase):
|
|||||||
dir(youtube_dl.compat))) - set(['unicode_literals'])
|
dir(youtube_dl.compat))) - set(['unicode_literals'])
|
||||||
self.assertEqual(all_names, sorted(present_names))
|
self.assertEqual(all_names, sorted(present_names))
|
||||||
|
|
||||||
|
def test_compat_urllib_parse_unquote(self):
|
||||||
|
self.assertEqual(compat_urllib_parse_unquote('abc%20def'), 'abc def')
|
||||||
|
self.assertEqual(compat_urllib_parse_unquote('%7e/abc+def'), '~/abc+def')
|
||||||
|
self.assertEqual(compat_urllib_parse_unquote(''), '')
|
||||||
|
self.assertEqual(compat_urllib_parse_unquote('%'), '%')
|
||||||
|
self.assertEqual(compat_urllib_parse_unquote('%%'), '%%')
|
||||||
|
self.assertEqual(compat_urllib_parse_unquote('%%%'), '%%%')
|
||||||
|
self.assertEqual(compat_urllib_parse_unquote('%2F'), '/')
|
||||||
|
self.assertEqual(compat_urllib_parse_unquote('%2f'), '/')
|
||||||
|
self.assertEqual(compat_urllib_parse_unquote('%E6%B4%A5%E6%B3%A2'), '津波')
|
||||||
|
self.assertEqual(
|
||||||
|
compat_urllib_parse_unquote('''<meta property="og:description" content="%E2%96%81%E2%96%82%E2%96%83%E2%96%84%25%E2%96%85%E2%96%86%E2%96%87%E2%96%88" />
|
||||||
|
%<a href="https://ar.wikipedia.org/wiki/%D8%AA%D8%B3%D9%88%D9%86%D8%A7%D9%85%D9%8A">%a'''),
|
||||||
|
'''<meta property="og:description" content="▁▂▃▄%▅▆▇█" />
|
||||||
|
%<a href="https://ar.wikipedia.org/wiki/تسونامي">%a''')
|
||||||
|
self.assertEqual(
|
||||||
|
compat_urllib_parse_unquote('''%28%5E%E2%97%A3_%E2%97%A2%5E%29%E3%81%A3%EF%B8%BB%E3%83%87%E2%95%90%E4%B8%80 %E2%87%80 %E2%87%80 %E2%87%80 %E2%87%80 %E2%87%80 %E2%86%B6%I%Break%25Things%'''),
|
||||||
|
'''(^◣_◢^)っ︻デ═一 ⇀ ⇀ ⇀ ⇀ ⇀ ↶%I%Break%Things%''')
|
||||||
|
|
||||||
|
def test_compat_urllib_parse_unquote_plus(self):
|
||||||
|
self.assertEqual(compat_urllib_parse_unquote_plus('abc%20def'), 'abc def')
|
||||||
|
self.assertEqual(compat_urllib_parse_unquote_plus('%7e/abc+def'), '~/abc def')
|
||||||
|
|
||||||
|
def test_compat_shlex_split(self):
|
||||||
|
self.assertEqual(compat_shlex_split('-option "one two"'), ['-option', 'one two'])
|
||||||
|
|
||||||
|
def test_compat_etree_fromstring(self):
|
||||||
|
xml = '''
|
||||||
|
<root foo="bar" spam="中文">
|
||||||
|
<normal>foo</normal>
|
||||||
|
<chinese>中文</chinese>
|
||||||
|
<foo><bar>spam</bar></foo>
|
||||||
|
</root>
|
||||||
|
'''
|
||||||
|
doc = compat_etree_fromstring(xml.encode('utf-8'))
|
||||||
|
self.assertTrue(isinstance(doc.attrib['foo'], compat_str))
|
||||||
|
self.assertTrue(isinstance(doc.attrib['spam'], compat_str))
|
||||||
|
self.assertTrue(isinstance(doc.find('normal').text, compat_str))
|
||||||
|
self.assertTrue(isinstance(doc.find('chinese').text, compat_str))
|
||||||
|
self.assertTrue(isinstance(doc.find('foo/bar').text, compat_str))
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ def generator(test_case):
|
|||||||
|
|
||||||
params = get_params(test_case.get('params', {}))
|
params = get_params(test_case.get('params', {}))
|
||||||
if is_playlist and 'playlist' not in test_case:
|
if is_playlist and 'playlist' not in test_case:
|
||||||
params.setdefault('extract_flat', True)
|
params.setdefault('extract_flat', 'in_playlist')
|
||||||
params.setdefault('skip_download', True)
|
params.setdefault('skip_download', True)
|
||||||
|
|
||||||
ydl = YoutubeDL(params, auto_init=False)
|
ydl = YoutubeDL(params, auto_init=False)
|
||||||
@@ -136,7 +136,9 @@ def generator(test_case):
|
|||||||
# We're not using .download here sine that is just a shim
|
# We're not using .download here sine that is just a shim
|
||||||
# for outside error handling, and returns the exit code
|
# for outside error handling, and returns the exit code
|
||||||
# instead of the result dict.
|
# instead of the result dict.
|
||||||
res_dict = ydl.extract_info(test_case['url'])
|
res_dict = ydl.extract_info(
|
||||||
|
test_case['url'],
|
||||||
|
force_generic_extractor=params.get('force_generic_extractor', False))
|
||||||
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):
|
||||||
@@ -153,7 +155,7 @@ def generator(test_case):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if is_playlist:
|
if is_playlist:
|
||||||
self.assertEqual(res_dict['_type'], 'playlist')
|
self.assertTrue(res_dict['_type'] in ['playlist', 'multi_video'])
|
||||||
self.assertTrue('entries' in res_dict)
|
self.assertTrue('entries' in res_dict)
|
||||||
expect_info_dict(self, res_dict, test_case.get('info_dict', {}))
|
expect_info_dict(self, res_dict, test_case.get('info_dict', {}))
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
# coding: utf-8
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
@@ -6,6 +8,9 @@ import unittest
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from youtube_dl.utils import encodeArgument
|
||||||
|
|
||||||
rootDir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
rootDir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
@@ -27,5 +32,12 @@ class TestExecution(unittest.TestCase):
|
|||||||
def test_main_exec(self):
|
def test_main_exec(self):
|
||||||
subprocess.check_call([sys.executable, 'youtube_dl/__main__.py', '--version'], cwd=rootDir, stdout=_DEV_NULL)
|
subprocess.check_call([sys.executable, 'youtube_dl/__main__.py', '--version'], cwd=rootDir, stdout=_DEV_NULL)
|
||||||
|
|
||||||
|
def test_cmdline_umlauts(self):
|
||||||
|
p = subprocess.Popen(
|
||||||
|
[sys.executable, 'youtube_dl/__main__.py', encodeArgument('ä'), '--version'],
|
||||||
|
cwd=rootDir, stdout=_DEV_NULL, stderr=subprocess.PIPE)
|
||||||
|
_, stderr = p.communicate()
|
||||||
|
self.assertFalse(stderr)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ 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 import YoutubeDL
|
from youtube_dl import YoutubeDL
|
||||||
from youtube_dl.compat import compat_http_server
|
from youtube_dl.compat import compat_http_server, compat_urllib_request
|
||||||
import ssl
|
import ssl
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
@@ -68,5 +68,52 @@ class TestHTTP(unittest.TestCase):
|
|||||||
r = ydl.extract_info('https://localhost:%d/video.html' % self.port)
|
r = ydl.extract_info('https://localhost:%d/video.html' % self.port)
|
||||||
self.assertEqual(r['url'], 'https://localhost:%d/vid.mp4' % self.port)
|
self.assertEqual(r['url'], 'https://localhost:%d/vid.mp4' % self.port)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_proxy_handler(name):
|
||||||
|
class HTTPTestRequestHandler(compat_http_server.BaseHTTPRequestHandler):
|
||||||
|
proxy_name = name
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-Type', 'text/plain; charset=utf-8')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write('{self.proxy_name}: {self.path}'.format(self=self).encode('utf-8'))
|
||||||
|
return HTTPTestRequestHandler
|
||||||
|
|
||||||
|
|
||||||
|
class TestProxy(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.proxy = compat_http_server.HTTPServer(
|
||||||
|
('localhost', 0), _build_proxy_handler('normal'))
|
||||||
|
self.port = self.proxy.socket.getsockname()[1]
|
||||||
|
self.proxy_thread = threading.Thread(target=self.proxy.serve_forever)
|
||||||
|
self.proxy_thread.daemon = True
|
||||||
|
self.proxy_thread.start()
|
||||||
|
|
||||||
|
self.cn_proxy = compat_http_server.HTTPServer(
|
||||||
|
('localhost', 0), _build_proxy_handler('cn'))
|
||||||
|
self.cn_port = self.cn_proxy.socket.getsockname()[1]
|
||||||
|
self.cn_proxy_thread = threading.Thread(target=self.cn_proxy.serve_forever)
|
||||||
|
self.cn_proxy_thread.daemon = True
|
||||||
|
self.cn_proxy_thread.start()
|
||||||
|
|
||||||
|
def test_proxy(self):
|
||||||
|
cn_proxy = 'localhost:{0}'.format(self.cn_port)
|
||||||
|
ydl = YoutubeDL({
|
||||||
|
'proxy': 'localhost:{0}'.format(self.port),
|
||||||
|
'cn_verification_proxy': cn_proxy,
|
||||||
|
})
|
||||||
|
url = 'http://foo.com/bar'
|
||||||
|
response = ydl.urlopen(url).read().decode('utf-8')
|
||||||
|
self.assertEqual(response, 'normal: {0}'.format(url))
|
||||||
|
|
||||||
|
req = compat_urllib_request.Request(url)
|
||||||
|
req.add_header('Ytdl-request-proxy', cn_proxy)
|
||||||
|
response = ydl.urlopen(req).read().decode('utf-8')
|
||||||
|
self.assertEqual(response, 'cn: {0}'.format(url))
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -19,6 +19,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('var x5 = function(){return 42;}')
|
||||||
|
self.assertEqual(jsi.call_function('x5'), 42)
|
||||||
|
|
||||||
def test_calc(self):
|
def test_calc(self):
|
||||||
jsi = JSInterpreter('function x4(a){return 2*a+1;}')
|
jsi = JSInterpreter('function x4(a){return 2*a+1;}')
|
||||||
self.assertEqual(jsi.call_function('x4', 3), 7)
|
self.assertEqual(jsi.call_function('x4', 3), 7)
|
||||||
|
|||||||
26
test/test_netrc.py
Normal file
26
test/test_netrc.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
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 (
|
||||||
|
gen_extractors,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestNetRc(unittest.TestCase):
|
||||||
|
def test_netrc_present(self):
|
||||||
|
for ie in gen_extractors():
|
||||||
|
if not hasattr(ie, '_login'):
|
||||||
|
continue
|
||||||
|
self.assertTrue(
|
||||||
|
hasattr(ie, '_NETRC_MACHINE'),
|
||||||
|
'Extractor %s supports login, but is missing a _NETRC_MACHINE property' % ie.IE_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
17
test/test_postprocessors.py
Normal file
17
test/test_postprocessors.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#!/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.postprocessor import MetadataFromTitlePP
|
||||||
|
|
||||||
|
|
||||||
|
class TestMetadataFromTitle(unittest.TestCase):
|
||||||
|
def test_format_to_regex(self):
|
||||||
|
pp = MetadataFromTitlePP(None, '%(title)s - %(artist)s')
|
||||||
|
self.assertEqual(pp._titleregex, '(?P<title>.+)\ \-\ (?P<artist>.+)')
|
||||||
@@ -11,13 +11,23 @@ from test.helper import FakeYDL, md5
|
|||||||
|
|
||||||
|
|
||||||
from youtube_dl.extractor import (
|
from youtube_dl.extractor import (
|
||||||
BlipTVIE,
|
|
||||||
YoutubeIE,
|
YoutubeIE,
|
||||||
DailymotionIE,
|
DailymotionIE,
|
||||||
TEDIE,
|
TEDIE,
|
||||||
VimeoIE,
|
VimeoIE,
|
||||||
WallaIE,
|
WallaIE,
|
||||||
CeskaTelevizeIE,
|
CeskaTelevizeIE,
|
||||||
|
LyndaIE,
|
||||||
|
NPOIE,
|
||||||
|
ComedyCentralIE,
|
||||||
|
NRKTVIE,
|
||||||
|
RaiTVIE,
|
||||||
|
VikiIE,
|
||||||
|
ThePlatformIE,
|
||||||
|
ThePlatformFeedIE,
|
||||||
|
RTVEALaCartaIE,
|
||||||
|
FunnyOrDieIE,
|
||||||
|
DemocracynowIE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -27,48 +37,44 @@ class BaseTestSubtitles(unittest.TestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.DL = FakeYDL()
|
self.DL = FakeYDL()
|
||||||
self.ie = self.IE(self.DL)
|
self.ie = self.IE()
|
||||||
|
self.DL.add_info_extractor(self.ie)
|
||||||
|
|
||||||
def getInfoDict(self):
|
def getInfoDict(self):
|
||||||
info_dict = self.ie.extract(self.url)
|
info_dict = self.DL.extract_info(self.url, download=False)
|
||||||
return info_dict
|
return info_dict
|
||||||
|
|
||||||
def getSubtitles(self):
|
def getSubtitles(self):
|
||||||
info_dict = self.getInfoDict()
|
info_dict = self.getInfoDict()
|
||||||
return info_dict['subtitles']
|
subtitles = info_dict['requested_subtitles']
|
||||||
|
if not subtitles:
|
||||||
|
return subtitles
|
||||||
|
for sub_info in subtitles.values():
|
||||||
|
if sub_info.get('data') is None:
|
||||||
|
uf = self.DL.urlopen(sub_info['url'])
|
||||||
|
sub_info['data'] = uf.read().decode('utf-8')
|
||||||
|
return dict((l, sub_info['data']) for l, sub_info in subtitles.items())
|
||||||
|
|
||||||
|
|
||||||
class TestYoutubeSubtitles(BaseTestSubtitles):
|
class TestYoutubeSubtitles(BaseTestSubtitles):
|
||||||
url = 'QRS8MkLhQmM'
|
url = 'QRS8MkLhQmM'
|
||||||
IE = YoutubeIE
|
IE = YoutubeIE
|
||||||
|
|
||||||
def test_youtube_no_writesubtitles(self):
|
|
||||||
self.DL.params['writesubtitles'] = False
|
|
||||||
subtitles = self.getSubtitles()
|
|
||||||
self.assertEqual(subtitles, None)
|
|
||||||
|
|
||||||
def test_youtube_subtitles(self):
|
|
||||||
self.DL.params['writesubtitles'] = True
|
|
||||||
subtitles = self.getSubtitles()
|
|
||||||
self.assertEqual(md5(subtitles['en']), '4cd9278a35ba2305f47354ee13472260')
|
|
||||||
|
|
||||||
def test_youtube_subtitles_lang(self):
|
|
||||||
self.DL.params['writesubtitles'] = True
|
|
||||||
self.DL.params['subtitleslangs'] = ['it']
|
|
||||||
subtitles = self.getSubtitles()
|
|
||||||
self.assertEqual(md5(subtitles['it']), '164a51f16f260476a05b50fe4c2f161d')
|
|
||||||
|
|
||||||
def test_youtube_allsubtitles(self):
|
def test_youtube_allsubtitles(self):
|
||||||
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(len(subtitles.keys()), 13)
|
self.assertEqual(len(subtitles.keys()), 13)
|
||||||
|
self.assertEqual(md5(subtitles['en']), '3cb210999d3e021bd6c7f0ea751eab06')
|
||||||
|
self.assertEqual(md5(subtitles['it']), '6d752b98c31f1cf8d597050c7a2cb4b5')
|
||||||
|
for lang in ['fr', 'de']:
|
||||||
|
self.assertTrue(subtitles.get(lang) is not None, 'Subtitles for \'%s\' not extracted' % lang)
|
||||||
|
|
||||||
def test_youtube_subtitles_sbv_format(self):
|
def test_youtube_subtitles_ttml_format(self):
|
||||||
self.DL.params['writesubtitles'] = True
|
self.DL.params['writesubtitles'] = True
|
||||||
self.DL.params['subtitlesformat'] = 'sbv'
|
self.DL.params['subtitlesformat'] = 'ttml'
|
||||||
subtitles = self.getSubtitles()
|
subtitles = self.getSubtitles()
|
||||||
self.assertEqual(md5(subtitles['en']), '13aeaa0c245a8bed9a451cb643e3ad8b')
|
self.assertEqual(md5(subtitles['en']), 'e306f8c42842f723447d9f63ad65df54')
|
||||||
|
|
||||||
def test_youtube_subtitles_vtt_format(self):
|
def test_youtube_subtitles_vtt_format(self):
|
||||||
self.DL.params['writesubtitles'] = True
|
self.DL.params['writesubtitles'] = True
|
||||||
@@ -76,12 +82,6 @@ class TestYoutubeSubtitles(BaseTestSubtitles):
|
|||||||
subtitles = self.getSubtitles()
|
subtitles = self.getSubtitles()
|
||||||
self.assertEqual(md5(subtitles['en']), '3cb210999d3e021bd6c7f0ea751eab06')
|
self.assertEqual(md5(subtitles['en']), '3cb210999d3e021bd6c7f0ea751eab06')
|
||||||
|
|
||||||
def test_youtube_list_subtitles(self):
|
|
||||||
self.DL.expect_warning('Video doesn\'t have automatic captions')
|
|
||||||
self.DL.params['listsubtitles'] = True
|
|
||||||
info_dict = self.getInfoDict()
|
|
||||||
self.assertEqual(info_dict, None)
|
|
||||||
|
|
||||||
def test_youtube_automatic_captions(self):
|
def test_youtube_automatic_captions(self):
|
||||||
self.url = '8YoUxe5ncPo'
|
self.url = '8YoUxe5ncPo'
|
||||||
self.DL.params['writeautomaticsub'] = True
|
self.DL.params['writeautomaticsub'] = True
|
||||||
@@ -103,55 +103,22 @@ class TestYoutubeSubtitles(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(len(subtitles), 0)
|
self.assertFalse(subtitles)
|
||||||
|
|
||||||
def test_youtube_multiple_langs(self):
|
|
||||||
self.url = 'QRS8MkLhQmM'
|
|
||||||
self.DL.params['writesubtitles'] = True
|
|
||||||
langs = ['it', 'fr', 'de']
|
|
||||||
self.DL.params['subtitleslangs'] = langs
|
|
||||||
subtitles = self.getSubtitles()
|
|
||||||
for lang in langs:
|
|
||||||
self.assertTrue(subtitles.get(lang) is not None, 'Subtitles for \'%s\' not extracted' % lang)
|
|
||||||
|
|
||||||
|
|
||||||
class TestDailymotionSubtitles(BaseTestSubtitles):
|
class TestDailymotionSubtitles(BaseTestSubtitles):
|
||||||
url = 'http://www.dailymotion.com/video/xczg00'
|
url = 'http://www.dailymotion.com/video/xczg00'
|
||||||
IE = DailymotionIE
|
IE = DailymotionIE
|
||||||
|
|
||||||
def test_no_writesubtitles(self):
|
|
||||||
subtitles = self.getSubtitles()
|
|
||||||
self.assertEqual(subtitles, None)
|
|
||||||
|
|
||||||
def test_subtitles(self):
|
|
||||||
self.DL.params['writesubtitles'] = True
|
|
||||||
subtitles = self.getSubtitles()
|
|
||||||
self.assertEqual(md5(subtitles['en']), '976553874490cba125086bbfea3ff76f')
|
|
||||||
|
|
||||||
def test_subtitles_lang(self):
|
|
||||||
self.DL.params['writesubtitles'] = True
|
|
||||||
self.DL.params['subtitleslangs'] = ['fr']
|
|
||||||
subtitles = self.getSubtitles()
|
|
||||||
self.assertEqual(md5(subtitles['fr']), '594564ec7d588942e384e920e5341792')
|
|
||||||
|
|
||||||
def test_allsubtitles(self):
|
def test_allsubtitles(self):
|
||||||
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.assertTrue(len(subtitles.keys()) >= 6)
|
self.assertTrue(len(subtitles.keys()) >= 6)
|
||||||
|
self.assertEqual(md5(subtitles['en']), '976553874490cba125086bbfea3ff76f')
|
||||||
def test_list_subtitles(self):
|
self.assertEqual(md5(subtitles['fr']), '594564ec7d588942e384e920e5341792')
|
||||||
self.DL.expect_warning('Automatic Captions not supported by this server')
|
for lang in ['es', 'fr', 'de']:
|
||||||
self.DL.params['listsubtitles'] = True
|
self.assertTrue(subtitles.get(lang) is not None, 'Subtitles for \'%s\' not extracted' % lang)
|
||||||
info_dict = self.getInfoDict()
|
|
||||||
self.assertEqual(info_dict, None)
|
|
||||||
|
|
||||||
def test_automatic_captions(self):
|
|
||||||
self.DL.expect_warning('Automatic Captions not supported by this server')
|
|
||||||
self.DL.params['writeautomaticsub'] = True
|
|
||||||
self.DL.params['subtitleslang'] = ['en']
|
|
||||||
subtitles = self.getSubtitles()
|
|
||||||
self.assertTrue(len(subtitles.keys()) == 0)
|
|
||||||
|
|
||||||
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')
|
||||||
@@ -159,120 +126,35 @@ class TestDailymotionSubtitles(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(len(subtitles), 0)
|
self.assertFalse(subtitles)
|
||||||
|
|
||||||
def test_multiple_langs(self):
|
|
||||||
self.DL.params['writesubtitles'] = True
|
|
||||||
langs = ['es', 'fr', 'de']
|
|
||||||
self.DL.params['subtitleslangs'] = langs
|
|
||||||
subtitles = self.getSubtitles()
|
|
||||||
for lang in langs:
|
|
||||||
self.assertTrue(subtitles.get(lang) is not None, 'Subtitles for \'%s\' not extracted' % lang)
|
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
def test_no_writesubtitles(self):
|
|
||||||
subtitles = self.getSubtitles()
|
|
||||||
self.assertEqual(subtitles, None)
|
|
||||||
|
|
||||||
def test_subtitles(self):
|
|
||||||
self.DL.params['writesubtitles'] = True
|
|
||||||
subtitles = self.getSubtitles()
|
|
||||||
self.assertEqual(md5(subtitles['en']), '4262c1665ff928a2dada178f62cb8d14')
|
|
||||||
|
|
||||||
def test_subtitles_lang(self):
|
|
||||||
self.DL.params['writesubtitles'] = True
|
|
||||||
self.DL.params['subtitleslangs'] = ['fr']
|
|
||||||
subtitles = self.getSubtitles()
|
|
||||||
self.assertEqual(md5(subtitles['fr']), '66a63f7f42c97a50f8c0e90bc7797bb5')
|
|
||||||
|
|
||||||
def test_allsubtitles(self):
|
def test_allsubtitles(self):
|
||||||
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.assertTrue(len(subtitles.keys()) >= 28)
|
self.assertTrue(len(subtitles.keys()) >= 28)
|
||||||
|
self.assertEqual(md5(subtitles['en']), '4262c1665ff928a2dada178f62cb8d14')
|
||||||
def test_list_subtitles(self):
|
self.assertEqual(md5(subtitles['fr']), '66a63f7f42c97a50f8c0e90bc7797bb5')
|
||||||
self.DL.expect_warning('Automatic Captions not supported by this server')
|
for lang in ['es', 'fr', 'de']:
|
||||||
self.DL.params['listsubtitles'] = True
|
|
||||||
info_dict = self.getInfoDict()
|
|
||||||
self.assertEqual(info_dict, None)
|
|
||||||
|
|
||||||
def test_automatic_captions(self):
|
|
||||||
self.DL.expect_warning('Automatic Captions not supported by this server')
|
|
||||||
self.DL.params['writeautomaticsub'] = True
|
|
||||||
self.DL.params['subtitleslang'] = ['en']
|
|
||||||
subtitles = self.getSubtitles()
|
|
||||||
self.assertTrue(len(subtitles.keys()) == 0)
|
|
||||||
|
|
||||||
def test_multiple_langs(self):
|
|
||||||
self.DL.params['writesubtitles'] = True
|
|
||||||
langs = ['es', 'fr', 'de']
|
|
||||||
self.DL.params['subtitleslangs'] = langs
|
|
||||||
subtitles = self.getSubtitles()
|
|
||||||
for lang in langs:
|
|
||||||
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)
|
||||||
|
|
||||||
|
|
||||||
class TestBlipTVSubtitles(BaseTestSubtitles):
|
|
||||||
url = 'http://blip.tv/a/a-6603250'
|
|
||||||
IE = BlipTVIE
|
|
||||||
|
|
||||||
def test_list_subtitles(self):
|
|
||||||
self.DL.expect_warning('Automatic Captions not supported by this server')
|
|
||||||
self.DL.params['listsubtitles'] = True
|
|
||||||
info_dict = self.getInfoDict()
|
|
||||||
self.assertEqual(info_dict, None)
|
|
||||||
|
|
||||||
def test_allsubtitles(self):
|
|
||||||
self.DL.expect_warning('Automatic Captions not supported by this server')
|
|
||||||
self.DL.params['writesubtitles'] = True
|
|
||||||
self.DL.params['allsubtitles'] = True
|
|
||||||
subtitles = self.getSubtitles()
|
|
||||||
self.assertEqual(set(subtitles.keys()), set(['en']))
|
|
||||||
self.assertEqual(md5(subtitles['en']), '5b75c300af65fe4476dff79478bb93e4')
|
|
||||||
|
|
||||||
|
|
||||||
class TestVimeoSubtitles(BaseTestSubtitles):
|
class TestVimeoSubtitles(BaseTestSubtitles):
|
||||||
url = 'http://vimeo.com/76979871'
|
url = 'http://vimeo.com/76979871'
|
||||||
IE = VimeoIE
|
IE = VimeoIE
|
||||||
|
|
||||||
def test_no_writesubtitles(self):
|
|
||||||
subtitles = self.getSubtitles()
|
|
||||||
self.assertEqual(subtitles, None)
|
|
||||||
|
|
||||||
def test_subtitles(self):
|
|
||||||
self.DL.params['writesubtitles'] = True
|
|
||||||
subtitles = self.getSubtitles()
|
|
||||||
self.assertEqual(md5(subtitles['en']), '8062383cf4dec168fc40a088aa6d5888')
|
|
||||||
|
|
||||||
def test_subtitles_lang(self):
|
|
||||||
self.DL.params['writesubtitles'] = True
|
|
||||||
self.DL.params['subtitleslangs'] = ['fr']
|
|
||||||
subtitles = self.getSubtitles()
|
|
||||||
self.assertEqual(md5(subtitles['fr']), 'b6191146a6c5d3a452244d853fde6dc8')
|
|
||||||
|
|
||||||
def test_allsubtitles(self):
|
def test_allsubtitles(self):
|
||||||
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(['de', 'en', 'es', 'fr']))
|
self.assertEqual(set(subtitles.keys()), set(['de', 'en', 'es', 'fr']))
|
||||||
|
self.assertEqual(md5(subtitles['en']), '8062383cf4dec168fc40a088aa6d5888')
|
||||||
def test_list_subtitles(self):
|
self.assertEqual(md5(subtitles['fr']), 'b6191146a6c5d3a452244d853fde6dc8')
|
||||||
self.DL.expect_warning('Automatic Captions not supported by this server')
|
|
||||||
self.DL.params['listsubtitles'] = True
|
|
||||||
info_dict = self.getInfoDict()
|
|
||||||
self.assertEqual(info_dict, None)
|
|
||||||
|
|
||||||
def test_automatic_captions(self):
|
|
||||||
self.DL.expect_warning('Automatic Captions not supported by this server')
|
|
||||||
self.DL.params['writeautomaticsub'] = True
|
|
||||||
self.DL.params['subtitleslang'] = ['en']
|
|
||||||
subtitles = self.getSubtitles()
|
|
||||||
self.assertTrue(len(subtitles.keys()) == 0)
|
|
||||||
|
|
||||||
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')
|
||||||
@@ -280,27 +162,13 @@ class TestVimeoSubtitles(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(len(subtitles), 0)
|
self.assertFalse(subtitles)
|
||||||
|
|
||||||
def test_multiple_langs(self):
|
|
||||||
self.DL.params['writesubtitles'] = True
|
|
||||||
langs = ['es', 'fr', 'de']
|
|
||||||
self.DL.params['subtitleslangs'] = langs
|
|
||||||
subtitles = self.getSubtitles()
|
|
||||||
for lang in langs:
|
|
||||||
self.assertTrue(subtitles.get(lang) is not None, 'Subtitles for \'%s\' not extracted' % lang)
|
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
def test_list_subtitles(self):
|
|
||||||
self.DL.expect_warning('Automatic Captions not supported by this server')
|
|
||||||
self.DL.params['listsubtitles'] = True
|
|
||||||
info_dict = self.getInfoDict()
|
|
||||||
self.assertEqual(info_dict, None)
|
|
||||||
|
|
||||||
def test_allsubtitles(self):
|
def test_allsubtitles(self):
|
||||||
self.DL.expect_warning('Automatic Captions not supported by this server')
|
self.DL.expect_warning('Automatic Captions not supported by this server')
|
||||||
self.DL.params['writesubtitles'] = True
|
self.DL.params['writesubtitles'] = True
|
||||||
@@ -315,19 +183,13 @@ class TestWallaSubtitles(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(len(subtitles), 0)
|
self.assertFalse(subtitles)
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
def test_list_subtitles(self):
|
|
||||||
self.DL.expect_warning('Automatic Captions not supported by this server')
|
|
||||||
self.DL.params['listsubtitles'] = True
|
|
||||||
info_dict = self.getInfoDict()
|
|
||||||
self.assertEqual(info_dict, None)
|
|
||||||
|
|
||||||
def test_allsubtitles(self):
|
def test_allsubtitles(self):
|
||||||
self.DL.expect_warning('Automatic Captions not supported by this server')
|
self.DL.expect_warning('Automatic Captions not supported by this server')
|
||||||
self.DL.params['writesubtitles'] = True
|
self.DL.params['writesubtitles'] = True
|
||||||
@@ -342,7 +204,154 @@ class TestCeskaTelevizeSubtitles(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(len(subtitles), 0)
|
self.assertFalse(subtitles)
|
||||||
|
|
||||||
|
|
||||||
|
class TestLyndaSubtitles(BaseTestSubtitles):
|
||||||
|
url = 'http://www.lynda.com/Bootstrap-tutorials/Using-exercise-files/110885/114408-4.html'
|
||||||
|
IE = LyndaIE
|
||||||
|
|
||||||
|
def test_allsubtitles(self):
|
||||||
|
self.DL.params['writesubtitles'] = True
|
||||||
|
self.DL.params['allsubtitles'] = True
|
||||||
|
subtitles = self.getSubtitles()
|
||||||
|
self.assertEqual(set(subtitles.keys()), set(['en']))
|
||||||
|
self.assertEqual(md5(subtitles['en']), '09bbe67222259bed60deaa26997d73a7')
|
||||||
|
|
||||||
|
|
||||||
|
class TestNPOSubtitles(BaseTestSubtitles):
|
||||||
|
url = 'http://www.npo.nl/nos-journaal/28-08-2014/POW_00722860'
|
||||||
|
IE = NPOIE
|
||||||
|
|
||||||
|
def test_allsubtitles(self):
|
||||||
|
self.DL.params['writesubtitles'] = True
|
||||||
|
self.DL.params['allsubtitles'] = True
|
||||||
|
subtitles = self.getSubtitles()
|
||||||
|
self.assertEqual(set(subtitles.keys()), set(['nl']))
|
||||||
|
self.assertEqual(md5(subtitles['nl']), 'fc6435027572b63fb4ab143abd5ad3f4')
|
||||||
|
|
||||||
|
|
||||||
|
class TestMTVSubtitles(BaseTestSubtitles):
|
||||||
|
url = 'http://www.cc.com/video-clips/kllhuv/stand-up-greg-fitzsimmons--uncensored---too-good-of-a-mother'
|
||||||
|
IE = ComedyCentralIE
|
||||||
|
|
||||||
|
def getInfoDict(self):
|
||||||
|
return super(TestMTVSubtitles, self).getInfoDict()['entries'][0]
|
||||||
|
|
||||||
|
def test_allsubtitles(self):
|
||||||
|
self.DL.params['writesubtitles'] = True
|
||||||
|
self.DL.params['allsubtitles'] = True
|
||||||
|
subtitles = self.getSubtitles()
|
||||||
|
self.assertEqual(set(subtitles.keys()), set(['en']))
|
||||||
|
self.assertEqual(md5(subtitles['en']), 'b9f6ca22a6acf597ec76f61749765e65')
|
||||||
|
|
||||||
|
|
||||||
|
class TestNRKSubtitles(BaseTestSubtitles):
|
||||||
|
url = 'http://tv.nrk.no/serie/ikke-gjoer-dette-hjemme/DMPV73000411/sesong-2/episode-1'
|
||||||
|
IE = NRKTVIE
|
||||||
|
|
||||||
|
def test_allsubtitles(self):
|
||||||
|
self.DL.params['writesubtitles'] = True
|
||||||
|
self.DL.params['allsubtitles'] = True
|
||||||
|
subtitles = self.getSubtitles()
|
||||||
|
self.assertEqual(set(subtitles.keys()), set(['no']))
|
||||||
|
self.assertEqual(md5(subtitles['no']), '544fa917d3197fcbee64634559221cc2')
|
||||||
|
|
||||||
|
|
||||||
|
class TestRaiSubtitles(BaseTestSubtitles):
|
||||||
|
url = 'http://www.rai.tv/dl/RaiTV/programmi/media/ContentItem-cb27157f-9dd0-4aee-b788-b1f67643a391.html'
|
||||||
|
IE = RaiTVIE
|
||||||
|
|
||||||
|
def test_allsubtitles(self):
|
||||||
|
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']), 'b1d90a98755126b61e667567a1f6680a')
|
||||||
|
|
||||||
|
|
||||||
|
class TestVikiSubtitles(BaseTestSubtitles):
|
||||||
|
url = 'http://www.viki.com/videos/1060846v-punch-episode-18'
|
||||||
|
IE = VikiIE
|
||||||
|
|
||||||
|
def test_allsubtitles(self):
|
||||||
|
self.DL.params['writesubtitles'] = True
|
||||||
|
self.DL.params['allsubtitles'] = True
|
||||||
|
subtitles = self.getSubtitles()
|
||||||
|
self.assertEqual(set(subtitles.keys()), set(['en']))
|
||||||
|
self.assertEqual(md5(subtitles['en']), '53cb083a5914b2d84ef1ab67b880d18a')
|
||||||
|
|
||||||
|
|
||||||
|
class TestThePlatformSubtitles(BaseTestSubtitles):
|
||||||
|
# from http://www.3playmedia.com/services-features/tools/integrations/theplatform/
|
||||||
|
# (see http://theplatform.com/about/partners/type/subtitles-closed-captioning/)
|
||||||
|
url = 'theplatform:JFUjUE1_ehvq'
|
||||||
|
IE = ThePlatformIE
|
||||||
|
|
||||||
|
def test_allsubtitles(self):
|
||||||
|
self.DL.params['writesubtitles'] = True
|
||||||
|
self.DL.params['allsubtitles'] = True
|
||||||
|
subtitles = self.getSubtitles()
|
||||||
|
self.assertEqual(set(subtitles.keys()), set(['en']))
|
||||||
|
self.assertEqual(md5(subtitles['en']), '97e7670cbae3c4d26ae8bcc7fdd78d4b')
|
||||||
|
|
||||||
|
|
||||||
|
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'
|
||||||
|
IE = ThePlatformFeedIE
|
||||||
|
|
||||||
|
def test_allsubtitles(self):
|
||||||
|
self.DL.params['writesubtitles'] = True
|
||||||
|
self.DL.params['allsubtitles'] = True
|
||||||
|
subtitles = self.getSubtitles()
|
||||||
|
self.assertEqual(set(subtitles.keys()), set(['en']))
|
||||||
|
self.assertEqual(md5(subtitles['en']), '48649a22e82b2da21c9a67a395eedade')
|
||||||
|
|
||||||
|
|
||||||
|
class TestRtveSubtitles(BaseTestSubtitles):
|
||||||
|
url = 'http://www.rtve.es/alacarta/videos/los-misterios-de-laura/misterios-laura-capitulo-32-misterio-del-numero-17-2-parte/2428621/'
|
||||||
|
IE = RTVEALaCartaIE
|
||||||
|
|
||||||
|
def test_allsubtitles(self):
|
||||||
|
print('Skipping, only available from Spain')
|
||||||
|
return
|
||||||
|
self.DL.params['writesubtitles'] = True
|
||||||
|
self.DL.params['allsubtitles'] = True
|
||||||
|
subtitles = self.getSubtitles()
|
||||||
|
self.assertEqual(set(subtitles.keys()), set(['es']))
|
||||||
|
self.assertEqual(md5(subtitles['es']), '69e70cae2d40574fb7316f31d6eb7fca')
|
||||||
|
|
||||||
|
|
||||||
|
class TestFunnyOrDieSubtitles(BaseTestSubtitles):
|
||||||
|
url = 'http://www.funnyordie.com/videos/224829ff6d/judd-apatow-will-direct-your-vine'
|
||||||
|
IE = FunnyOrDieIE
|
||||||
|
|
||||||
|
def test_allsubtitles(self):
|
||||||
|
self.DL.params['writesubtitles'] = True
|
||||||
|
self.DL.params['allsubtitles'] = True
|
||||||
|
subtitles = self.getSubtitles()
|
||||||
|
self.assertEqual(set(subtitles.keys()), set(['en']))
|
||||||
|
self.assertEqual(md5(subtitles['en']), 'c5593c193eacd353596c11c2d4f9ecc4')
|
||||||
|
|
||||||
|
|
||||||
|
class TestDemocracynowSubtitles(BaseTestSubtitles):
|
||||||
|
url = 'http://www.democracynow.org/shows/2015/7/3'
|
||||||
|
IE = DemocracynowIE
|
||||||
|
|
||||||
|
def test_allsubtitles(self):
|
||||||
|
self.DL.params['writesubtitles'] = True
|
||||||
|
self.DL.params['allsubtitles'] = True
|
||||||
|
subtitles = self.getSubtitles()
|
||||||
|
self.assertEqual(set(subtitles.keys()), set(['en']))
|
||||||
|
self.assertEqual(md5(subtitles['en']), 'acaca989e24a9e45a6719c9b3d60815c')
|
||||||
|
|
||||||
|
def test_subtitles_in_page(self):
|
||||||
|
self.url = 'http://www.democracynow.org/2015/7/3/this_flag_comes_down_today_bree'
|
||||||
|
self.DL.params['writesubtitles'] = True
|
||||||
|
self.DL.params['allsubtitles'] = True
|
||||||
|
subtitles = self.getSubtitles()
|
||||||
|
self.assertEqual(set(subtitles.keys()), set(['en']))
|
||||||
|
self.assertEqual(md5(subtitles['en']), 'acaca989e24a9e45a6719c9b3d60815c')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ def _make_testfunc(testfile):
|
|||||||
def test_func(self):
|
def test_func(self):
|
||||||
as_file = os.path.join(TEST_DIR, testfile)
|
as_file = os.path.join(TEST_DIR, testfile)
|
||||||
swf_file = os.path.join(TEST_DIR, test_id + '.swf')
|
swf_file = os.path.join(TEST_DIR, test_id + '.swf')
|
||||||
if ((not os.path.exists(swf_file))
|
if ((not os.path.exists(swf_file)) or
|
||||||
or os.path.getmtime(swf_file) < os.path.getmtime(as_file)):
|
os.path.getmtime(swf_file) < os.path.getmtime(as_file)):
|
||||||
# Recompile
|
# Recompile
|
||||||
try:
|
try:
|
||||||
subprocess.check_call([
|
subprocess.check_call([
|
||||||
|
|||||||
@@ -17,13 +17,22 @@ IGNORED_FILES = [
|
|||||||
'buildserver.py',
|
'buildserver.py',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
IGNORED_DIRS = [
|
||||||
|
'.git',
|
||||||
|
'.tox',
|
||||||
|
]
|
||||||
|
|
||||||
from test.helper import assertRegexpMatches
|
from test.helper import assertRegexpMatches
|
||||||
|
|
||||||
|
|
||||||
class TestUnicodeLiterals(unittest.TestCase):
|
class TestUnicodeLiterals(unittest.TestCase):
|
||||||
def test_all_files(self):
|
def test_all_files(self):
|
||||||
for dirpath, _, filenames in os.walk(rootDir):
|
for dirpath, dirnames, filenames in os.walk(rootDir):
|
||||||
|
for ignore_dir in IGNORED_DIRS:
|
||||||
|
if ignore_dir in dirnames:
|
||||||
|
# If we remove the directory from dirnames os.walk won't
|
||||||
|
# recurse into it
|
||||||
|
dirnames.remove(ignore_dir)
|
||||||
for basename in filenames:
|
for basename in filenames:
|
||||||
if not basename.endswith('.py'):
|
if not basename.endswith('.py'):
|
||||||
continue
|
continue
|
||||||
|
|||||||
30
test/test_update.py
Normal file
30
test/test_update.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/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__))))
|
||||||
|
|
||||||
|
|
||||||
|
import json
|
||||||
|
from youtube_dl.update import rsa_verify
|
||||||
|
|
||||||
|
|
||||||
|
class TestUpdate(unittest.TestCase):
|
||||||
|
def test_rsa_verify(self):
|
||||||
|
UPDATES_RSA_KEY = (0x9d60ee4d8f805312fdb15a62f87b95bd66177b91df176765d13514a0f1754bcd2057295c5b6f1d35daa6742c3ffc9a82d3e118861c207995a8031e151d863c9927e304576bc80692bc8e094896fcf11b66f3e29e04e3a71e9a11558558acea1840aec37fc396fb6b65dc81a1c4144e03bd1c011de62e3f1357b327d08426fe93, 65537)
|
||||||
|
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'versions.json'), 'rb') as f:
|
||||||
|
versions_info = f.read().decode()
|
||||||
|
versions_info = json.loads(versions_info)
|
||||||
|
signature = versions_info['signature']
|
||||||
|
del versions_info['signature']
|
||||||
|
self.assertTrue(rsa_verify(
|
||||||
|
json.dumps(versions_info, sort_keys=True).encode('utf-8'),
|
||||||
|
signature, UPDATES_RSA_KEY))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -21,9 +21,13 @@ from youtube_dl.utils import (
|
|||||||
clean_html,
|
clean_html,
|
||||||
DateRange,
|
DateRange,
|
||||||
detect_exe_version,
|
detect_exe_version,
|
||||||
|
determine_ext,
|
||||||
|
dict_get,
|
||||||
|
encode_compat_str,
|
||||||
encodeFilename,
|
encodeFilename,
|
||||||
escape_rfc3986,
|
escape_rfc3986,
|
||||||
escape_url,
|
escape_url,
|
||||||
|
ExtractorError,
|
||||||
find_xpath_attr,
|
find_xpath_attr,
|
||||||
fix_xml_ampersands,
|
fix_xml_ampersands,
|
||||||
InAdvancePagedList,
|
InAdvancePagedList,
|
||||||
@@ -38,6 +42,10 @@ from youtube_dl.utils import (
|
|||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
read_batch_urls,
|
read_batch_urls,
|
||||||
sanitize_filename,
|
sanitize_filename,
|
||||||
|
sanitize_path,
|
||||||
|
prepend_extension,
|
||||||
|
replace_extension,
|
||||||
|
remove_quotes,
|
||||||
shell_quote,
|
shell_quote,
|
||||||
smuggle_url,
|
smuggle_url,
|
||||||
str_to_int,
|
str_to_int,
|
||||||
@@ -48,12 +56,24 @@ from youtube_dl.utils import (
|
|||||||
unified_strdate,
|
unified_strdate,
|
||||||
unsmuggle_url,
|
unsmuggle_url,
|
||||||
uppercase_escape,
|
uppercase_escape,
|
||||||
|
lowercase_escape,
|
||||||
url_basename,
|
url_basename,
|
||||||
urlencode_postdata,
|
urlencode_postdata,
|
||||||
version_tuple,
|
version_tuple,
|
||||||
xpath_with_ns,
|
xpath_with_ns,
|
||||||
|
xpath_element,
|
||||||
|
xpath_text,
|
||||||
|
xpath_attr,
|
||||||
render_table,
|
render_table,
|
||||||
match_str,
|
match_str,
|
||||||
|
parse_dfxp_time_expr,
|
||||||
|
dfxp2srt,
|
||||||
|
cli_option,
|
||||||
|
cli_valueless_option,
|
||||||
|
cli_bool_option,
|
||||||
|
)
|
||||||
|
from youtube_dl.compat import (
|
||||||
|
compat_etree_fromstring,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -86,6 +106,11 @@ class TestUtil(unittest.TestCase):
|
|||||||
sanitize_filename('New World record at 0:12:34'),
|
sanitize_filename('New World record at 0:12:34'),
|
||||||
'New World record at 0_12_34')
|
'New World record at 0_12_34')
|
||||||
|
|
||||||
|
self.assertEqual(sanitize_filename('--gasdgf'), '_-gasdgf')
|
||||||
|
self.assertEqual(sanitize_filename('--gasdgf', is_id=True), '--gasdgf')
|
||||||
|
self.assertEqual(sanitize_filename('.gasdgf'), 'gasdgf')
|
||||||
|
self.assertEqual(sanitize_filename('.gasdgf', is_id=True), '.gasdgf')
|
||||||
|
|
||||||
forbidden = '"\0\\/'
|
forbidden = '"\0\\/'
|
||||||
for fc in forbidden:
|
for fc in forbidden:
|
||||||
for fbc in forbidden:
|
for fbc in forbidden:
|
||||||
@@ -126,6 +151,67 @@ class TestUtil(unittest.TestCase):
|
|||||||
self.assertEqual(sanitize_filename('_BD_eEpuzXw', is_id=True), '_BD_eEpuzXw')
|
self.assertEqual(sanitize_filename('_BD_eEpuzXw', is_id=True), '_BD_eEpuzXw')
|
||||||
self.assertEqual(sanitize_filename('N0Y__7-UOdI', is_id=True), 'N0Y__7-UOdI')
|
self.assertEqual(sanitize_filename('N0Y__7-UOdI', is_id=True), 'N0Y__7-UOdI')
|
||||||
|
|
||||||
|
def test_sanitize_path(self):
|
||||||
|
if sys.platform != 'win32':
|
||||||
|
return
|
||||||
|
|
||||||
|
self.assertEqual(sanitize_path('abc'), 'abc')
|
||||||
|
self.assertEqual(sanitize_path('abc/def'), 'abc\\def')
|
||||||
|
self.assertEqual(sanitize_path('abc\\def'), 'abc\\def')
|
||||||
|
self.assertEqual(sanitize_path('abc|def'), 'abc#def')
|
||||||
|
self.assertEqual(sanitize_path('<>:"|?*'), '#######')
|
||||||
|
self.assertEqual(sanitize_path('C:/abc/def'), 'C:\\abc\\def')
|
||||||
|
self.assertEqual(sanitize_path('C?:/abc/def'), 'C##\\abc\\def')
|
||||||
|
|
||||||
|
self.assertEqual(sanitize_path('\\\\?\\UNC\\ComputerName\\abc'), '\\\\?\\UNC\\ComputerName\\abc')
|
||||||
|
self.assertEqual(sanitize_path('\\\\?\\UNC/ComputerName/abc'), '\\\\?\\UNC\\ComputerName\\abc')
|
||||||
|
|
||||||
|
self.assertEqual(sanitize_path('\\\\?\\C:\\abc'), '\\\\?\\C:\\abc')
|
||||||
|
self.assertEqual(sanitize_path('\\\\?\\C:/abc'), '\\\\?\\C:\\abc')
|
||||||
|
self.assertEqual(sanitize_path('\\\\?\\C:\\ab?c\\de:f'), '\\\\?\\C:\\ab#c\\de#f')
|
||||||
|
self.assertEqual(sanitize_path('\\\\?\\C:\\abc'), '\\\\?\\C:\\abc')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
sanitize_path('youtube/%(uploader)s/%(autonumber)s-%(title)s-%(upload_date)s.%(ext)s'),
|
||||||
|
'youtube\\%(uploader)s\\%(autonumber)s-%(title)s-%(upload_date)s.%(ext)s')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
sanitize_path('youtube/TheWreckingYard ./00001-Not bad, Especially for Free! (1987 Yamaha 700)-20141116.mp4.part'),
|
||||||
|
'youtube\\TheWreckingYard #\\00001-Not bad, Especially for Free! (1987 Yamaha 700)-20141116.mp4.part')
|
||||||
|
self.assertEqual(sanitize_path('abc/def...'), 'abc\\def..#')
|
||||||
|
self.assertEqual(sanitize_path('abc.../def'), 'abc..#\\def')
|
||||||
|
self.assertEqual(sanitize_path('abc.../def...'), 'abc..#\\def..#')
|
||||||
|
|
||||||
|
self.assertEqual(sanitize_path('../abc'), '..\\abc')
|
||||||
|
self.assertEqual(sanitize_path('../../abc'), '..\\..\\abc')
|
||||||
|
self.assertEqual(sanitize_path('./abc'), 'abc')
|
||||||
|
self.assertEqual(sanitize_path('./../abc'), '..\\abc')
|
||||||
|
|
||||||
|
def test_prepend_extension(self):
|
||||||
|
self.assertEqual(prepend_extension('abc.ext', 'temp'), 'abc.temp.ext')
|
||||||
|
self.assertEqual(prepend_extension('abc.ext', 'temp', 'ext'), 'abc.temp.ext')
|
||||||
|
self.assertEqual(prepend_extension('abc.unexpected_ext', 'temp', 'ext'), 'abc.unexpected_ext.temp')
|
||||||
|
self.assertEqual(prepend_extension('abc', 'temp'), 'abc.temp')
|
||||||
|
self.assertEqual(prepend_extension('.abc', 'temp'), '.abc.temp')
|
||||||
|
self.assertEqual(prepend_extension('.abc.ext', 'temp'), '.abc.temp.ext')
|
||||||
|
|
||||||
|
def test_replace_extension(self):
|
||||||
|
self.assertEqual(replace_extension('abc.ext', 'temp'), 'abc.temp')
|
||||||
|
self.assertEqual(replace_extension('abc.ext', 'temp', 'ext'), 'abc.temp')
|
||||||
|
self.assertEqual(replace_extension('abc.unexpected_ext', 'temp', 'ext'), 'abc.unexpected_ext.temp')
|
||||||
|
self.assertEqual(replace_extension('abc', 'temp'), 'abc.temp')
|
||||||
|
self.assertEqual(replace_extension('.abc', 'temp'), '.abc.temp')
|
||||||
|
self.assertEqual(replace_extension('.abc.ext', 'temp'), '.abc.temp')
|
||||||
|
|
||||||
|
def test_remove_quotes(self):
|
||||||
|
self.assertEqual(remove_quotes(None), None)
|
||||||
|
self.assertEqual(remove_quotes('"'), '"')
|
||||||
|
self.assertEqual(remove_quotes("'"), "'")
|
||||||
|
self.assertEqual(remove_quotes(';'), ';')
|
||||||
|
self.assertEqual(remove_quotes('";'), '";')
|
||||||
|
self.assertEqual(remove_quotes('""'), '')
|
||||||
|
self.assertEqual(remove_quotes('";"'), ';')
|
||||||
|
|
||||||
def test_ordered_set(self):
|
def test_ordered_set(self):
|
||||||
self.assertEqual(orderedSet([1, 1, 2, 3, 4, 4, 5, 6, 7, 3, 5]), [1, 2, 3, 4, 5, 6, 7])
|
self.assertEqual(orderedSet([1, 1, 2, 3, 4, 4, 5, 6, 7, 3, 5]), [1, 2, 3, 4, 5, 6, 7])
|
||||||
self.assertEqual(orderedSet([]), [])
|
self.assertEqual(orderedSet([]), [])
|
||||||
@@ -135,8 +221,10 @@ class TestUtil(unittest.TestCase):
|
|||||||
|
|
||||||
def test_unescape_html(self):
|
def test_unescape_html(self):
|
||||||
self.assertEqual(unescapeHTML('%20;'), '%20;')
|
self.assertEqual(unescapeHTML('%20;'), '%20;')
|
||||||
self.assertEqual(
|
self.assertEqual(unescapeHTML('/'), '/')
|
||||||
unescapeHTML('é'), 'é')
|
self.assertEqual(unescapeHTML('/'), '/')
|
||||||
|
self.assertEqual(unescapeHTML('é'), 'é')
|
||||||
|
self.assertEqual(unescapeHTML('�'), '�')
|
||||||
|
|
||||||
def test_daterange(self):
|
def test_daterange(self):
|
||||||
_20century = DateRange("19000101", "20000101")
|
_20century = DateRange("19000101", "20000101")
|
||||||
@@ -160,6 +248,15 @@ class TestUtil(unittest.TestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
unified_strdate('2/2/2015 6:47:40 PM', day_first=False),
|
unified_strdate('2/2/2015 6:47:40 PM', day_first=False),
|
||||||
'20150202')
|
'20150202')
|
||||||
|
self.assertEqual(unified_strdate('25-09-2014'), '20140925')
|
||||||
|
self.assertEqual(unified_strdate('UNKNOWN DATE FORMAT'), None)
|
||||||
|
|
||||||
|
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/?download', None), None)
|
||||||
|
self.assertEqual(determine_ext('http://example.com/foo/bar.nonext/?download', None), None)
|
||||||
|
self.assertEqual(determine_ext('http://example.com/foo/bar/mp4?download', None), None)
|
||||||
|
self.assertEqual(determine_ext('http://example.com/foo/bar.m3u8//?download'), 'm3u8')
|
||||||
|
|
||||||
def test_find_xpath_attr(self):
|
def test_find_xpath_attr(self):
|
||||||
testxml = '''<root>
|
testxml = '''<root>
|
||||||
@@ -167,12 +264,21 @@ class TestUtil(unittest.TestCase):
|
|||||||
<node x="a"/>
|
<node x="a"/>
|
||||||
<node x="a" y="c" />
|
<node x="a" y="c" />
|
||||||
<node x="b" y="d" />
|
<node x="b" y="d" />
|
||||||
|
<node x="" />
|
||||||
</root>'''
|
</root>'''
|
||||||
doc = xml.etree.ElementTree.fromstring(testxml)
|
doc = compat_etree_fromstring(testxml)
|
||||||
|
|
||||||
|
self.assertEqual(find_xpath_attr(doc, './/fourohfour', 'n'), None)
|
||||||
self.assertEqual(find_xpath_attr(doc, './/fourohfour', 'n', 'v'), None)
|
self.assertEqual(find_xpath_attr(doc, './/fourohfour', 'n', 'v'), None)
|
||||||
|
self.assertEqual(find_xpath_attr(doc, './/node', 'n'), None)
|
||||||
|
self.assertEqual(find_xpath_attr(doc, './/node', 'n', 'v'), None)
|
||||||
|
self.assertEqual(find_xpath_attr(doc, './/node', 'x'), doc[1])
|
||||||
self.assertEqual(find_xpath_attr(doc, './/node', 'x', 'a'), doc[1])
|
self.assertEqual(find_xpath_attr(doc, './/node', 'x', 'a'), doc[1])
|
||||||
|
self.assertEqual(find_xpath_attr(doc, './/node', 'x', 'b'), doc[3])
|
||||||
|
self.assertEqual(find_xpath_attr(doc, './/node', 'y'), doc[2])
|
||||||
self.assertEqual(find_xpath_attr(doc, './/node', 'y', 'c'), doc[2])
|
self.assertEqual(find_xpath_attr(doc, './/node', 'y', 'c'), doc[2])
|
||||||
|
self.assertEqual(find_xpath_attr(doc, './/node', 'y', 'd'), doc[3])
|
||||||
|
self.assertEqual(find_xpath_attr(doc, './/node', 'x', ''), doc[4])
|
||||||
|
|
||||||
def test_xpath_with_ns(self):
|
def test_xpath_with_ns(self):
|
||||||
testxml = '''<root xmlns:media="http://example.com/">
|
testxml = '''<root xmlns:media="http://example.com/">
|
||||||
@@ -181,12 +287,56 @@ class TestUtil(unittest.TestCase):
|
|||||||
<url>http://server.com/download.mp3</url>
|
<url>http://server.com/download.mp3</url>
|
||||||
</media:song>
|
</media:song>
|
||||||
</root>'''
|
</root>'''
|
||||||
doc = xml.etree.ElementTree.fromstring(testxml)
|
doc = compat_etree_fromstring(testxml)
|
||||||
find = lambda p: doc.find(xpath_with_ns(p, {'media': 'http://example.com/'}))
|
find = lambda p: doc.find(xpath_with_ns(p, {'media': 'http://example.com/'}))
|
||||||
self.assertTrue(find('media:song') is not None)
|
self.assertTrue(find('media:song') is not None)
|
||||||
self.assertEqual(find('media:song/media:author').text, 'The Author')
|
self.assertEqual(find('media:song/media:author').text, 'The Author')
|
||||||
self.assertEqual(find('media:song/url').text, 'http://server.com/download.mp3')
|
self.assertEqual(find('media:song/url').text, 'http://server.com/download.mp3')
|
||||||
|
|
||||||
|
def test_xpath_element(self):
|
||||||
|
doc = xml.etree.ElementTree.Element('root')
|
||||||
|
div = xml.etree.ElementTree.SubElement(doc, 'div')
|
||||||
|
p = xml.etree.ElementTree.SubElement(div, 'p')
|
||||||
|
p.text = 'Foo'
|
||||||
|
self.assertEqual(xpath_element(doc, 'div/p'), p)
|
||||||
|
self.assertEqual(xpath_element(doc, ['div/p']), p)
|
||||||
|
self.assertEqual(xpath_element(doc, ['div/bar', 'div/p']), p)
|
||||||
|
self.assertEqual(xpath_element(doc, 'div/bar', default='default'), 'default')
|
||||||
|
self.assertEqual(xpath_element(doc, ['div/bar'], default='default'), 'default')
|
||||||
|
self.assertTrue(xpath_element(doc, 'div/bar') is None)
|
||||||
|
self.assertTrue(xpath_element(doc, ['div/bar']) is None)
|
||||||
|
self.assertTrue(xpath_element(doc, ['div/bar'], 'div/baz') is None)
|
||||||
|
self.assertRaises(ExtractorError, xpath_element, doc, 'div/bar', fatal=True)
|
||||||
|
self.assertRaises(ExtractorError, xpath_element, doc, ['div/bar'], fatal=True)
|
||||||
|
self.assertRaises(ExtractorError, xpath_element, doc, ['div/bar', 'div/baz'], fatal=True)
|
||||||
|
|
||||||
|
def test_xpath_text(self):
|
||||||
|
testxml = '''<root>
|
||||||
|
<div>
|
||||||
|
<p>Foo</p>
|
||||||
|
</div>
|
||||||
|
</root>'''
|
||||||
|
doc = compat_etree_fromstring(testxml)
|
||||||
|
self.assertEqual(xpath_text(doc, 'div/p'), 'Foo')
|
||||||
|
self.assertEqual(xpath_text(doc, 'div/bar', default='default'), 'default')
|
||||||
|
self.assertTrue(xpath_text(doc, 'div/bar') is None)
|
||||||
|
self.assertRaises(ExtractorError, xpath_text, doc, 'div/bar', fatal=True)
|
||||||
|
|
||||||
|
def test_xpath_attr(self):
|
||||||
|
testxml = '''<root>
|
||||||
|
<div>
|
||||||
|
<p x="a">Foo</p>
|
||||||
|
</div>
|
||||||
|
</root>'''
|
||||||
|
doc = compat_etree_fromstring(testxml)
|
||||||
|
self.assertEqual(xpath_attr(doc, 'div/p', 'x'), 'a')
|
||||||
|
self.assertEqual(xpath_attr(doc, 'div/bar', 'x'), None)
|
||||||
|
self.assertEqual(xpath_attr(doc, 'div/p', 'y'), None)
|
||||||
|
self.assertEqual(xpath_attr(doc, 'div/bar', 'x', default='default'), 'default')
|
||||||
|
self.assertEqual(xpath_attr(doc, 'div/p', 'y', default='default'), 'default')
|
||||||
|
self.assertRaises(ExtractorError, xpath_attr, doc, 'div/bar', 'x', fatal=True)
|
||||||
|
self.assertRaises(ExtractorError, xpath_attr, doc, 'div/p', 'y', fatal=True)
|
||||||
|
|
||||||
def test_smuggle_url(self):
|
def test_smuggle_url(self):
|
||||||
data = {"ö": "ö", "abc": [3]}
|
data = {"ö": "ö", "abc": [3]}
|
||||||
url = 'https://foo.bar/baz?x=y#a'
|
url = 'https://foo.bar/baz?x=y#a'
|
||||||
@@ -244,6 +394,8 @@ class TestUtil(unittest.TestCase):
|
|||||||
self.assertEqual(parse_duration('2.5 hours'), 9000)
|
self.assertEqual(parse_duration('2.5 hours'), 9000)
|
||||||
self.assertEqual(parse_duration('02:03:04'), 7384)
|
self.assertEqual(parse_duration('02:03:04'), 7384)
|
||||||
self.assertEqual(parse_duration('01:02:03:04'), 93784)
|
self.assertEqual(parse_duration('01:02:03:04'), 93784)
|
||||||
|
self.assertEqual(parse_duration('1 hour 3 minutes'), 3780)
|
||||||
|
self.assertEqual(parse_duration('87 Min.'), 5220)
|
||||||
|
|
||||||
def test_fix_xml_ampersands(self):
|
def test_fix_xml_ampersands(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -299,11 +451,39 @@ class TestUtil(unittest.TestCase):
|
|||||||
data = urlencode_postdata({'username': 'foo@bar.com', 'password': '1234'})
|
data = urlencode_postdata({'username': 'foo@bar.com', 'password': '1234'})
|
||||||
self.assertTrue(isinstance(data, bytes))
|
self.assertTrue(isinstance(data, bytes))
|
||||||
|
|
||||||
|
def test_dict_get(self):
|
||||||
|
FALSE_VALUES = {
|
||||||
|
'none': None,
|
||||||
|
'false': False,
|
||||||
|
'zero': 0,
|
||||||
|
'empty_string': '',
|
||||||
|
'empty_list': [],
|
||||||
|
}
|
||||||
|
d = FALSE_VALUES.copy()
|
||||||
|
d['a'] = 42
|
||||||
|
self.assertEqual(dict_get(d, 'a'), 42)
|
||||||
|
self.assertEqual(dict_get(d, 'b'), None)
|
||||||
|
self.assertEqual(dict_get(d, 'b', 42), 42)
|
||||||
|
self.assertEqual(dict_get(d, ('a', )), 42)
|
||||||
|
self.assertEqual(dict_get(d, ('b', 'a', )), 42)
|
||||||
|
self.assertEqual(dict_get(d, ('b', 'c', 'a', 'd', )), 42)
|
||||||
|
self.assertEqual(dict_get(d, ('b', 'c', )), None)
|
||||||
|
self.assertEqual(dict_get(d, ('b', 'c', ), 42), 42)
|
||||||
|
for key, false_value in FALSE_VALUES.items():
|
||||||
|
self.assertEqual(dict_get(d, ('b', 'c', key, )), None)
|
||||||
|
self.assertEqual(dict_get(d, ('b', 'c', key, ), skip_false_values=False), false_value)
|
||||||
|
|
||||||
|
def test_encode_compat_str(self):
|
||||||
|
self.assertEqual(encode_compat_str(b'\xd1\x82\xd0\xb5\xd1\x81\xd1\x82', 'utf-8'), 'тест')
|
||||||
|
self.assertEqual(encode_compat_str('тест', 'utf-8'), 'тест')
|
||||||
|
|
||||||
def test_parse_iso8601(self):
|
def test_parse_iso8601(self):
|
||||||
self.assertEqual(parse_iso8601('2014-03-23T23:04:26+0100'), 1395612266)
|
self.assertEqual(parse_iso8601('2014-03-23T23:04:26+0100'), 1395612266)
|
||||||
self.assertEqual(parse_iso8601('2014-03-23T22:04:26+0000'), 1395612266)
|
self.assertEqual(parse_iso8601('2014-03-23T22:04:26+0000'), 1395612266)
|
||||||
self.assertEqual(parse_iso8601('2014-03-23T22:04:26Z'), 1395612266)
|
self.assertEqual(parse_iso8601('2014-03-23T22:04:26Z'), 1395612266)
|
||||||
self.assertEqual(parse_iso8601('2014-03-23T22:04:26.1234Z'), 1395612266)
|
self.assertEqual(parse_iso8601('2014-03-23T22:04:26.1234Z'), 1395612266)
|
||||||
|
self.assertEqual(parse_iso8601('2015-09-29T08:27:31.727'), 1443515251)
|
||||||
|
self.assertEqual(parse_iso8601('2015-09-29T08-27-31.727'), None)
|
||||||
|
|
||||||
def test_strip_jsonp(self):
|
def test_strip_jsonp(self):
|
||||||
stripped = strip_jsonp('cb ([ {"id":"532cb",\n\n\n"x":\n3}\n]\n);')
|
stripped = strip_jsonp('cb ([ {"id":"532cb",\n\n\n"x":\n3}\n]\n);')
|
||||||
@@ -314,10 +494,18 @@ class TestUtil(unittest.TestCase):
|
|||||||
d = json.loads(stripped)
|
d = json.loads(stripped)
|
||||||
self.assertEqual(d, {'STATUS': 'OK'})
|
self.assertEqual(d, {'STATUS': 'OK'})
|
||||||
|
|
||||||
|
stripped = strip_jsonp('ps.embedHandler({"status": "success"});')
|
||||||
|
d = json.loads(stripped)
|
||||||
|
self.assertEqual(d, {'status': 'success'})
|
||||||
|
|
||||||
def test_uppercase_escape(self):
|
def test_uppercase_escape(self):
|
||||||
self.assertEqual(uppercase_escape('aä'), 'aä')
|
self.assertEqual(uppercase_escape('aä'), 'aä')
|
||||||
self.assertEqual(uppercase_escape('\\U0001d550'), '𝕐')
|
self.assertEqual(uppercase_escape('\\U0001d550'), '𝕐')
|
||||||
|
|
||||||
|
def test_lowercase_escape(self):
|
||||||
|
self.assertEqual(lowercase_escape('aä'), 'aä')
|
||||||
|
self.assertEqual(lowercase_escape('\\u0026'), '&')
|
||||||
|
|
||||||
def test_limit_length(self):
|
def test_limit_length(self):
|
||||||
self.assertEqual(limit_length(None, 12), None)
|
self.assertEqual(limit_length(None, 12), None)
|
||||||
self.assertEqual(limit_length('foo', 12), 'foo')
|
self.assertEqual(limit_length('foo', 12), 'foo')
|
||||||
@@ -370,6 +558,9 @@ class TestUtil(unittest.TestCase):
|
|||||||
"playlist":[{"controls":{"all":null}}]
|
"playlist":[{"controls":{"all":null}}]
|
||||||
}''')
|
}''')
|
||||||
|
|
||||||
|
inp = '''"The CW\\'s \\'Crazy Ex-Girlfriend\\'"'''
|
||||||
|
self.assertEqual(js_to_json(inp), '''"The CW's 'Crazy Ex-Girlfriend'"''')
|
||||||
|
|
||||||
inp = '"SAND Number: SAND 2013-7800P\\nPresenter: Tom Russo\\nHabanero Software Training - Xyce Software\\nXyce, Sandia\\u0027s"'
|
inp = '"SAND Number: SAND 2013-7800P\\nPresenter: Tom Russo\\nHabanero Software Training - Xyce Software\\nXyce, Sandia\\u0027s"'
|
||||||
json_code = js_to_json(inp)
|
json_code = js_to_json(inp)
|
||||||
self.assertEqual(json.loads(json_code), json.loads(inp))
|
self.assertEqual(json.loads(json_code), json.loads(inp))
|
||||||
@@ -391,6 +582,12 @@ class TestUtil(unittest.TestCase):
|
|||||||
self.assertEqual(d['x'], 1)
|
self.assertEqual(d['x'], 1)
|
||||||
self.assertEqual(d['y'], 'a')
|
self.assertEqual(d['y'], 'a')
|
||||||
|
|
||||||
|
on = js_to_json('["abc", "def",]')
|
||||||
|
self.assertEqual(json.loads(on), ['abc', 'def'])
|
||||||
|
|
||||||
|
on = js_to_json('{"abc": "def",}')
|
||||||
|
self.assertEqual(json.loads(on), {'abc': 'def'})
|
||||||
|
|
||||||
def test_clean_html(self):
|
def test_clean_html(self):
|
||||||
self.assertEqual(clean_html('a:\nb'), 'a: b')
|
self.assertEqual(clean_html('a:\nb'), 'a: b')
|
||||||
self.assertEqual(clean_html('a:\n "b"'), 'a: "b"')
|
self.assertEqual(clean_html('a:\n "b"'), 'a: "b"')
|
||||||
@@ -495,6 +692,106 @@ ffmpeg version 2.4.4 Copyright (c) 2000-2014 the FFmpeg ...'''), '2.4.4')
|
|||||||
'like_count > 100 & dislike_count <? 50 & description',
|
'like_count > 100 & dislike_count <? 50 & description',
|
||||||
{'like_count': 190, 'dislike_count': 10}))
|
{'like_count': 190, 'dislike_count': 10}))
|
||||||
|
|
||||||
|
def test_parse_dfxp_time_expr(self):
|
||||||
|
self.assertEqual(parse_dfxp_time_expr(None), None)
|
||||||
|
self.assertEqual(parse_dfxp_time_expr(''), None)
|
||||||
|
self.assertEqual(parse_dfxp_time_expr('0.1'), 0.1)
|
||||||
|
self.assertEqual(parse_dfxp_time_expr('0.1s'), 0.1)
|
||||||
|
self.assertEqual(parse_dfxp_time_expr('00:00:01'), 1.0)
|
||||||
|
self.assertEqual(parse_dfxp_time_expr('00:00:01.100'), 1.1)
|
||||||
|
self.assertEqual(parse_dfxp_time_expr('00:00:01:100'), 1.1)
|
||||||
|
|
||||||
|
def test_dfxp2srt(self):
|
||||||
|
dfxp_data = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<tt xmlns="http://www.w3.org/ns/ttml" xml:lang="en" xmlns:tts="http://www.w3.org/ns/ttml#parameter">
|
||||||
|
<body>
|
||||||
|
<div xml:lang="en">
|
||||||
|
<p begin="0" end="1">The following line contains Chinese characters and special symbols</p>
|
||||||
|
<p begin="1" end="2">第二行<br/>♪♪</p>
|
||||||
|
<p begin="2" dur="1"><span>Third<br/>Line</span></p>
|
||||||
|
<p begin="3" end="-1">Lines with invalid timestamps are ignored</p>
|
||||||
|
<p begin="-1" end="-1">Ignore, two</p>
|
||||||
|
<p begin="3" dur="-1">Ignored, three</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</tt>'''
|
||||||
|
srt_data = '''1
|
||||||
|
00:00:00,000 --> 00:00:01,000
|
||||||
|
The following line contains Chinese characters and special symbols
|
||||||
|
|
||||||
|
2
|
||||||
|
00:00:01,000 --> 00:00:02,000
|
||||||
|
第二行
|
||||||
|
♪♪
|
||||||
|
|
||||||
|
3
|
||||||
|
00:00:02,000 --> 00:00:03,000
|
||||||
|
Third
|
||||||
|
Line
|
||||||
|
|
||||||
|
'''
|
||||||
|
self.assertEqual(dfxp2srt(dfxp_data), srt_data)
|
||||||
|
|
||||||
|
dfxp_data_no_default_namespace = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<tt xml:lang="en" xmlns:tts="http://www.w3.org/ns/ttml#parameter">
|
||||||
|
<body>
|
||||||
|
<div xml:lang="en">
|
||||||
|
<p begin="0" end="1">The first line</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</tt>'''
|
||||||
|
srt_data = '''1
|
||||||
|
00:00:00,000 --> 00:00:01,000
|
||||||
|
The first line
|
||||||
|
|
||||||
|
'''
|
||||||
|
self.assertEqual(dfxp2srt(dfxp_data_no_default_namespace), srt_data)
|
||||||
|
|
||||||
|
def test_cli_option(self):
|
||||||
|
self.assertEqual(cli_option({'proxy': '127.0.0.1:3128'}, '--proxy', 'proxy'), ['--proxy', '127.0.0.1:3128'])
|
||||||
|
self.assertEqual(cli_option({'proxy': None}, '--proxy', 'proxy'), [])
|
||||||
|
self.assertEqual(cli_option({}, '--proxy', 'proxy'), [])
|
||||||
|
|
||||||
|
def test_cli_valueless_option(self):
|
||||||
|
self.assertEqual(cli_valueless_option(
|
||||||
|
{'downloader': 'external'}, '--external-downloader', 'downloader', 'external'), ['--external-downloader'])
|
||||||
|
self.assertEqual(cli_valueless_option(
|
||||||
|
{'downloader': 'internal'}, '--external-downloader', 'downloader', 'external'), [])
|
||||||
|
self.assertEqual(cli_valueless_option(
|
||||||
|
{'nocheckcertificate': True}, '--no-check-certificate', 'nocheckcertificate'), ['--no-check-certificate'])
|
||||||
|
self.assertEqual(cli_valueless_option(
|
||||||
|
{'nocheckcertificate': False}, '--no-check-certificate', 'nocheckcertificate'), [])
|
||||||
|
self.assertEqual(cli_valueless_option(
|
||||||
|
{'checkcertificate': True}, '--no-check-certificate', 'checkcertificate', False), [])
|
||||||
|
self.assertEqual(cli_valueless_option(
|
||||||
|
{'checkcertificate': False}, '--no-check-certificate', 'checkcertificate', False), ['--no-check-certificate'])
|
||||||
|
|
||||||
|
def test_cli_bool_option(self):
|
||||||
|
self.assertEqual(
|
||||||
|
cli_bool_option(
|
||||||
|
{'nocheckcertificate': True}, '--no-check-certificate', 'nocheckcertificate'),
|
||||||
|
['--no-check-certificate', 'true'])
|
||||||
|
self.assertEqual(
|
||||||
|
cli_bool_option(
|
||||||
|
{'nocheckcertificate': True}, '--no-check-certificate', 'nocheckcertificate', separator='='),
|
||||||
|
['--no-check-certificate=true'])
|
||||||
|
self.assertEqual(
|
||||||
|
cli_bool_option(
|
||||||
|
{'nocheckcertificate': True}, '--check-certificate', 'nocheckcertificate', 'false', 'true'),
|
||||||
|
['--check-certificate', 'false'])
|
||||||
|
self.assertEqual(
|
||||||
|
cli_bool_option(
|
||||||
|
{'nocheckcertificate': True}, '--check-certificate', 'nocheckcertificate', 'false', 'true', '='),
|
||||||
|
['--check-certificate=false'])
|
||||||
|
self.assertEqual(
|
||||||
|
cli_bool_option(
|
||||||
|
{'nocheckcertificate': False}, '--check-certificate', 'nocheckcertificate', 'false', 'true'),
|
||||||
|
['--check-certificate', 'true'])
|
||||||
|
self.assertEqual(
|
||||||
|
cli_bool_option(
|
||||||
|
{'nocheckcertificate': False}, '--check-certificate', 'nocheckcertificate', 'false', 'true', '='),
|
||||||
|
['--check-certificate=true'])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ params = get_params({
|
|||||||
|
|
||||||
|
|
||||||
TEST_ID = 'gr51aVj-mLg'
|
TEST_ID = 'gr51aVj-mLg'
|
||||||
ANNOTATIONS_FILE = TEST_ID + '.flv.annotations.xml'
|
ANNOTATIONS_FILE = TEST_ID + '.annotations.xml'
|
||||||
EXPECTED_ANNOTATIONS = ['Speech bubble', 'Note', 'Title', 'Spotlight', 'Label']
|
EXPECTED_ANNOTATIONS = ['Speech bubble', 'Note', 'Title', 'Spotlight', 'Label']
|
||||||
|
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ class TestAnnotations(unittest.TestCase):
|
|||||||
textTag = a.find('TEXT')
|
textTag = a.find('TEXT')
|
||||||
text = textTag.text
|
text = textTag.text
|
||||||
self.assertTrue(text in expected) # assertIn only added in python 2.7
|
self.assertTrue(text in expected) # assertIn only added in python 2.7
|
||||||
# remove the first occurance, there could be more than one annotation with the same text
|
# remove the first occurrence, there could be more than one annotation with the same text
|
||||||
expected.remove(text)
|
expected.remove(text)
|
||||||
# We should have seen (and removed) all the expected annotation texts.
|
# We should have seen (and removed) all the expected annotation texts.
|
||||||
self.assertEqual(len(expected), 0, 'Not all expected annotations were found.')
|
self.assertEqual(len(expected), 0, 'Not all expected annotations were found.')
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class TestYoutubeLists(unittest.TestCase):
|
|||||||
ie = YoutubePlaylistIE(dl)
|
ie = YoutubePlaylistIE(dl)
|
||||||
# TODO find a > 100 (paginating?) videos course
|
# TODO find a > 100 (paginating?) videos course
|
||||||
result = ie.extract('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')
|
result = ie.extract('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')
|
||||||
entries = result['entries']
|
entries = list(result['entries'])
|
||||||
self.assertEqual(YoutubeIE().extract_id(entries[0]['url']), 'j9WZyLZCBzs')
|
self.assertEqual(YoutubeIE().extract_id(entries[0]['url']), 'j9WZyLZCBzs')
|
||||||
self.assertEqual(len(entries), 25)
|
self.assertEqual(len(entries), 25)
|
||||||
self.assertEqual(YoutubeIE().extract_id(entries[-1]['url']), 'rYefUsYuEp0')
|
self.assertEqual(YoutubeIE().extract_id(entries[-1]['url']), 'rYefUsYuEp0')
|
||||||
@@ -57,5 +57,14 @@ class TestYoutubeLists(unittest.TestCase):
|
|||||||
entries = result['entries']
|
entries = result['entries']
|
||||||
self.assertEqual(len(entries), 100)
|
self.assertEqual(len(entries), 100)
|
||||||
|
|
||||||
|
def test_youtube_flat_playlist_titles(self):
|
||||||
|
dl = FakeYDL()
|
||||||
|
dl.params['extract_flat'] = True
|
||||||
|
ie = YoutubePlaylistIE(dl)
|
||||||
|
result = ie.extract('https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re')
|
||||||
|
self.assertIsPlaylist(result)
|
||||||
|
for entry in result['entries']:
|
||||||
|
self.assertTrue(entry.get('title'))
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
34
test/versions.json
Normal file
34
test/versions.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"latest": "2013.01.06",
|
||||||
|
"signature": "72158cdba391628569ffdbea259afbcf279bbe3d8aeb7492690735dc1cfa6afa754f55c61196f3871d429599ab22f2667f1fec98865527b32632e7f4b3675a7ef0f0fbe084d359256ae4bba68f0d33854e531a70754712f244be71d4b92e664302aa99653ee4df19800d955b6c4149cd2b3f24288d6e4b40b16126e01f4c8ce6",
|
||||||
|
"versions": {
|
||||||
|
"2013.01.02": {
|
||||||
|
"bin": [
|
||||||
|
"http://youtube-dl.org/downloads/2013.01.02/youtube-dl",
|
||||||
|
"f5b502f8aaa77675c4884938b1e4871ebca2611813a0c0e74f60c0fbd6dcca6b"
|
||||||
|
],
|
||||||
|
"exe": [
|
||||||
|
"http://youtube-dl.org/downloads/2013.01.02/youtube-dl.exe",
|
||||||
|
"75fa89d2ce297d102ff27675aa9d92545bbc91013f52ec52868c069f4f9f0422"
|
||||||
|
],
|
||||||
|
"tar": [
|
||||||
|
"http://youtube-dl.org/downloads/2013.01.02/youtube-dl-2013.01.02.tar.gz",
|
||||||
|
"6a66d022ac8e1c13da284036288a133ec8dba003b7bd3a5179d0c0daca8c8196"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2013.01.06": {
|
||||||
|
"bin": [
|
||||||
|
"http://youtube-dl.org/downloads/2013.01.06/youtube-dl",
|
||||||
|
"64b6ed8865735c6302e836d4d832577321b4519aa02640dc508580c1ee824049"
|
||||||
|
],
|
||||||
|
"exe": [
|
||||||
|
"http://youtube-dl.org/downloads/2013.01.06/youtube-dl.exe",
|
||||||
|
"58609baf91e4389d36e3ba586e21dab882daaaee537e4448b1265392ae86ff84"
|
||||||
|
],
|
||||||
|
"tar": [
|
||||||
|
"http://youtube-dl.org/downloads/2013.01.06/youtube-dl-2013.01.06.tar.gz",
|
||||||
|
"fe77ab20a95d980ed17a659aa67e371fdd4d656d19c4c7950e7b720b0c2f1a86"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
tox.ini
9
tox.ini
@@ -1,8 +1,13 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist = py26,py27,py33
|
envlist = py26,py27,py33,py34,py35
|
||||||
[testenv]
|
[testenv]
|
||||||
deps =
|
deps =
|
||||||
nose
|
nose
|
||||||
coverage
|
coverage
|
||||||
commands = nosetests --verbose {posargs:test} # --with-coverage --cover-package=youtube_dl --cover-html
|
# We need a valid $HOME for test_compat_expanduser
|
||||||
|
passenv = HOME
|
||||||
|
defaultargs = test --exclude test_download.py --exclude test_age_restriction.py
|
||||||
|
--exclude test_subtitles.py --exclude test_write_annotations.py
|
||||||
|
--exclude test_youtube_lists.py
|
||||||
|
commands = nosetests --verbose {posargs:{[testenv]defaultargs}} # --with-coverage --cover-package=youtube_dl --cover-html
|
||||||
# test.test_download:TestDownload.test_NowVideo
|
# test.test_download:TestDownload.test_NowVideo
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,7 @@ from .compat import (
|
|||||||
compat_expanduser,
|
compat_expanduser,
|
||||||
compat_getpass,
|
compat_getpass,
|
||||||
compat_print,
|
compat_print,
|
||||||
|
compat_shlex_split,
|
||||||
workaround_optparse_bug9161,
|
workaround_optparse_bug9161,
|
||||||
)
|
)
|
||||||
from .utils import (
|
from .utils import (
|
||||||
@@ -168,8 +169,11 @@ def _real_main(argv=None):
|
|||||||
if not opts.audioquality.isdigit():
|
if not opts.audioquality.isdigit():
|
||||||
parser.error('invalid audio quality specified')
|
parser.error('invalid audio quality specified')
|
||||||
if opts.recodevideo is not None:
|
if opts.recodevideo is not None:
|
||||||
if opts.recodevideo not in ['mp4', 'flv', 'webm', 'ogg', 'mkv']:
|
if opts.recodevideo not in ['mp4', 'flv', 'webm', 'ogg', 'mkv', 'avi']:
|
||||||
parser.error('invalid video recode format specified')
|
parser.error('invalid video recode format specified')
|
||||||
|
if opts.convertsubtitles is not None:
|
||||||
|
if opts.convertsubtitles not in ['srt', 'vtt', 'ass']:
|
||||||
|
parser.error('invalid subtitle format specified')
|
||||||
|
|
||||||
if opts.date is not None:
|
if opts.date is not None:
|
||||||
date = DateRange.day(opts.date)
|
date = DateRange.day(opts.date)
|
||||||
@@ -185,18 +189,14 @@ def _real_main(argv=None):
|
|||||||
if opts.allsubtitles and not opts.writeautomaticsub:
|
if opts.allsubtitles and not opts.writeautomaticsub:
|
||||||
opts.writesubtitles = True
|
opts.writesubtitles = True
|
||||||
|
|
||||||
if sys.version_info < (3,):
|
outtmpl = ((opts.outtmpl is not None and opts.outtmpl) or
|
||||||
# In Python 2, sys.argv is a bytestring (also note http://bugs.python.org/issue2128 for Windows systems)
|
(opts.format == '-1' and opts.usetitle and '%(title)s-%(id)s-%(format)s.%(ext)s') or
|
||||||
if opts.outtmpl is not None:
|
(opts.format == '-1' and '%(id)s-%(format)s.%(ext)s') or
|
||||||
opts.outtmpl = opts.outtmpl.decode(preferredencoding())
|
(opts.usetitle and opts.autonumber and '%(autonumber)s-%(title)s-%(id)s.%(ext)s') or
|
||||||
outtmpl = ((opts.outtmpl is not None and opts.outtmpl)
|
(opts.usetitle and '%(title)s-%(id)s.%(ext)s') or
|
||||||
or (opts.format == '-1' and opts.usetitle and '%(title)s-%(id)s-%(format)s.%(ext)s')
|
(opts.useid and '%(id)s.%(ext)s') or
|
||||||
or (opts.format == '-1' and '%(id)s-%(format)s.%(ext)s')
|
(opts.autonumber and '%(autonumber)s-%(id)s.%(ext)s') or
|
||||||
or (opts.usetitle and opts.autonumber and '%(autonumber)s-%(title)s-%(id)s.%(ext)s')
|
DEFAULT_OUTTMPL)
|
||||||
or (opts.usetitle and '%(title)s-%(id)s.%(ext)s')
|
|
||||||
or (opts.useid and '%(id)s.%(ext)s')
|
|
||||||
or (opts.autonumber and '%(autonumber)s-%(id)s.%(ext)s')
|
|
||||||
or DEFAULT_OUTTMPL)
|
|
||||||
if not os.path.splitext(outtmpl)[1] and opts.extractaudio:
|
if not os.path.splitext(outtmpl)[1] and opts.extractaudio:
|
||||||
parser.error('Cannot download a video and extract audio into the same'
|
parser.error('Cannot download a video and extract audio into the same'
|
||||||
' file! Use "{0}.%(ext)s" instead of "{0}" as the output'
|
' file! Use "{0}.%(ext)s" instead of "{0}" as the output'
|
||||||
@@ -209,6 +209,11 @@ def _real_main(argv=None):
|
|||||||
# PostProcessors
|
# PostProcessors
|
||||||
postprocessors = []
|
postprocessors = []
|
||||||
# Add the metadata pp first, the other pps will copy it
|
# Add the metadata pp first, the other pps will copy it
|
||||||
|
if opts.metafromtitle:
|
||||||
|
postprocessors.append({
|
||||||
|
'key': 'MetadataFromTitle',
|
||||||
|
'titleformat': opts.metafromtitle
|
||||||
|
})
|
||||||
if opts.addmetadata:
|
if opts.addmetadata:
|
||||||
postprocessors.append({'key': 'FFmpegMetadata'})
|
postprocessors.append({'key': 'FFmpegMetadata'})
|
||||||
if opts.extractaudio:
|
if opts.extractaudio:
|
||||||
@@ -223,23 +228,30 @@ def _real_main(argv=None):
|
|||||||
'key': 'FFmpegVideoConvertor',
|
'key': 'FFmpegVideoConvertor',
|
||||||
'preferedformat': opts.recodevideo,
|
'preferedformat': opts.recodevideo,
|
||||||
})
|
})
|
||||||
|
if opts.convertsubtitles:
|
||||||
|
postprocessors.append({
|
||||||
|
'key': 'FFmpegSubtitlesConvertor',
|
||||||
|
'format': opts.convertsubtitles,
|
||||||
|
})
|
||||||
if opts.embedsubtitles:
|
if opts.embedsubtitles:
|
||||||
postprocessors.append({
|
postprocessors.append({
|
||||||
'key': 'FFmpegEmbedSubtitle',
|
'key': 'FFmpegEmbedSubtitle',
|
||||||
'subtitlesformat': opts.subtitlesformat,
|
|
||||||
})
|
})
|
||||||
if opts.xattrs:
|
if opts.xattrs:
|
||||||
postprocessors.append({'key': 'XAttrMetadata'})
|
postprocessors.append({'key': 'XAttrMetadata'})
|
||||||
if opts.embedthumbnail:
|
if opts.embedthumbnail:
|
||||||
if not opts.addmetadata:
|
already_have_thumbnail = opts.writethumbnail or opts.write_all_thumbnails
|
||||||
postprocessors.append({'key': 'FFmpegAudioFix'})
|
postprocessors.append({
|
||||||
postprocessors.append({'key': 'AtomicParsley'})
|
'key': 'EmbedThumbnail',
|
||||||
|
'already_have_thumbnail': already_have_thumbnail
|
||||||
|
})
|
||||||
|
if not already_have_thumbnail:
|
||||||
|
opts.writethumbnail = True
|
||||||
# Please keep ExecAfterDownload towards the bottom as it allows the user to modify the final file in any way.
|
# Please keep ExecAfterDownload towards the bottom as it allows the user to modify the final file in any way.
|
||||||
# So if the user is able to remove the file before your postprocessor runs it might cause a few problems.
|
# So if the user is able to remove the file before your postprocessor runs it might cause a few problems.
|
||||||
if opts.exec_cmd:
|
if opts.exec_cmd:
|
||||||
postprocessors.append({
|
postprocessors.append({
|
||||||
'key': 'ExecAfterDownload',
|
'key': 'ExecAfterDownload',
|
||||||
'verboseOutput': opts.verbose,
|
|
||||||
'exec_cmd': opts.exec_cmd,
|
'exec_cmd': opts.exec_cmd,
|
||||||
})
|
})
|
||||||
if opts.xattr_set_filesize:
|
if opts.xattr_set_filesize:
|
||||||
@@ -248,6 +260,12 @@ def _real_main(argv=None):
|
|||||||
xattr # Confuse flake8
|
xattr # Confuse flake8
|
||||||
except ImportError:
|
except ImportError:
|
||||||
parser.error('setting filesize xattr requested but python-xattr is not available')
|
parser.error('setting filesize xattr requested but python-xattr is not available')
|
||||||
|
external_downloader_args = None
|
||||||
|
if opts.external_downloader_args:
|
||||||
|
external_downloader_args = compat_shlex_split(opts.external_downloader_args)
|
||||||
|
postprocessor_args = None
|
||||||
|
if opts.postprocessor_args:
|
||||||
|
postprocessor_args = compat_shlex_split(opts.postprocessor_args)
|
||||||
match_filter = (
|
match_filter = (
|
||||||
None if opts.match_filter is None
|
None if opts.match_filter is None
|
||||||
else match_filter_func(opts.match_filter))
|
else match_filter_func(opts.match_filter))
|
||||||
@@ -273,12 +291,12 @@ def _real_main(argv=None):
|
|||||||
'simulate': opts.simulate or any_getting,
|
'simulate': opts.simulate or any_getting,
|
||||||
'skip_download': opts.skip_download,
|
'skip_download': opts.skip_download,
|
||||||
'format': opts.format,
|
'format': opts.format,
|
||||||
'format_limit': opts.format_limit,
|
|
||||||
'listformats': opts.listformats,
|
'listformats': opts.listformats,
|
||||||
'outtmpl': outtmpl,
|
'outtmpl': outtmpl,
|
||||||
'autonumber_size': opts.autonumber_size,
|
'autonumber_size': opts.autonumber_size,
|
||||||
'restrictfilenames': opts.restrictfilenames,
|
'restrictfilenames': opts.restrictfilenames,
|
||||||
'ignoreerrors': opts.ignoreerrors,
|
'ignoreerrors': opts.ignoreerrors,
|
||||||
|
'force_generic_extractor': opts.force_generic_extractor,
|
||||||
'ratelimit': opts.ratelimit,
|
'ratelimit': opts.ratelimit,
|
||||||
'nooverwrites': opts.nooverwrites,
|
'nooverwrites': opts.nooverwrites,
|
||||||
'retries': opts_retries,
|
'retries': opts_retries,
|
||||||
@@ -336,7 +354,6 @@ def _real_main(argv=None):
|
|||||||
'default_search': opts.default_search,
|
'default_search': opts.default_search,
|
||||||
'youtube_include_dash_manifest': opts.youtube_include_dash_manifest,
|
'youtube_include_dash_manifest': opts.youtube_include_dash_manifest,
|
||||||
'encoding': opts.encoding,
|
'encoding': opts.encoding,
|
||||||
'exec_cmd': opts.exec_cmd,
|
|
||||||
'extract_flat': opts.extract_flat,
|
'extract_flat': opts.extract_flat,
|
||||||
'merge_output_format': opts.merge_output_format,
|
'merge_output_format': opts.merge_output_format,
|
||||||
'postprocessors': postprocessors,
|
'postprocessors': postprocessors,
|
||||||
@@ -352,12 +369,16 @@ def _real_main(argv=None):
|
|||||||
'no_color': opts.no_color,
|
'no_color': opts.no_color,
|
||||||
'ffmpeg_location': opts.ffmpeg_location,
|
'ffmpeg_location': opts.ffmpeg_location,
|
||||||
'hls_prefer_native': opts.hls_prefer_native,
|
'hls_prefer_native': opts.hls_prefer_native,
|
||||||
|
'hls_use_mpegts': opts.hls_use_mpegts,
|
||||||
|
'external_downloader_args': external_downloader_args,
|
||||||
|
'postprocessor_args': postprocessor_args,
|
||||||
|
'cn_verification_proxy': opts.cn_verification_proxy,
|
||||||
}
|
}
|
||||||
|
|
||||||
with YoutubeDL(ydl_opts) as ydl:
|
with YoutubeDL(ydl_opts) as ydl:
|
||||||
# Update version
|
# Update version
|
||||||
if opts.update_self:
|
if opts.update_self:
|
||||||
update_self(ydl.to_screen, opts.verbose)
|
update_self(ydl.to_screen, opts.verbose, ydl._opener)
|
||||||
|
|
||||||
# Remove cache dir
|
# Remove cache dir
|
||||||
if opts.rm_cachedir:
|
if opts.rm_cachedir:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ if __package__ is None and not hasattr(sys, "frozen"):
|
|||||||
# direct call of __main__.py
|
# direct call of __main__.py
|
||||||
import os.path
|
import os.path
|
||||||
path = os.path.realpath(os.path.abspath(__file__))
|
path = os.path.realpath(os.path.abspath(__file__))
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(path)))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(path)))
|
||||||
|
|
||||||
import youtube_dl
|
import youtube_dl
|
||||||
|
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ def aes_decrypt_text(data, password, key_size_bytes):
|
|||||||
"""
|
"""
|
||||||
NONCE_LENGTH_BYTES = 8
|
NONCE_LENGTH_BYTES = 8
|
||||||
|
|
||||||
data = bytes_to_intlist(base64.b64decode(data))
|
data = bytes_to_intlist(base64.b64decode(data.encode('utf-8')))
|
||||||
password = bytes_to_intlist(password.encode('utf-8'))
|
password = bytes_to_intlist(password.encode('utf-8'))
|
||||||
|
|
||||||
key = password[:key_size_bytes] + [0] * (key_size_bytes - len(password))
|
key = password[:key_size_bytes] + [0] * (key_size_bytes - len(password))
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import binascii
|
||||||
|
import collections
|
||||||
|
import email
|
||||||
import getpass
|
import getpass
|
||||||
|
import io
|
||||||
import optparse
|
import optparse
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import shlex
|
||||||
|
import shutil
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import itertools
|
||||||
|
import xml.etree.ElementTree
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -34,21 +42,26 @@ try:
|
|||||||
except ImportError: # Python 2
|
except ImportError: # Python 2
|
||||||
import urlparse as compat_urlparse
|
import urlparse as compat_urlparse
|
||||||
|
|
||||||
|
try:
|
||||||
|
import urllib.response as compat_urllib_response
|
||||||
|
except ImportError: # Python 2
|
||||||
|
import urllib as compat_urllib_response
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import http.cookiejar as compat_cookiejar
|
import http.cookiejar as compat_cookiejar
|
||||||
except ImportError: # Python 2
|
except ImportError: # Python 2
|
||||||
import cookielib as compat_cookiejar
|
import cookielib as compat_cookiejar
|
||||||
|
|
||||||
|
try:
|
||||||
|
import http.cookies as compat_cookies
|
||||||
|
except ImportError: # Python 2
|
||||||
|
import Cookie as compat_cookies
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import html.entities as compat_html_entities
|
import html.entities as compat_html_entities
|
||||||
except ImportError: # Python 2
|
except ImportError: # Python 2
|
||||||
import htmlentitydefs as compat_html_entities
|
import htmlentitydefs as compat_html_entities
|
||||||
|
|
||||||
try:
|
|
||||||
import html.parser as compat_html_parser
|
|
||||||
except ImportError: # Python 2
|
|
||||||
import HTMLParser as compat_html_parser
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import http.client as compat_http_client
|
import http.client as compat_http_client
|
||||||
except ImportError: # Python 2
|
except ImportError: # Python 2
|
||||||
@@ -77,47 +90,113 @@ except ImportError:
|
|||||||
import BaseHTTPServer as compat_http_server
|
import BaseHTTPServer as compat_http_server
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
compat_str = unicode # Python 2
|
||||||
|
except NameError:
|
||||||
|
compat_str = str
|
||||||
|
|
||||||
|
try:
|
||||||
|
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
|
||||||
except ImportError:
|
from urllib.parse import unquote_plus as compat_urllib_parse_unquote_plus
|
||||||
def compat_urllib_parse_unquote(string, encoding='utf-8', errors='replace'):
|
except ImportError: # Python 2
|
||||||
if string == '':
|
_asciire = (compat_urllib_parse._asciire if hasattr(compat_urllib_parse, '_asciire')
|
||||||
|
else re.compile('([\x00-\x7f]+)'))
|
||||||
|
|
||||||
|
# HACK: The following are the correct unquote_to_bytes, unquote and unquote_plus
|
||||||
|
# implementations from cpython 3.4.3's stdlib. Python 2's version
|
||||||
|
# is apparently broken (see https://github.com/rg3/youtube-dl/pull/6244)
|
||||||
|
|
||||||
|
def compat_urllib_parse_unquote_to_bytes(string):
|
||||||
|
"""unquote_to_bytes('abc%20def') -> b'abc def'."""
|
||||||
|
# Note: strings are encoded as UTF-8. This is only an issue if it contains
|
||||||
|
# unescaped non-ASCII characters, which URIs should not.
|
||||||
|
if not string:
|
||||||
|
# Is it a string-like object?
|
||||||
|
string.split
|
||||||
|
return b''
|
||||||
|
if isinstance(string, compat_str):
|
||||||
|
string = string.encode('utf-8')
|
||||||
|
bits = string.split(b'%')
|
||||||
|
if len(bits) == 1:
|
||||||
return string
|
return string
|
||||||
res = string.split('%')
|
res = [bits[0]]
|
||||||
if len(res) == 1:
|
append = res.append
|
||||||
|
for item in bits[1:]:
|
||||||
|
try:
|
||||||
|
append(compat_urllib_parse._hextochr[item[:2]])
|
||||||
|
append(item[2:])
|
||||||
|
except KeyError:
|
||||||
|
append(b'%')
|
||||||
|
append(item)
|
||||||
|
return b''.join(res)
|
||||||
|
|
||||||
|
def compat_urllib_parse_unquote(string, encoding='utf-8', errors='replace'):
|
||||||
|
"""Replace %xx escapes by their single-character equivalent. The optional
|
||||||
|
encoding and errors parameters specify how to decode percent-encoded
|
||||||
|
sequences into Unicode characters, as accepted by the bytes.decode()
|
||||||
|
method.
|
||||||
|
By default, percent-encoded sequences are decoded with UTF-8, and invalid
|
||||||
|
sequences are replaced by a placeholder character.
|
||||||
|
|
||||||
|
unquote('abc%20def') -> 'abc def'.
|
||||||
|
"""
|
||||||
|
if '%' not in string:
|
||||||
|
string.split
|
||||||
return string
|
return string
|
||||||
if encoding is None:
|
if encoding is None:
|
||||||
encoding = 'utf-8'
|
encoding = 'utf-8'
|
||||||
if errors is None:
|
if errors is None:
|
||||||
errors = 'replace'
|
errors = 'replace'
|
||||||
# pct_sequence: contiguous sequence of percent-encoded bytes, decoded
|
bits = _asciire.split(string)
|
||||||
pct_sequence = b''
|
res = [bits[0]]
|
||||||
string = res[0]
|
append = res.append
|
||||||
for item in res[1:]:
|
for i in range(1, len(bits), 2):
|
||||||
try:
|
append(compat_urllib_parse_unquote_to_bytes(bits[i]).decode(encoding, errors))
|
||||||
if not item:
|
append(bits[i + 1])
|
||||||
raise ValueError
|
return ''.join(res)
|
||||||
pct_sequence += item[:2].decode('hex')
|
|
||||||
rest = item[2:]
|
def compat_urllib_parse_unquote_plus(string, encoding='utf-8', errors='replace'):
|
||||||
if not rest:
|
"""Like unquote(), but also replace plus signs by spaces, as required for
|
||||||
# This segment was just a single percent-encoded character.
|
unquoting HTML form values.
|
||||||
# May be part of a sequence of code units, so delay decoding.
|
|
||||||
# (Stored in pct_sequence).
|
unquote_plus('%7e/abc+def') -> '~/abc def'
|
||||||
continue
|
"""
|
||||||
except ValueError:
|
string = string.replace('+', ' ')
|
||||||
rest = '%' + item
|
return compat_urllib_parse_unquote(string, encoding, errors)
|
||||||
# Encountered non-percent-encoded characters. Flush the current
|
|
||||||
# pct_sequence.
|
|
||||||
string += pct_sequence.decode(encoding, errors) + rest
|
|
||||||
pct_sequence = b''
|
|
||||||
if pct_sequence:
|
|
||||||
# Flush the final pct_sequence
|
|
||||||
string += pct_sequence.decode(encoding, errors)
|
|
||||||
return string
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
compat_str = unicode # Python 2
|
from urllib.request import DataHandler as compat_urllib_request_DataHandler
|
||||||
except NameError:
|
except ImportError: # Python < 3.4
|
||||||
compat_str = str
|
# Ported from CPython 98774:1733b3bd46db, Lib/urllib/request.py
|
||||||
|
class compat_urllib_request_DataHandler(compat_urllib_request.BaseHandler):
|
||||||
|
def data_open(self, req):
|
||||||
|
# data URLs as specified in RFC 2397.
|
||||||
|
#
|
||||||
|
# ignores POSTed data
|
||||||
|
#
|
||||||
|
# syntax:
|
||||||
|
# dataurl := "data:" [ mediatype ] [ ";base64" ] "," data
|
||||||
|
# mediatype := [ type "/" subtype ] *( ";" parameter )
|
||||||
|
# data := *urlchar
|
||||||
|
# parameter := attribute "=" value
|
||||||
|
url = req.get_full_url()
|
||||||
|
|
||||||
|
scheme, data = url.split(":", 1)
|
||||||
|
mediatype, data = data.split(",", 1)
|
||||||
|
|
||||||
|
# even base64 encoded data URLs might be quoted so unquote in any case:
|
||||||
|
data = compat_urllib_parse_unquote_to_bytes(data)
|
||||||
|
if mediatype.endswith(";base64"):
|
||||||
|
data = binascii.a2b_base64(data)
|
||||||
|
mediatype = mediatype[:-7]
|
||||||
|
|
||||||
|
if not mediatype:
|
||||||
|
mediatype = "text/plain;charset=US-ASCII"
|
||||||
|
|
||||||
|
headers = email.message_from_string(
|
||||||
|
"Content-type: %s\nContent-length: %d\n" % (mediatype, len(data)))
|
||||||
|
|
||||||
|
return compat_urllib_response.addinfourl(io.BytesIO(data), headers, url)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
compat_basestring = basestring # Python 2
|
compat_basestring = basestring # Python 2
|
||||||
@@ -134,6 +213,43 @@ try:
|
|||||||
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
|
||||||
|
|
||||||
|
if sys.version_info[0] >= 3:
|
||||||
|
compat_etree_fromstring = xml.etree.ElementTree.fromstring
|
||||||
|
else:
|
||||||
|
# python 2.x tries to encode unicode strings with ascii (see the
|
||||||
|
# XMLParser._fixtext method)
|
||||||
|
etree = xml.etree.ElementTree
|
||||||
|
|
||||||
|
try:
|
||||||
|
_etree_iter = etree.Element.iter
|
||||||
|
except AttributeError: # Python <=2.6
|
||||||
|
def _etree_iter(root):
|
||||||
|
for el in root.findall('*'):
|
||||||
|
yield el
|
||||||
|
for sub in _etree_iter(el):
|
||||||
|
yield sub
|
||||||
|
|
||||||
|
# on 2.6 XML doesn't have a parser argument, function copied from CPython
|
||||||
|
# 2.7 source
|
||||||
|
def _XML(text, parser=None):
|
||||||
|
if not parser:
|
||||||
|
parser = etree.XMLParser(target=etree.TreeBuilder())
|
||||||
|
parser.feed(text)
|
||||||
|
return parser.close()
|
||||||
|
|
||||||
|
def _element_factory(*args, **kwargs):
|
||||||
|
el = etree.Element(*args, **kwargs)
|
||||||
|
for k, v in el.items():
|
||||||
|
if isinstance(v, bytes):
|
||||||
|
el.set(k, v.decode('utf-8'))
|
||||||
|
return el
|
||||||
|
|
||||||
|
def compat_etree_fromstring(text):
|
||||||
|
doc = _XML(text, parser=etree.XMLParser(target=etree.TreeBuilder(element_factory=_element_factory)))
|
||||||
|
for el in _etree_iter(doc):
|
||||||
|
if el.text is not None and isinstance(el.text, bytes):
|
||||||
|
el.text = el.text.decode('utf-8')
|
||||||
|
return doc
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from urllib.parse import parse_qs as compat_parse_qs
|
from urllib.parse import parse_qs as compat_parse_qs
|
||||||
@@ -192,6 +308,17 @@ except ImportError: # Python < 3.3
|
|||||||
return "'" + s.replace("'", "'\"'\"'") + "'"
|
return "'" + s.replace("'", "'\"'\"'") + "'"
|
||||||
|
|
||||||
|
|
||||||
|
if sys.version_info >= (2, 7, 3):
|
||||||
|
compat_shlex_split = shlex.split
|
||||||
|
else:
|
||||||
|
# Working around shlex issue with unicode strings on some python 2
|
||||||
|
# versions (see http://bugs.python.org/issue1548891)
|
||||||
|
def compat_shlex_split(s, comments=False, posix=True):
|
||||||
|
if isinstance(s, compat_str):
|
||||||
|
s = s.encode('utf-8')
|
||||||
|
return shlex.split(s, comments, posix)
|
||||||
|
|
||||||
|
|
||||||
def compat_ord(c):
|
def compat_ord(c):
|
||||||
if type(c) is int:
|
if type(c) is int:
|
||||||
return c
|
return c
|
||||||
@@ -306,7 +433,7 @@ if sys.version_info < (3, 0) and sys.platform == 'win32':
|
|||||||
else:
|
else:
|
||||||
compat_getpass = getpass.getpass
|
compat_getpass = getpass.getpass
|
||||||
|
|
||||||
# Old 2.6 and 2.7 releases require kwargs to be bytes
|
# Python < 2.6.5 require kwargs to be bytes
|
||||||
try:
|
try:
|
||||||
def _testfunc(x):
|
def _testfunc(x):
|
||||||
pass
|
pass
|
||||||
@@ -364,31 +491,87 @@ def workaround_optparse_bug9161():
|
|||||||
return real_add_option(self, *bargs, **bkwargs)
|
return real_add_option(self, *bargs, **bkwargs)
|
||||||
optparse.OptionGroup.add_option = _compat_add_option
|
optparse.OptionGroup.add_option = _compat_add_option
|
||||||
|
|
||||||
|
if hasattr(shutil, 'get_terminal_size'): # Python >= 3.3
|
||||||
|
compat_get_terminal_size = shutil.get_terminal_size
|
||||||
|
else:
|
||||||
|
_terminal_size = collections.namedtuple('terminal_size', ['columns', 'lines'])
|
||||||
|
|
||||||
|
def compat_get_terminal_size(fallback=(80, 24)):
|
||||||
|
columns = compat_getenv('COLUMNS')
|
||||||
|
if columns:
|
||||||
|
columns = int(columns)
|
||||||
|
else:
|
||||||
|
columns = None
|
||||||
|
lines = compat_getenv('LINES')
|
||||||
|
if lines:
|
||||||
|
lines = int(lines)
|
||||||
|
else:
|
||||||
|
lines = None
|
||||||
|
|
||||||
|
if columns is None or lines is None or columns <= 0 or lines <= 0:
|
||||||
|
try:
|
||||||
|
sp = subprocess.Popen(
|
||||||
|
['stty', 'size'],
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
out, err = sp.communicate()
|
||||||
|
_lines, _columns = map(int, out.split())
|
||||||
|
except Exception:
|
||||||
|
_columns, _lines = _terminal_size(*fallback)
|
||||||
|
|
||||||
|
if columns is None or columns <= 0:
|
||||||
|
columns = _columns
|
||||||
|
if lines is None or lines <= 0:
|
||||||
|
lines = _lines
|
||||||
|
return _terminal_size(columns, lines)
|
||||||
|
|
||||||
|
try:
|
||||||
|
itertools.count(start=0, step=1)
|
||||||
|
compat_itertools_count = itertools.count
|
||||||
|
except TypeError: # Python 2.6
|
||||||
|
def compat_itertools_count(start=0, step=1):
|
||||||
|
n = start
|
||||||
|
while True:
|
||||||
|
yield n
|
||||||
|
n += step
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 0):
|
||||||
|
from tokenize import tokenize as compat_tokenize_tokenize
|
||||||
|
else:
|
||||||
|
from tokenize import generate_tokens as compat_tokenize_tokenize
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'compat_HTTPError',
|
'compat_HTTPError',
|
||||||
'compat_basestring',
|
'compat_basestring',
|
||||||
'compat_chr',
|
'compat_chr',
|
||||||
'compat_cookiejar',
|
'compat_cookiejar',
|
||||||
|
'compat_cookies',
|
||||||
|
'compat_etree_fromstring',
|
||||||
'compat_expanduser',
|
'compat_expanduser',
|
||||||
|
'compat_get_terminal_size',
|
||||||
'compat_getenv',
|
'compat_getenv',
|
||||||
'compat_getpass',
|
'compat_getpass',
|
||||||
'compat_html_entities',
|
'compat_html_entities',
|
||||||
'compat_html_parser',
|
|
||||||
'compat_http_client',
|
'compat_http_client',
|
||||||
'compat_http_server',
|
'compat_http_server',
|
||||||
|
'compat_itertools_count',
|
||||||
'compat_kwargs',
|
'compat_kwargs',
|
||||||
'compat_ord',
|
'compat_ord',
|
||||||
'compat_parse_qs',
|
'compat_parse_qs',
|
||||||
'compat_print',
|
'compat_print',
|
||||||
|
'compat_shlex_split',
|
||||||
'compat_socket_create_connection',
|
'compat_socket_create_connection',
|
||||||
'compat_str',
|
'compat_str',
|
||||||
'compat_subprocess_get_DEVNULL',
|
'compat_subprocess_get_DEVNULL',
|
||||||
|
'compat_tokenize_tokenize',
|
||||||
'compat_urllib_error',
|
'compat_urllib_error',
|
||||||
'compat_urllib_parse',
|
'compat_urllib_parse',
|
||||||
'compat_urllib_parse_unquote',
|
'compat_urllib_parse_unquote',
|
||||||
|
'compat_urllib_parse_unquote_plus',
|
||||||
|
'compat_urllib_parse_unquote_to_bytes',
|
||||||
'compat_urllib_parse_urlparse',
|
'compat_urllib_parse_urlparse',
|
||||||
'compat_urllib_request',
|
'compat_urllib_request',
|
||||||
|
'compat_urllib_request_DataHandler',
|
||||||
|
'compat_urllib_response',
|
||||||
'compat_urlparse',
|
'compat_urlparse',
|
||||||
'compat_urlretrieve',
|
'compat_urlretrieve',
|
||||||
'compat_xml_parse_error',
|
'compat_xml_parse_error',
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ from .f4m import F4mFD
|
|||||||
from .hls import HlsFD
|
from .hls import HlsFD
|
||||||
from .hls import NativeHlsFD
|
from .hls import NativeHlsFD
|
||||||
from .http import HttpFD
|
from .http import HttpFD
|
||||||
from .mplayer import MplayerFD
|
from .rtsp import RtspFD
|
||||||
from .rtmp import RtmpFD
|
from .rtmp import RtmpFD
|
||||||
|
from .dash import DashSegmentsFD
|
||||||
|
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
determine_protocol,
|
determine_protocol,
|
||||||
@@ -17,9 +18,10 @@ PROTOCOL_MAP = {
|
|||||||
'rtmp': RtmpFD,
|
'rtmp': RtmpFD,
|
||||||
'm3u8_native': NativeHlsFD,
|
'm3u8_native': NativeHlsFD,
|
||||||
'm3u8': HlsFD,
|
'm3u8': HlsFD,
|
||||||
'mms': MplayerFD,
|
'mms': RtspFD,
|
||||||
'rtsp': MplayerFD,
|
'rtsp': RtspFD,
|
||||||
'f4m': F4mFD,
|
'f4m': F4mFD,
|
||||||
|
'http_dash_segments': DashSegmentsFD,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from ..compat import compat_str
|
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
encodeFilename,
|
encodeFilename,
|
||||||
|
error_to_compat_str,
|
||||||
|
decodeArgument,
|
||||||
format_bytes,
|
format_bytes,
|
||||||
timeconvert,
|
timeconvert,
|
||||||
)
|
)
|
||||||
@@ -41,7 +42,10 @@ class FileDownloader(object):
|
|||||||
min_filesize: Skip files smaller than this size
|
min_filesize: Skip files smaller than this size
|
||||||
max_filesize: Skip files larger than this size
|
max_filesize: Skip files larger than this size
|
||||||
xattr_set_filesize: Set ytdl.filesize user xattribute with expected size.
|
xattr_set_filesize: Set ytdl.filesize user xattribute with expected size.
|
||||||
(experimenatal)
|
(experimental)
|
||||||
|
external_downloader_args: A list of additional command-line arguments for the
|
||||||
|
external downloader.
|
||||||
|
hls_use_mpegts: Use the mpegts container for HLS videos.
|
||||||
|
|
||||||
Subclasses of this one must re-define the real_download method.
|
Subclasses of this one must re-define the real_download method.
|
||||||
"""
|
"""
|
||||||
@@ -183,7 +187,7 @@ class FileDownloader(object):
|
|||||||
return
|
return
|
||||||
os.rename(encodeFilename(old_filename), encodeFilename(new_filename))
|
os.rename(encodeFilename(old_filename), encodeFilename(new_filename))
|
||||||
except (IOError, OSError) as err:
|
except (IOError, OSError) as err:
|
||||||
self.report_error('unable to rename file: %s' % compat_str(err))
|
self.report_error('unable to rename file: %s' % error_to_compat_str(err))
|
||||||
|
|
||||||
def try_utime(self, filename, last_modified_hdr):
|
def try_utime(self, filename, last_modified_hdr):
|
||||||
"""Try to set the last-modified time of the given file."""
|
"""Try to set the last-modified time of the given file."""
|
||||||
@@ -202,7 +206,7 @@ class FileDownloader(object):
|
|||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
os.utime(filename, (time.time(), filetime))
|
os.utime(filename, (time.time(), filetime))
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return filetime
|
return filetime
|
||||||
|
|
||||||
@@ -292,7 +296,7 @@ class FileDownloader(object):
|
|||||||
|
|
||||||
def report_retry(self, count, retries):
|
def report_retry(self, count, retries):
|
||||||
"""Report retry in case of HTTP error 5xx"""
|
"""Report retry in case of HTTP error 5xx"""
|
||||||
self.to_screen('[download] Got server HTTP error. Retrying (attempt %d of %d)...' % (count, retries))
|
self.to_screen('[download] Got server HTTP error. Retrying (attempt %d of %.0f)...' % (count, retries))
|
||||||
|
|
||||||
def report_file_already_downloaded(self, file_name):
|
def report_file_already_downloaded(self, file_name):
|
||||||
"""Report file has already been fully downloaded."""
|
"""Report file has already been fully downloaded."""
|
||||||
@@ -311,18 +315,18 @@ class FileDownloader(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
nooverwrites_and_exists = (
|
nooverwrites_and_exists = (
|
||||||
self.params.get('nooverwrites', False)
|
self.params.get('nooverwrites', False) and
|
||||||
and os.path.exists(encodeFilename(filename))
|
os.path.exists(encodeFilename(filename))
|
||||||
)
|
)
|
||||||
|
|
||||||
continuedl_and_exists = (
|
continuedl_and_exists = (
|
||||||
self.params.get('continuedl', False)
|
self.params.get('continuedl', True) and
|
||||||
and os.path.isfile(encodeFilename(filename))
|
os.path.isfile(encodeFilename(filename)) and
|
||||||
and not self.params.get('nopart', False)
|
not self.params.get('nopart', False)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check file already present
|
# Check file already present
|
||||||
if filename != '-' and nooverwrites_and_exists or continuedl_and_exists:
|
if filename != '-' and (nooverwrites_and_exists or continuedl_and_exists):
|
||||||
self.report_file_already_downloaded(filename)
|
self.report_file_already_downloaded(filename)
|
||||||
self._hook_progress({
|
self._hook_progress({
|
||||||
'filename': filename,
|
'filename': filename,
|
||||||
@@ -351,19 +355,15 @@ class FileDownloader(object):
|
|||||||
# this interface
|
# this interface
|
||||||
self._progress_hooks.append(ph)
|
self._progress_hooks.append(ph)
|
||||||
|
|
||||||
def _debug_cmd(self, args, subprocess_encoding, exe=None):
|
def _debug_cmd(self, args, exe=None):
|
||||||
if not self.params.get('verbose', False):
|
if not self.params.get('verbose', False):
|
||||||
return
|
return
|
||||||
|
|
||||||
if exe is None:
|
str_args = [decodeArgument(a) for a in args]
|
||||||
exe = os.path.basename(args[0])
|
|
||||||
|
if exe is None:
|
||||||
|
exe = os.path.basename(str_args[0])
|
||||||
|
|
||||||
if subprocess_encoding:
|
|
||||||
str_args = [
|
|
||||||
a.decode(subprocess_encoding) if isinstance(a, bytes) else a
|
|
||||||
for a in args]
|
|
||||||
else:
|
|
||||||
str_args = args
|
|
||||||
try:
|
try:
|
||||||
import pipes
|
import pipes
|
||||||
shell_quote = lambda args: ' '.join(map(pipes.quote, str_args))
|
shell_quote = lambda args: ' '.join(map(pipes.quote, str_args))
|
||||||
|
|||||||
67
youtube_dl/downloader/dash.py
Normal file
67
youtube_dl/downloader/dash.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import FileDownloader
|
||||||
|
from ..utils import sanitized_Request
|
||||||
|
|
||||||
|
|
||||||
|
class DashSegmentsFD(FileDownloader):
|
||||||
|
"""
|
||||||
|
Download segments in a DASH manifest
|
||||||
|
"""
|
||||||
|
def real_download(self, filename, info_dict):
|
||||||
|
self.report_destination(filename)
|
||||||
|
tmpfilename = self.temp_name(filename)
|
||||||
|
base_url = info_dict['url']
|
||||||
|
segment_urls = info_dict['segment_urls']
|
||||||
|
|
||||||
|
is_test = self.params.get('test', False)
|
||||||
|
remaining_bytes = self._TEST_FILE_SIZE if is_test else None
|
||||||
|
byte_counter = 0
|
||||||
|
|
||||||
|
def append_url_to_file(outf, target_url, target_name, remaining_bytes=None):
|
||||||
|
self.to_screen('[DashSegments] %s: Downloading %s' % (info_dict['id'], target_name))
|
||||||
|
req = sanitized_Request(target_url)
|
||||||
|
if remaining_bytes is not None:
|
||||||
|
req.add_header('Range', 'bytes=0-%d' % (remaining_bytes - 1))
|
||||||
|
|
||||||
|
data = self.ydl.urlopen(req).read()
|
||||||
|
|
||||||
|
if remaining_bytes is not None:
|
||||||
|
data = data[:remaining_bytes]
|
||||||
|
|
||||||
|
outf.write(data)
|
||||||
|
return len(data)
|
||||||
|
|
||||||
|
def combine_url(base_url, target_url):
|
||||||
|
if re.match(r'^https?://', target_url):
|
||||||
|
return target_url
|
||||||
|
return '%s%s%s' % (base_url, '' if base_url.endswith('/') else '/', target_url)
|
||||||
|
|
||||||
|
with open(tmpfilename, 'wb') as outf:
|
||||||
|
if info_dict.get('initialization_url'):
|
||||||
|
append_url_to_file(
|
||||||
|
outf, combine_url(base_url, info_dict['initialization_url']),
|
||||||
|
'initialization segment')
|
||||||
|
for i, segment_url in enumerate(segment_urls):
|
||||||
|
segment_len = append_url_to_file(
|
||||||
|
outf, combine_url(base_url, segment_url),
|
||||||
|
'segment %d / %d' % (i + 1, len(segment_urls)),
|
||||||
|
remaining_bytes)
|
||||||
|
byte_counter += segment_len
|
||||||
|
if remaining_bytes is not None:
|
||||||
|
remaining_bytes -= segment_len
|
||||||
|
if remaining_bytes <= 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
self.try_rename(tmpfilename, filename)
|
||||||
|
|
||||||
|
self._hook_progress({
|
||||||
|
'downloaded_bytes': byte_counter,
|
||||||
|
'total_bytes': byte_counter,
|
||||||
|
'filename': filename,
|
||||||
|
'status': 'finished',
|
||||||
|
})
|
||||||
|
|
||||||
|
return True
|
||||||
@@ -2,11 +2,15 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
|
||||||
|
|
||||||
from .common import FileDownloader
|
from .common import FileDownloader
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
cli_option,
|
||||||
|
cli_valueless_option,
|
||||||
|
cli_bool_option,
|
||||||
|
cli_configuration_args,
|
||||||
encodeFilename,
|
encodeFilename,
|
||||||
|
encodeArgument,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -45,25 +49,23 @@ class ExternalFD(FileDownloader):
|
|||||||
def supports(cls, info_dict):
|
def supports(cls, info_dict):
|
||||||
return info_dict['protocol'] in ('http', 'https', 'ftp', 'ftps')
|
return info_dict['protocol'] in ('http', 'https', 'ftp', 'ftps')
|
||||||
|
|
||||||
def _source_address(self, command_option):
|
def _option(self, command_option, param):
|
||||||
source_address = self.params.get('source_address')
|
return cli_option(self.params, command_option, param)
|
||||||
if source_address is None:
|
|
||||||
return []
|
def _bool_option(self, command_option, param, true_value='true', false_value='false', separator=None):
|
||||||
return [command_option, source_address]
|
return cli_bool_option(self.params, command_option, param, true_value, false_value, separator)
|
||||||
|
|
||||||
|
def _valueless_option(self, command_option, param, expected_value=True):
|
||||||
|
return cli_valueless_option(self.params, command_option, param, expected_value)
|
||||||
|
|
||||||
|
def _configuration_args(self, default=[]):
|
||||||
|
return cli_configuration_args(self.params, 'external_downloader_args', default)
|
||||||
|
|
||||||
def _call_downloader(self, tmpfilename, info_dict):
|
def _call_downloader(self, tmpfilename, info_dict):
|
||||||
""" Either overwrite this or implement _make_cmd """
|
""" Either overwrite this or implement _make_cmd """
|
||||||
cmd = self._make_cmd(tmpfilename, info_dict)
|
cmd = [encodeArgument(a) for a in self._make_cmd(tmpfilename, info_dict)]
|
||||||
|
|
||||||
if sys.platform == 'win32' and sys.version_info < (3, 0):
|
self._debug_cmd(cmd)
|
||||||
# Windows subprocess module does not actually support Unicode
|
|
||||||
# on Python 2.x
|
|
||||||
# See http://stackoverflow.com/a/9951851/35070
|
|
||||||
subprocess_encoding = sys.getfilesystemencoding()
|
|
||||||
cmd = [a.encode(subprocess_encoding, 'ignore') for a in cmd]
|
|
||||||
else:
|
|
||||||
subprocess_encoding = None
|
|
||||||
self._debug_cmd(cmd, subprocess_encoding)
|
|
||||||
|
|
||||||
p = subprocess.Popen(
|
p = subprocess.Popen(
|
||||||
cmd, stderr=subprocess.PIPE)
|
cmd, stderr=subprocess.PIPE)
|
||||||
@@ -78,7 +80,20 @@ class CurlFD(ExternalFD):
|
|||||||
cmd = [self.exe, '--location', '-o', tmpfilename]
|
cmd = [self.exe, '--location', '-o', tmpfilename]
|
||||||
for key, val in info_dict['http_headers'].items():
|
for key, val in info_dict['http_headers'].items():
|
||||||
cmd += ['--header', '%s: %s' % (key, val)]
|
cmd += ['--header', '%s: %s' % (key, val)]
|
||||||
cmd += self._source_address('--interface')
|
cmd += self._option('--interface', 'source_address')
|
||||||
|
cmd += self._option('--proxy', 'proxy')
|
||||||
|
cmd += self._valueless_option('--insecure', 'nocheckcertificate')
|
||||||
|
cmd += self._configuration_args()
|
||||||
|
cmd += ['--', info_dict['url']]
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
|
class AxelFD(ExternalFD):
|
||||||
|
def _make_cmd(self, tmpfilename, info_dict):
|
||||||
|
cmd = [self.exe, '-o', tmpfilename]
|
||||||
|
for key, val in info_dict['http_headers'].items():
|
||||||
|
cmd += ['-H', '%s: %s' % (key, val)]
|
||||||
|
cmd += self._configuration_args()
|
||||||
cmd += ['--', info_dict['url']]
|
cmd += ['--', info_dict['url']]
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
@@ -88,26 +103,39 @@ class WgetFD(ExternalFD):
|
|||||||
cmd = [self.exe, '-O', tmpfilename, '-nv', '--no-cookies']
|
cmd = [self.exe, '-O', tmpfilename, '-nv', '--no-cookies']
|
||||||
for key, val in info_dict['http_headers'].items():
|
for key, val in info_dict['http_headers'].items():
|
||||||
cmd += ['--header', '%s: %s' % (key, val)]
|
cmd += ['--header', '%s: %s' % (key, val)]
|
||||||
cmd += self._source_address('--bind-address')
|
cmd += self._option('--bind-address', 'source_address')
|
||||||
|
cmd += self._option('--proxy', 'proxy')
|
||||||
|
cmd += self._valueless_option('--no-check-certificate', 'nocheckcertificate')
|
||||||
|
cmd += self._configuration_args()
|
||||||
cmd += ['--', info_dict['url']]
|
cmd += ['--', info_dict['url']]
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
class Aria2cFD(ExternalFD):
|
class Aria2cFD(ExternalFD):
|
||||||
def _make_cmd(self, tmpfilename, info_dict):
|
def _make_cmd(self, tmpfilename, info_dict):
|
||||||
cmd = [
|
cmd = [self.exe, '-c']
|
||||||
self.exe, '-c',
|
cmd += self._configuration_args([
|
||||||
'--min-split-size', '1M', '--max-connection-per-server', '4']
|
'--min-split-size', '1M', '--max-connection-per-server', '4'])
|
||||||
dn = os.path.dirname(tmpfilename)
|
dn = os.path.dirname(tmpfilename)
|
||||||
if dn:
|
if dn:
|
||||||
cmd += ['--dir', dn]
|
cmd += ['--dir', dn]
|
||||||
cmd += ['--out', os.path.basename(tmpfilename)]
|
cmd += ['--out', os.path.basename(tmpfilename)]
|
||||||
for key, val in info_dict['http_headers'].items():
|
for key, val in info_dict['http_headers'].items():
|
||||||
cmd += ['--header', '%s: %s' % (key, val)]
|
cmd += ['--header', '%s: %s' % (key, val)]
|
||||||
cmd += self._source_address('--interface')
|
cmd += self._option('--interface', 'source_address')
|
||||||
|
cmd += self._option('--all-proxy', 'proxy')
|
||||||
|
cmd += self._bool_option('--check-certificate', 'nocheckcertificate', 'false', 'true', '=')
|
||||||
cmd += ['--', info_dict['url']]
|
cmd += ['--', info_dict['url']]
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
|
class HttpieFD(ExternalFD):
|
||||||
|
def _make_cmd(self, tmpfilename, info_dict):
|
||||||
|
cmd = ['http', '--download', '--output', tmpfilename, info_dict['url']]
|
||||||
|
for key, val in info_dict['http_headers'].items():
|
||||||
|
cmd += ['%s:%s' % (key, val)]
|
||||||
|
return cmd
|
||||||
|
|
||||||
_BY_NAME = dict(
|
_BY_NAME = dict(
|
||||||
(klass.get_basename(), klass)
|
(klass.get_basename(), klass)
|
||||||
for name, klass in globals().items()
|
for name, klass in globals().items()
|
||||||
@@ -122,5 +150,6 @@ def list_external_downloaders():
|
|||||||
def get_external_downloader(external_downloader):
|
def get_external_downloader(external_downloader):
|
||||||
""" Given the name of the executable, see whether we support the given
|
""" Given the name of the executable, see whether we support the given
|
||||||
downloader . """
|
downloader . """
|
||||||
bn = os.path.basename(external_downloader)
|
# Drop .exe extension on Windows
|
||||||
|
bn = os.path.splitext(os.path.basename(external_downloader))[0]
|
||||||
return _BY_NAME[bn]
|
return _BY_NAME[bn]
|
||||||
|
|||||||
@@ -5,18 +5,20 @@ import io
|
|||||||
import itertools
|
import itertools
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import xml.etree.ElementTree as etree
|
|
||||||
|
|
||||||
from .common import FileDownloader
|
from .fragment import FragmentFD
|
||||||
from .http import HttpFD
|
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
|
compat_etree_fromstring,
|
||||||
compat_urlparse,
|
compat_urlparse,
|
||||||
|
compat_urllib_error,
|
||||||
|
compat_urllib_parse_urlparse,
|
||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
encodeFilename,
|
||||||
|
fix_xml_ampersands,
|
||||||
|
sanitize_open,
|
||||||
struct_pack,
|
struct_pack,
|
||||||
struct_unpack,
|
struct_unpack,
|
||||||
encodeFilename,
|
|
||||||
sanitize_open,
|
|
||||||
xpath_text,
|
xpath_text,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -121,7 +123,8 @@ class FlvReader(io.BytesIO):
|
|||||||
|
|
||||||
self.read_unsigned_int() # BootstrapinfoVersion
|
self.read_unsigned_int() # BootstrapinfoVersion
|
||||||
# Profile,Live,Update,Reserved
|
# Profile,Live,Update,Reserved
|
||||||
self.read(1)
|
flags = self.read_unsigned_char()
|
||||||
|
live = flags & 0x20 != 0
|
||||||
# time scale
|
# time scale
|
||||||
self.read_unsigned_int()
|
self.read_unsigned_int()
|
||||||
# CurrentMediaTime
|
# CurrentMediaTime
|
||||||
@@ -160,6 +163,7 @@ class FlvReader(io.BytesIO):
|
|||||||
return {
|
return {
|
||||||
'segments': segments,
|
'segments': segments,
|
||||||
'fragments': fragments,
|
'fragments': fragments,
|
||||||
|
'live': live,
|
||||||
}
|
}
|
||||||
|
|
||||||
def read_bootstrap_info(self):
|
def read_bootstrap_info(self):
|
||||||
@@ -182,6 +186,10 @@ def build_fragments_list(boot_info):
|
|||||||
for segment, fragments_count in segment_run_table['segment_run']:
|
for segment, fragments_count in segment_run_table['segment_run']:
|
||||||
for _ in range(fragments_count):
|
for _ in range(fragments_count):
|
||||||
res.append((segment, next(fragments_counter)))
|
res.append((segment, next(fragments_counter)))
|
||||||
|
|
||||||
|
if boot_info['live']:
|
||||||
|
res = res[-2:]
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
@@ -219,16 +227,13 @@ def _add_ns(prop):
|
|||||||
return '{http://ns.adobe.com/f4m/1.0}%s' % prop
|
return '{http://ns.adobe.com/f4m/1.0}%s' % prop
|
||||||
|
|
||||||
|
|
||||||
class HttpQuietDownloader(HttpFD):
|
class F4mFD(FragmentFD):
|
||||||
def to_screen(self, *args, **kargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class F4mFD(FileDownloader):
|
|
||||||
"""
|
"""
|
||||||
A downloader for f4m manifests or AdobeHDS.
|
A downloader for f4m manifests or AdobeHDS.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
FD_NAME = 'f4m'
|
||||||
|
|
||||||
def _get_unencrypted_media(self, doc):
|
def _get_unencrypted_media(self, doc):
|
||||||
media = doc.findall(_add_ns('media'))
|
media = doc.findall(_add_ns('media'))
|
||||||
if not media:
|
if not media:
|
||||||
@@ -246,13 +251,56 @@ class F4mFD(FileDownloader):
|
|||||||
self.report_error('Unsupported DRM')
|
self.report_error('Unsupported DRM')
|
||||||
return media
|
return media
|
||||||
|
|
||||||
|
def _get_bootstrap_from_url(self, bootstrap_url):
|
||||||
|
bootstrap = self.ydl.urlopen(bootstrap_url).read()
|
||||||
|
return read_bootstrap_info(bootstrap)
|
||||||
|
|
||||||
|
def _update_live_fragments(self, bootstrap_url, latest_fragment):
|
||||||
|
fragments_list = []
|
||||||
|
retries = 30
|
||||||
|
while (not fragments_list) and (retries > 0):
|
||||||
|
boot_info = self._get_bootstrap_from_url(bootstrap_url)
|
||||||
|
fragments_list = build_fragments_list(boot_info)
|
||||||
|
fragments_list = [f for f in fragments_list if f[1] > latest_fragment]
|
||||||
|
if not fragments_list:
|
||||||
|
# Retry after a while
|
||||||
|
time.sleep(5.0)
|
||||||
|
retries -= 1
|
||||||
|
|
||||||
|
if not fragments_list:
|
||||||
|
self.report_error('Failed to update fragments')
|
||||||
|
|
||||||
|
return fragments_list
|
||||||
|
|
||||||
|
def _parse_bootstrap_node(self, node, base_url):
|
||||||
|
# Sometimes non empty inline bootstrap info can be specified along
|
||||||
|
# with bootstrap url attribute (e.g. dummy inline bootstrap info
|
||||||
|
# contains whitespace characters in [1]). We will prefer bootstrap
|
||||||
|
# url over inline bootstrap info when present.
|
||||||
|
# 1. http://live-1-1.rutube.ru/stream/1024/HDS/SD/C2NKsS85HQNckgn5HdEmOQ/1454167650/S-s604419906/move/four/dirs/upper/1024-576p.f4m
|
||||||
|
bootstrap_url = node.get('url')
|
||||||
|
if bootstrap_url:
|
||||||
|
bootstrap_url = compat_urlparse.urljoin(
|
||||||
|
base_url, bootstrap_url)
|
||||||
|
boot_info = self._get_bootstrap_from_url(bootstrap_url)
|
||||||
|
else:
|
||||||
|
bootstrap_url = None
|
||||||
|
bootstrap = base64.b64decode(node.text.encode('ascii'))
|
||||||
|
boot_info = read_bootstrap_info(bootstrap)
|
||||||
|
return boot_info, bootstrap_url
|
||||||
|
|
||||||
def real_download(self, filename, info_dict):
|
def real_download(self, filename, info_dict):
|
||||||
man_url = info_dict['url']
|
man_url = info_dict['url']
|
||||||
requested_bitrate = info_dict.get('tbr')
|
requested_bitrate = info_dict.get('tbr')
|
||||||
self.to_screen('[download] Downloading f4m manifest')
|
self.to_screen('[%s] Downloading f4m manifest' % self.FD_NAME)
|
||||||
manifest = self.ydl.urlopen(man_url).read()
|
urlh = self.ydl.urlopen(man_url)
|
||||||
|
man_url = urlh.geturl()
|
||||||
|
# Some manifests may be malformed, e.g. prosiebensat1 generated manifests
|
||||||
|
# (see https://github.com/rg3/youtube-dl/issues/6215#issuecomment-121704244
|
||||||
|
# and https://github.com/rg3/youtube-dl/issues/7823)
|
||||||
|
manifest = fix_xml_ampersands(urlh.read().decode('utf-8', 'ignore')).strip()
|
||||||
|
|
||||||
doc = etree.fromstring(manifest)
|
doc = compat_etree_fromstring(manifest)
|
||||||
formats = [(int(f.attrib.get('bitrate', -1)), f)
|
formats = [(int(f.attrib.get('bitrate', -1)), f)
|
||||||
for f in self._get_unencrypted_media(doc)]
|
for f in self._get_unencrypted_media(doc)]
|
||||||
if requested_bitrate is None:
|
if requested_bitrate is None:
|
||||||
@@ -265,122 +313,91 @@ class F4mFD(FileDownloader):
|
|||||||
|
|
||||||
base_url = compat_urlparse.urljoin(man_url, media.attrib['url'])
|
base_url = compat_urlparse.urljoin(man_url, media.attrib['url'])
|
||||||
bootstrap_node = doc.find(_add_ns('bootstrapInfo'))
|
bootstrap_node = doc.find(_add_ns('bootstrapInfo'))
|
||||||
if bootstrap_node.text is None:
|
boot_info, bootstrap_url = self._parse_bootstrap_node(bootstrap_node, base_url)
|
||||||
bootstrap_url = compat_urlparse.urljoin(
|
live = boot_info['live']
|
||||||
base_url, bootstrap_node.attrib['url'])
|
|
||||||
bootstrap = self.ydl.urlopen(bootstrap_url).read()
|
|
||||||
else:
|
|
||||||
bootstrap = base64.b64decode(bootstrap_node.text)
|
|
||||||
metadata_node = media.find(_add_ns('metadata'))
|
metadata_node = media.find(_add_ns('metadata'))
|
||||||
if metadata_node is not None:
|
if metadata_node is not None:
|
||||||
metadata = base64.b64decode(metadata_node.text)
|
metadata = base64.b64decode(metadata_node.text.encode('ascii'))
|
||||||
else:
|
else:
|
||||||
metadata = None
|
metadata = None
|
||||||
boot_info = read_bootstrap_info(bootstrap)
|
|
||||||
|
|
||||||
fragments_list = build_fragments_list(boot_info)
|
fragments_list = build_fragments_list(boot_info)
|
||||||
if self.params.get('test', False):
|
test = self.params.get('test', False)
|
||||||
|
if test:
|
||||||
# We only download the first fragment
|
# We only download the first fragment
|
||||||
fragments_list = fragments_list[:1]
|
fragments_list = fragments_list[:1]
|
||||||
total_frags = len(fragments_list)
|
total_frags = len(fragments_list)
|
||||||
# For some akamai manifests we'll need to add a query to the fragment url
|
# For some akamai manifests we'll need to add a query to the fragment url
|
||||||
akamai_pv = xpath_text(doc, _add_ns('pv-2.0'))
|
akamai_pv = xpath_text(doc, _add_ns('pv-2.0'))
|
||||||
|
|
||||||
self.report_destination(filename)
|
ctx = {
|
||||||
http_dl = HttpQuietDownloader(
|
'filename': filename,
|
||||||
self.ydl,
|
'total_frags': total_frags,
|
||||||
{
|
'live': live,
|
||||||
'continuedl': True,
|
}
|
||||||
'quiet': True,
|
|
||||||
'noprogress': True,
|
self._prepare_frag_download(ctx)
|
||||||
'ratelimit': self.params.get('ratelimit', None),
|
|
||||||
'test': self.params.get('test', False),
|
dest_stream = ctx['dest_stream']
|
||||||
}
|
|
||||||
)
|
|
||||||
tmpfilename = self.temp_name(filename)
|
|
||||||
(dest_stream, tmpfilename) = sanitize_open(tmpfilename, 'wb')
|
|
||||||
|
|
||||||
write_flv_header(dest_stream)
|
write_flv_header(dest_stream)
|
||||||
write_metadata_tag(dest_stream, metadata)
|
if not live:
|
||||||
|
write_metadata_tag(dest_stream, metadata)
|
||||||
|
|
||||||
# This dict stores the download progress, it's updated by the progress
|
base_url_parsed = compat_urllib_parse_urlparse(base_url)
|
||||||
# hook
|
|
||||||
state = {
|
|
||||||
'status': 'downloading',
|
|
||||||
'downloaded_bytes': 0,
|
|
||||||
'frag_index': 0,
|
|
||||||
'frag_count': total_frags,
|
|
||||||
'filename': filename,
|
|
||||||
'tmpfilename': tmpfilename,
|
|
||||||
}
|
|
||||||
start = time.time()
|
|
||||||
|
|
||||||
def frag_progress_hook(s):
|
self._start_frag_download(ctx)
|
||||||
if s['status'] not in ('downloading', 'finished'):
|
|
||||||
return
|
|
||||||
|
|
||||||
frag_total_bytes = s.get('total_bytes', 0)
|
|
||||||
if s['status'] == 'finished':
|
|
||||||
state['downloaded_bytes'] += frag_total_bytes
|
|
||||||
state['frag_index'] += 1
|
|
||||||
|
|
||||||
estimated_size = (
|
|
||||||
(state['downloaded_bytes'] + frag_total_bytes)
|
|
||||||
/ (state['frag_index'] + 1) * total_frags)
|
|
||||||
time_now = time.time()
|
|
||||||
state['total_bytes_estimate'] = estimated_size
|
|
||||||
state['elapsed'] = time_now - start
|
|
||||||
|
|
||||||
if s['status'] == 'finished':
|
|
||||||
progress = self.calc_percent(state['frag_index'], total_frags)
|
|
||||||
else:
|
|
||||||
frag_downloaded_bytes = s['downloaded_bytes']
|
|
||||||
frag_progress = self.calc_percent(frag_downloaded_bytes,
|
|
||||||
frag_total_bytes)
|
|
||||||
progress = self.calc_percent(state['frag_index'], total_frags)
|
|
||||||
progress += frag_progress / float(total_frags)
|
|
||||||
|
|
||||||
state['eta'] = self.calc_eta(
|
|
||||||
start, time_now, estimated_size, state['downloaded_bytes'] + frag_downloaded_bytes)
|
|
||||||
state['speed'] = s.get('speed')
|
|
||||||
self._hook_progress(state)
|
|
||||||
|
|
||||||
http_dl.add_progress_hook(frag_progress_hook)
|
|
||||||
|
|
||||||
frags_filenames = []
|
frags_filenames = []
|
||||||
for (seg_i, frag_i) in fragments_list:
|
while fragments_list:
|
||||||
|
seg_i, frag_i = fragments_list.pop(0)
|
||||||
name = 'Seg%d-Frag%d' % (seg_i, frag_i)
|
name = 'Seg%d-Frag%d' % (seg_i, frag_i)
|
||||||
url = base_url + name
|
query = []
|
||||||
|
if base_url_parsed.query:
|
||||||
|
query.append(base_url_parsed.query)
|
||||||
if akamai_pv:
|
if akamai_pv:
|
||||||
url += '?' + akamai_pv.strip(';')
|
query.append(akamai_pv.strip(';'))
|
||||||
frag_filename = '%s-%s' % (tmpfilename, name)
|
if info_dict.get('extra_param_to_segment_url'):
|
||||||
success = http_dl.download(frag_filename, {'url': url})
|
query.append(info_dict['extra_param_to_segment_url'])
|
||||||
if not success:
|
url_parsed = base_url_parsed._replace(path=base_url_parsed.path + name, query='&'.join(query))
|
||||||
return False
|
frag_filename = '%s-%s' % (ctx['tmpfilename'], name)
|
||||||
with open(frag_filename, 'rb') as down:
|
try:
|
||||||
|
success = ctx['dl'].download(frag_filename, {'url': url_parsed.geturl()})
|
||||||
|
if not success:
|
||||||
|
return False
|
||||||
|
(down, frag_sanitized) = sanitize_open(frag_filename, 'rb')
|
||||||
down_data = down.read()
|
down_data = down.read()
|
||||||
|
down.close()
|
||||||
reader = FlvReader(down_data)
|
reader = FlvReader(down_data)
|
||||||
while True:
|
while True:
|
||||||
_, box_type, box_data = reader.read_box_info()
|
_, box_type, box_data = reader.read_box_info()
|
||||||
if box_type == b'mdat':
|
if box_type == b'mdat':
|
||||||
dest_stream.write(box_data)
|
dest_stream.write(box_data)
|
||||||
break
|
break
|
||||||
frags_filenames.append(frag_filename)
|
if live:
|
||||||
|
os.remove(encodeFilename(frag_sanitized))
|
||||||
|
else:
|
||||||
|
frags_filenames.append(frag_sanitized)
|
||||||
|
except (compat_urllib_error.HTTPError, ) as err:
|
||||||
|
if live and (err.code == 404 or err.code == 410):
|
||||||
|
# We didn't keep up with the live window. Continue
|
||||||
|
# with the next available fragment.
|
||||||
|
msg = 'Fragment %d unavailable' % frag_i
|
||||||
|
self.report_warning(msg)
|
||||||
|
fragments_list = []
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
dest_stream.close()
|
if not fragments_list and not test and live and bootstrap_url:
|
||||||
|
fragments_list = self._update_live_fragments(bootstrap_url, frag_i)
|
||||||
|
total_frags += len(fragments_list)
|
||||||
|
if fragments_list and (fragments_list[0][1] > frag_i + 1):
|
||||||
|
msg = 'Missed %d fragments' % (fragments_list[0][1] - (frag_i + 1))
|
||||||
|
self.report_warning(msg)
|
||||||
|
|
||||||
|
self._finish_frag_download(ctx)
|
||||||
|
|
||||||
elapsed = time.time() - start
|
|
||||||
self.try_rename(tmpfilename, filename)
|
|
||||||
for frag_file in frags_filenames:
|
for frag_file in frags_filenames:
|
||||||
os.remove(frag_file)
|
os.remove(encodeFilename(frag_file))
|
||||||
|
|
||||||
fsize = os.path.getsize(encodeFilename(filename))
|
|
||||||
self._hook_progress({
|
|
||||||
'downloaded_bytes': fsize,
|
|
||||||
'total_bytes': fsize,
|
|
||||||
'filename': filename,
|
|
||||||
'status': 'finished',
|
|
||||||
'elapsed': elapsed,
|
|
||||||
})
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|||||||
122
youtube_dl/downloader/fragment.py
Normal file
122
youtube_dl/downloader/fragment.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
from __future__ import division, unicode_literals
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
from .common import FileDownloader
|
||||||
|
from .http import HttpFD
|
||||||
|
from ..utils import (
|
||||||
|
encodeFilename,
|
||||||
|
sanitize_open,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HttpQuietDownloader(HttpFD):
|
||||||
|
def to_screen(self, *args, **kargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FragmentFD(FileDownloader):
|
||||||
|
"""
|
||||||
|
A base file downloader class for fragmented media (e.g. f4m/m3u8 manifests).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _prepare_and_start_frag_download(self, ctx):
|
||||||
|
self._prepare_frag_download(ctx)
|
||||||
|
self._start_frag_download(ctx)
|
||||||
|
|
||||||
|
def _prepare_frag_download(self, ctx):
|
||||||
|
if 'live' not in ctx:
|
||||||
|
ctx['live'] = False
|
||||||
|
self.to_screen(
|
||||||
|
'[%s] Total fragments: %s'
|
||||||
|
% (self.FD_NAME, ctx['total_frags'] if not ctx['live'] else 'unknown (live)'))
|
||||||
|
self.report_destination(ctx['filename'])
|
||||||
|
dl = HttpQuietDownloader(
|
||||||
|
self.ydl,
|
||||||
|
{
|
||||||
|
'continuedl': True,
|
||||||
|
'quiet': True,
|
||||||
|
'noprogress': True,
|
||||||
|
'ratelimit': self.params.get('ratelimit', None),
|
||||||
|
'retries': self.params.get('retries', 0),
|
||||||
|
'test': self.params.get('test', False),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
tmpfilename = self.temp_name(ctx['filename'])
|
||||||
|
dest_stream, tmpfilename = sanitize_open(tmpfilename, 'wb')
|
||||||
|
ctx.update({
|
||||||
|
'dl': dl,
|
||||||
|
'dest_stream': dest_stream,
|
||||||
|
'tmpfilename': tmpfilename,
|
||||||
|
})
|
||||||
|
|
||||||
|
def _start_frag_download(self, ctx):
|
||||||
|
total_frags = ctx['total_frags']
|
||||||
|
# This dict stores the download progress, it's updated by the progress
|
||||||
|
# hook
|
||||||
|
state = {
|
||||||
|
'status': 'downloading',
|
||||||
|
'downloaded_bytes': 0,
|
||||||
|
'frag_index': 0,
|
||||||
|
'frag_count': total_frags,
|
||||||
|
'filename': ctx['filename'],
|
||||||
|
'tmpfilename': ctx['tmpfilename'],
|
||||||
|
}
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
ctx.update({
|
||||||
|
'started': start,
|
||||||
|
# Total complete fragments downloaded so far in bytes
|
||||||
|
'complete_frags_downloaded_bytes': 0,
|
||||||
|
# Amount of fragment's bytes downloaded by the time of the previous
|
||||||
|
# frag progress hook invocation
|
||||||
|
'prev_frag_downloaded_bytes': 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
def frag_progress_hook(s):
|
||||||
|
if s['status'] not in ('downloading', 'finished'):
|
||||||
|
return
|
||||||
|
|
||||||
|
time_now = time.time()
|
||||||
|
state['elapsed'] = time_now - start
|
||||||
|
frag_total_bytes = s.get('total_bytes') or 0
|
||||||
|
if not ctx['live']:
|
||||||
|
estimated_size = (
|
||||||
|
(ctx['complete_frags_downloaded_bytes'] + frag_total_bytes) /
|
||||||
|
(state['frag_index'] + 1) * total_frags)
|
||||||
|
state['total_bytes_estimate'] = estimated_size
|
||||||
|
|
||||||
|
if s['status'] == 'finished':
|
||||||
|
state['frag_index'] += 1
|
||||||
|
state['downloaded_bytes'] += frag_total_bytes - ctx['prev_frag_downloaded_bytes']
|
||||||
|
ctx['complete_frags_downloaded_bytes'] = state['downloaded_bytes']
|
||||||
|
ctx['prev_frag_downloaded_bytes'] = 0
|
||||||
|
else:
|
||||||
|
frag_downloaded_bytes = s['downloaded_bytes']
|
||||||
|
state['downloaded_bytes'] += frag_downloaded_bytes - ctx['prev_frag_downloaded_bytes']
|
||||||
|
if not ctx['live']:
|
||||||
|
state['eta'] = self.calc_eta(
|
||||||
|
start, time_now, estimated_size,
|
||||||
|
state['downloaded_bytes'])
|
||||||
|
state['speed'] = s.get('speed')
|
||||||
|
ctx['prev_frag_downloaded_bytes'] = frag_downloaded_bytes
|
||||||
|
self._hook_progress(state)
|
||||||
|
|
||||||
|
ctx['dl'].add_progress_hook(frag_progress_hook)
|
||||||
|
|
||||||
|
return start
|
||||||
|
|
||||||
|
def _finish_frag_download(self, ctx):
|
||||||
|
ctx['dest_stream'].close()
|
||||||
|
elapsed = time.time() - ctx['started']
|
||||||
|
self.try_rename(ctx['tmpfilename'], ctx['filename'])
|
||||||
|
fsize = os.path.getsize(encodeFilename(ctx['filename']))
|
||||||
|
|
||||||
|
self._hook_progress({
|
||||||
|
'downloaded_bytes': fsize,
|
||||||
|
'total_bytes': fsize,
|
||||||
|
'filename': ctx['filename'],
|
||||||
|
'status': 'finished',
|
||||||
|
'elapsed': elapsed,
|
||||||
|
})
|
||||||
@@ -3,16 +3,18 @@ from __future__ import unicode_literals
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
from ..postprocessor.ffmpeg import FFmpegPostProcessor
|
|
||||||
from .common import FileDownloader
|
from .common import FileDownloader
|
||||||
from ..compat import (
|
from .fragment import FragmentFD
|
||||||
compat_urlparse,
|
|
||||||
compat_urllib_request,
|
from ..compat import compat_urlparse
|
||||||
)
|
from ..postprocessor.ffmpeg import FFmpegPostProcessor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
encodeArgument,
|
encodeArgument,
|
||||||
encodeFilename,
|
encodeFilename,
|
||||||
|
sanitize_open,
|
||||||
|
handle_youtubedl_headers,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -28,12 +30,39 @@ class HlsFD(FileDownloader):
|
|||||||
return False
|
return False
|
||||||
ffpp.check_version()
|
ffpp.check_version()
|
||||||
|
|
||||||
args = [
|
args = [ffpp.executable, '-y']
|
||||||
encodeArgument(opt)
|
|
||||||
for opt in (ffpp.executable, '-y', '-i', url, '-f', 'mp4', '-c', 'copy', '-bsf:a', 'aac_adtstoasc')]
|
|
||||||
args.append(encodeFilename(tmpfilename, True))
|
|
||||||
|
|
||||||
retval = subprocess.call(args)
|
if info_dict['http_headers'] and re.match(r'^https?://', url):
|
||||||
|
# Trailing \r\n after each HTTP header is important to prevent warning from ffmpeg/avconv:
|
||||||
|
# [http @ 00000000003d2fa0] No trailing CRLF found in HTTP header.
|
||||||
|
headers = handle_youtubedl_headers(info_dict['http_headers'])
|
||||||
|
args += [
|
||||||
|
'-headers',
|
||||||
|
''.join('%s: %s\r\n' % (key, val) for key, val in headers.items())]
|
||||||
|
|
||||||
|
args += ['-i', url, '-c', 'copy']
|
||||||
|
if self.params.get('hls_use_mpegts', False):
|
||||||
|
args += ['-f', 'mpegts']
|
||||||
|
else:
|
||||||
|
args += ['-f', 'mp4', '-bsf:a', 'aac_adtstoasc']
|
||||||
|
|
||||||
|
args = [encodeArgument(opt) for opt in args]
|
||||||
|
args.append(encodeFilename(ffpp._ffmpeg_filename_argument(tmpfilename), True))
|
||||||
|
|
||||||
|
self._debug_cmd(args)
|
||||||
|
|
||||||
|
proc = subprocess.Popen(args, stdin=subprocess.PIPE)
|
||||||
|
try:
|
||||||
|
retval = proc.wait()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
# subprocces.run would send the SIGKILL signal to ffmpeg and the
|
||||||
|
# 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
|
||||||
|
# streams). Note that Windows is not affected and produces playable
|
||||||
|
# files (see https://github.com/rg3/youtube-dl/issues/8300).
|
||||||
|
if sys.platform != 'win32':
|
||||||
|
proc.communicate(b'q')
|
||||||
|
raise
|
||||||
if retval == 0:
|
if retval == 0:
|
||||||
fsize = os.path.getsize(encodeFilename(tmpfilename))
|
fsize = os.path.getsize(encodeFilename(tmpfilename))
|
||||||
self.to_screen('\r[%s] %s bytes' % (args[0], fsize))
|
self.to_screen('\r[%s] %s bytes' % (args[0], fsize))
|
||||||
@@ -51,54 +80,51 @@ class HlsFD(FileDownloader):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class NativeHlsFD(FileDownloader):
|
class NativeHlsFD(FragmentFD):
|
||||||
""" A more limited implementation that does not require ffmpeg """
|
""" A more limited implementation that does not require ffmpeg """
|
||||||
|
|
||||||
def real_download(self, filename, info_dict):
|
FD_NAME = 'hlsnative'
|
||||||
url = info_dict['url']
|
|
||||||
self.report_destination(filename)
|
|
||||||
tmpfilename = self.temp_name(filename)
|
|
||||||
|
|
||||||
self.to_screen(
|
def real_download(self, filename, info_dict):
|
||||||
'[hlsnative] %s: Downloading m3u8 manifest' % info_dict['id'])
|
man_url = info_dict['url']
|
||||||
data = self.ydl.urlopen(url).read()
|
self.to_screen('[%s] Downloading m3u8 manifest' % self.FD_NAME)
|
||||||
s = data.decode('utf-8', 'ignore')
|
manifest = self.ydl.urlopen(man_url).read()
|
||||||
segment_urls = []
|
|
||||||
|
s = manifest.decode('utf-8', 'ignore')
|
||||||
|
fragment_urls = []
|
||||||
for line in s.splitlines():
|
for line in s.splitlines():
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if line and not line.startswith('#'):
|
if line and not line.startswith('#'):
|
||||||
segment_url = (
|
segment_url = (
|
||||||
line
|
line
|
||||||
if re.match(r'^https?://', line)
|
if re.match(r'^https?://', line)
|
||||||
else compat_urlparse.urljoin(url, line))
|
else compat_urlparse.urljoin(man_url, line))
|
||||||
segment_urls.append(segment_url)
|
fragment_urls.append(segment_url)
|
||||||
|
# We only download the first fragment during the test
|
||||||
is_test = self.params.get('test', False)
|
if self.params.get('test', False):
|
||||||
remaining_bytes = self._TEST_FILE_SIZE if is_test else None
|
|
||||||
byte_counter = 0
|
|
||||||
with open(tmpfilename, 'wb') as outf:
|
|
||||||
for i, segurl in enumerate(segment_urls):
|
|
||||||
self.to_screen(
|
|
||||||
'[hlsnative] %s: Downloading segment %d / %d' %
|
|
||||||
(info_dict['id'], i + 1, len(segment_urls)))
|
|
||||||
seg_req = compat_urllib_request.Request(segurl)
|
|
||||||
if remaining_bytes is not None:
|
|
||||||
seg_req.add_header('Range', 'bytes=0-%d' % (remaining_bytes - 1))
|
|
||||||
|
|
||||||
segment = self.ydl.urlopen(seg_req).read()
|
|
||||||
if remaining_bytes is not None:
|
|
||||||
segment = segment[:remaining_bytes]
|
|
||||||
remaining_bytes -= len(segment)
|
|
||||||
outf.write(segment)
|
|
||||||
byte_counter += len(segment)
|
|
||||||
if remaining_bytes is not None and remaining_bytes <= 0:
|
|
||||||
break
|
break
|
||||||
|
|
||||||
self._hook_progress({
|
ctx = {
|
||||||
'downloaded_bytes': byte_counter,
|
|
||||||
'total_bytes': byte_counter,
|
|
||||||
'filename': filename,
|
'filename': filename,
|
||||||
'status': 'finished',
|
'total_frags': len(fragment_urls),
|
||||||
})
|
}
|
||||||
self.try_rename(tmpfilename, filename)
|
|
||||||
|
self._prepare_and_start_frag_download(ctx)
|
||||||
|
|
||||||
|
frags_filenames = []
|
||||||
|
for i, frag_url in enumerate(fragment_urls):
|
||||||
|
frag_filename = '%s-Frag%d' % (ctx['tmpfilename'], i)
|
||||||
|
success = ctx['dl'].download(frag_filename, {'url': frag_url})
|
||||||
|
if not success:
|
||||||
|
return False
|
||||||
|
down, frag_sanitized = sanitize_open(frag_filename, 'rb')
|
||||||
|
ctx['dest_stream'].write(down.read())
|
||||||
|
down.close()
|
||||||
|
frags_filenames.append(frag_sanitized)
|
||||||
|
|
||||||
|
self._finish_frag_download(ctx)
|
||||||
|
|
||||||
|
for frag_file in frags_filenames:
|
||||||
|
os.remove(encodeFilename(frag_file))
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -4,16 +4,15 @@ import errno
|
|||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
|
import re
|
||||||
|
|
||||||
from .common import FileDownloader
|
from .common import FileDownloader
|
||||||
from ..compat import (
|
from ..compat import compat_urllib_error
|
||||||
compat_urllib_request,
|
|
||||||
compat_urllib_error,
|
|
||||||
)
|
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ContentTooShortError,
|
ContentTooShortError,
|
||||||
encodeFilename,
|
encodeFilename,
|
||||||
sanitize_open,
|
sanitize_open,
|
||||||
|
sanitized_Request,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -28,13 +27,8 @@ class HttpFD(FileDownloader):
|
|||||||
add_headers = info_dict.get('http_headers')
|
add_headers = info_dict.get('http_headers')
|
||||||
if add_headers:
|
if add_headers:
|
||||||
headers.update(add_headers)
|
headers.update(add_headers)
|
||||||
data = info_dict.get('http_post_data')
|
basic_request = sanitized_Request(url, None, headers)
|
||||||
http_method = info_dict.get('http_method')
|
request = sanitized_Request(url, None, headers)
|
||||||
basic_request = compat_urllib_request.Request(url, data, headers)
|
|
||||||
request = compat_urllib_request.Request(url, data, headers)
|
|
||||||
if http_method is not None:
|
|
||||||
basic_request.get_method = lambda: http_method
|
|
||||||
request.get_method = lambda: http_method
|
|
||||||
|
|
||||||
is_test = self.params.get('test', False)
|
is_test = self.params.get('test', False)
|
||||||
|
|
||||||
@@ -49,7 +43,7 @@ class HttpFD(FileDownloader):
|
|||||||
|
|
||||||
open_mode = 'wb'
|
open_mode = 'wb'
|
||||||
if resume_len != 0:
|
if resume_len != 0:
|
||||||
if self.params.get('continuedl', False):
|
if self.params.get('continuedl', True):
|
||||||
self.report_resuming_byte(resume_len)
|
self.report_resuming_byte(resume_len)
|
||||||
request.add_header('Range', 'bytes=%d-' % resume_len)
|
request.add_header('Range', 'bytes=%d-' % resume_len)
|
||||||
open_mode = 'ab'
|
open_mode = 'ab'
|
||||||
@@ -62,6 +56,24 @@ class HttpFD(FileDownloader):
|
|||||||
# Establish connection
|
# Establish connection
|
||||||
try:
|
try:
|
||||||
data = self.ydl.urlopen(request)
|
data = self.ydl.urlopen(request)
|
||||||
|
# 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
|
||||||
|
# that don't support resuming and serve a whole file with no Content-Range
|
||||||
|
# set in response despite of requested Range (see
|
||||||
|
# https://github.com/rg3/youtube-dl/issues/6057#issuecomment-126129799)
|
||||||
|
if resume_len > 0:
|
||||||
|
content_range = data.headers.get('Content-Range')
|
||||||
|
if content_range:
|
||||||
|
content_range_m = re.search(r'bytes (\d+)-', content_range)
|
||||||
|
# Content-Range is present and matches requested Range, resume is possible
|
||||||
|
if content_range_m and resume_len == int(content_range_m.group(1)):
|
||||||
|
break
|
||||||
|
# Content-Range is either not present or invalid. Assuming remote webserver is
|
||||||
|
# trying to send the whole file, resume is not possible, so wiping the local file
|
||||||
|
# and performing entire redownload
|
||||||
|
self.report_unable_to_resume()
|
||||||
|
resume_len = 0
|
||||||
|
open_mode = 'wb'
|
||||||
break
|
break
|
||||||
except (compat_urllib_error.HTTPError, ) as err:
|
except (compat_urllib_error.HTTPError, ) as err:
|
||||||
if (err.code < 500 or err.code >= 600) and err.code != 416:
|
if (err.code < 500 or err.code >= 600) and err.code != 416:
|
||||||
@@ -92,6 +104,8 @@ class HttpFD(FileDownloader):
|
|||||||
self._hook_progress({
|
self._hook_progress({
|
||||||
'filename': filename,
|
'filename': filename,
|
||||||
'status': 'finished',
|
'status': 'finished',
|
||||||
|
'downloaded_bytes': resume_len,
|
||||||
|
'total_bytes': resume_len,
|
||||||
})
|
})
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
@@ -218,12 +232,6 @@ class HttpFD(FileDownloader):
|
|||||||
if tmpfilename != '-':
|
if tmpfilename != '-':
|
||||||
stream.close()
|
stream.close()
|
||||||
|
|
||||||
self._hook_progress({
|
|
||||||
'downloaded_bytes': byte_counter,
|
|
||||||
'total_bytes': data_len,
|
|
||||||
'tmpfilename': tmpfilename,
|
|
||||||
'status': 'error',
|
|
||||||
})
|
|
||||||
if data_len is not None and byte_counter != data_len:
|
if data_len is not None and byte_counter != data_len:
|
||||||
raise ContentTooShortError(byte_counter, int(data_len))
|
raise ContentTooShortError(byte_counter, int(data_len))
|
||||||
self.try_rename(tmpfilename, filename)
|
self.try_rename(tmpfilename, filename)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from __future__ import unicode_literals
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from .common import FileDownloader
|
from .common import FileDownloader
|
||||||
@@ -11,6 +10,7 @@ from ..compat import compat_str
|
|||||||
from ..utils import (
|
from ..utils import (
|
||||||
check_executable,
|
check_executable,
|
||||||
encodeFilename,
|
encodeFilename,
|
||||||
|
encodeArgument,
|
||||||
get_exe_version,
|
get_exe_version,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ class RtmpFD(FileDownloader):
|
|||||||
protocol = info_dict.get('rtmp_protocol', None)
|
protocol = info_dict.get('rtmp_protocol', None)
|
||||||
real_time = info_dict.get('rtmp_real_time', False)
|
real_time = info_dict.get('rtmp_real_time', False)
|
||||||
no_resume = info_dict.get('no_resume', False)
|
no_resume = info_dict.get('no_resume', False)
|
||||||
continue_dl = info_dict.get('continuedl', False)
|
continue_dl = self.params.get('continuedl', True)
|
||||||
|
|
||||||
self.report_destination(filename)
|
self.report_destination(filename)
|
||||||
tmpfilename = self.temp_name(filename)
|
tmpfilename = self.temp_name(filename)
|
||||||
@@ -117,9 +117,11 @@ class RtmpFD(FileDownloader):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Download using rtmpdump. rtmpdump returns exit code 2 when
|
# Download using rtmpdump. rtmpdump returns exit code 2 when
|
||||||
# the connection was interrumpted and resuming appears to be
|
# the connection was interrupted and resuming appears to be
|
||||||
# possible. This is part of rtmpdump's normal usage, AFAIK.
|
# possible. This is part of rtmpdump's normal usage, AFAIK.
|
||||||
basic_args = ['rtmpdump', '--verbose', '-r', url, '-o', tmpfilename]
|
basic_args = [
|
||||||
|
'rtmpdump', '--verbose', '-r', url,
|
||||||
|
'-o', tmpfilename]
|
||||||
if player_url is not None:
|
if player_url is not None:
|
||||||
basic_args += ['--swfVfy', player_url]
|
basic_args += ['--swfVfy', player_url]
|
||||||
if page_url is not None:
|
if page_url is not None:
|
||||||
@@ -129,7 +131,7 @@ class RtmpFD(FileDownloader):
|
|||||||
if play_path is not None:
|
if play_path is not None:
|
||||||
basic_args += ['--playpath', play_path]
|
basic_args += ['--playpath', play_path]
|
||||||
if tc_url is not None:
|
if tc_url is not None:
|
||||||
basic_args += ['--tcUrl', url]
|
basic_args += ['--tcUrl', tc_url]
|
||||||
if test:
|
if test:
|
||||||
basic_args += ['--stop', '1']
|
basic_args += ['--stop', '1']
|
||||||
if flash_version is not None:
|
if flash_version is not None:
|
||||||
@@ -152,16 +154,9 @@ class RtmpFD(FileDownloader):
|
|||||||
if not live and continue_dl:
|
if not live and continue_dl:
|
||||||
args += ['--skip', '1']
|
args += ['--skip', '1']
|
||||||
|
|
||||||
if sys.platform == 'win32' and sys.version_info < (3, 0):
|
args = [encodeArgument(a) for a in args]
|
||||||
# Windows subprocess module does not actually support Unicode
|
|
||||||
# on Python 2.x
|
|
||||||
# See http://stackoverflow.com/a/9951851/35070
|
|
||||||
subprocess_encoding = sys.getfilesystemencoding()
|
|
||||||
args = [a.encode(subprocess_encoding, 'ignore') for a in args]
|
|
||||||
else:
|
|
||||||
subprocess_encoding = None
|
|
||||||
|
|
||||||
self._debug_cmd(args, subprocess_encoding, exe='rtmpdump')
|
self._debug_cmd(args, exe='rtmpdump')
|
||||||
|
|
||||||
RD_SUCCESS = 0
|
RD_SUCCESS = 0
|
||||||
RD_FAILED = 1
|
RD_FAILED = 1
|
||||||
@@ -178,7 +173,11 @@ class RtmpFD(FileDownloader):
|
|||||||
prevsize = os.path.getsize(encodeFilename(tmpfilename))
|
prevsize = os.path.getsize(encodeFilename(tmpfilename))
|
||||||
self.to_screen('[rtmpdump] %s bytes' % prevsize)
|
self.to_screen('[rtmpdump] %s bytes' % prevsize)
|
||||||
time.sleep(5.0) # This seems to be needed
|
time.sleep(5.0) # This seems to be needed
|
||||||
retval = run_rtmpdump(basic_args + ['-e'] + [[], ['-k', '1']][retval == RD_FAILED])
|
args = basic_args + ['--resume']
|
||||||
|
if retval == RD_FAILED:
|
||||||
|
args += ['--skip', '1']
|
||||||
|
args = [encodeArgument(a) for a in args]
|
||||||
|
retval = run_rtmpdump(args)
|
||||||
cursize = os.path.getsize(encodeFilename(tmpfilename))
|
cursize = os.path.getsize(encodeFilename(tmpfilename))
|
||||||
if prevsize == cursize and retval == RD_FAILED:
|
if prevsize == cursize and retval == RD_FAILED:
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -10,21 +10,23 @@ from ..utils import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MplayerFD(FileDownloader):
|
class RtspFD(FileDownloader):
|
||||||
def real_download(self, filename, info_dict):
|
def real_download(self, filename, info_dict):
|
||||||
url = info_dict['url']
|
url = info_dict['url']
|
||||||
self.report_destination(filename)
|
self.report_destination(filename)
|
||||||
tmpfilename = self.temp_name(filename)
|
tmpfilename = self.temp_name(filename)
|
||||||
|
|
||||||
args = [
|
if check_executable('mplayer', ['-h']):
|
||||||
'mplayer', '-really-quiet', '-vo', 'null', '-vc', 'dummy',
|
args = [
|
||||||
'-dumpstream', '-dumpfile', tmpfilename, url]
|
'mplayer', '-really-quiet', '-vo', 'null', '-vc', 'dummy',
|
||||||
# Check for mplayer first
|
'-dumpstream', '-dumpfile', tmpfilename, url]
|
||||||
if not check_executable('mplayer', ['-h']):
|
elif check_executable('mpv', ['-h']):
|
||||||
self.report_error('MMS or RTSP download detected but "%s" could not be run' % args[0])
|
args = [
|
||||||
|
'mpv', '-really-quiet', '--vo=null', '--stream-dump=' + tmpfilename, url]
|
||||||
|
else:
|
||||||
|
self.report_error('MMS or RTSP download detected but neither "mplayer" nor "mpv" could be run. Please install any.')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Download using mplayer.
|
|
||||||
retval = subprocess.call(args)
|
retval = subprocess.call(args)
|
||||||
if retval == 0:
|
if retval == 0:
|
||||||
fsize = os.path.getsize(encodeFilename(tmpfilename))
|
fsize = os.path.getsize(encodeFilename(tmpfilename))
|
||||||
@@ -39,5 +41,5 @@ class MplayerFD(FileDownloader):
|
|||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
self.to_stderr('\n')
|
self.to_stderr('\n')
|
||||||
self.report_error('mplayer exited with code %d' % retval)
|
self.report_error('%s exited with code %d' % (args[0], retval))
|
||||||
return False
|
return False
|
||||||
@@ -3,11 +3,21 @@ from __future__ import unicode_literals
|
|||||||
from .abc import ABCIE
|
from .abc import ABCIE
|
||||||
from .abc7news import Abc7NewsIE
|
from .abc7news import Abc7NewsIE
|
||||||
from .academicearth import AcademicEarthCourseIE
|
from .academicearth import AcademicEarthCourseIE
|
||||||
|
from .acast import (
|
||||||
|
ACastIE,
|
||||||
|
ACastChannelIE,
|
||||||
|
)
|
||||||
from .addanime import AddAnimeIE
|
from .addanime import AddAnimeIE
|
||||||
from .adobetv import AdobeTVIE
|
from .adobetv import (
|
||||||
|
AdobeTVIE,
|
||||||
|
AdobeTVShowIE,
|
||||||
|
AdobeTVChannelIE,
|
||||||
|
AdobeTVVideoIE,
|
||||||
|
)
|
||||||
from .adultswim import AdultSwimIE
|
from .adultswim import AdultSwimIE
|
||||||
from .aftenposten import AftenpostenIE
|
from .aenetworks import AENetworksIE
|
||||||
from .aftonbladet import AftonbladetIE
|
from .aftonbladet import AftonbladetIE
|
||||||
|
from .airmozilla import AirMozillaIE
|
||||||
from .aljazeera import AlJazeeraIE
|
from .aljazeera import AlJazeeraIE
|
||||||
from .alphaporno import AlphaPornoIE
|
from .alphaporno import AlphaPornoIE
|
||||||
from .anitube import AnitubeIE
|
from .anitube import AnitubeIE
|
||||||
@@ -15,37 +25,60 @@ from .anysex import AnySexIE
|
|||||||
from .aol import AolIE
|
from .aol import AolIE
|
||||||
from .allocine import AllocineIE
|
from .allocine import AllocineIE
|
||||||
from .aparat import AparatIE
|
from .aparat import AparatIE
|
||||||
from .appletrailers import AppleTrailersIE
|
from .appleconnect import AppleConnectIE
|
||||||
|
from .appletrailers import (
|
||||||
|
AppleTrailersIE,
|
||||||
|
AppleTrailersSectionIE,
|
||||||
|
)
|
||||||
from .archiveorg import ArchiveOrgIE
|
from .archiveorg import ArchiveOrgIE
|
||||||
from .ard import ARDIE, ARDMediathekIE
|
from .ard import (
|
||||||
|
ARDIE,
|
||||||
|
ARDMediathekIE,
|
||||||
|
SportschauIE,
|
||||||
|
)
|
||||||
from .arte import (
|
from .arte import (
|
||||||
ArteTvIE,
|
ArteTvIE,
|
||||||
ArteTVPlus7IE,
|
ArteTVPlus7IE,
|
||||||
ArteTVCreativeIE,
|
ArteTVCreativeIE,
|
||||||
ArteTVConcertIE,
|
ArteTVConcertIE,
|
||||||
ArteTVFutureIE,
|
ArteTVFutureIE,
|
||||||
|
ArteTVCinemaIE,
|
||||||
ArteTVDDCIE,
|
ArteTVDDCIE,
|
||||||
ArteTVEmbedIE,
|
ArteTVEmbedIE,
|
||||||
)
|
)
|
||||||
from .atresplayer import AtresPlayerIE
|
from .atresplayer import AtresPlayerIE
|
||||||
from .atttechchannel import ATTTechChannelIE
|
from .atttechchannel import ATTTechChannelIE
|
||||||
|
from .audimedia import AudiMediaIE
|
||||||
from .audiomack import AudiomackIE, AudiomackAlbumIE
|
from .audiomack import AudiomackIE, AudiomackAlbumIE
|
||||||
from .azubu import AzubuIE
|
from .azubu import AzubuIE, AzubuLiveIE
|
||||||
|
from .baidu import BaiduVideoIE
|
||||||
from .bambuser import BambuserIE, BambuserChannelIE
|
from .bambuser import BambuserIE, BambuserChannelIE
|
||||||
from .bandcamp import BandcampIE, BandcampAlbumIE
|
from .bandcamp import BandcampIE, BandcampAlbumIE
|
||||||
from .bbccouk import BBCCoUkIE
|
from .bbc import (
|
||||||
|
BBCCoUkIE,
|
||||||
|
BBCCoUkArticleIE,
|
||||||
|
BBCIE,
|
||||||
|
)
|
||||||
from .beeg import BeegIE
|
from .beeg import BeegIE
|
||||||
from .behindkink import BehindKinkIE
|
from .behindkink import BehindKinkIE
|
||||||
|
from .beatportpro import BeatportProIE
|
||||||
from .bet import BetIE
|
from .bet import BetIE
|
||||||
|
from .bigflix import BigflixIE
|
||||||
from .bild import BildIE
|
from .bild import BildIE
|
||||||
from .bilibili import BiliBiliIE
|
from .bilibili import BiliBiliIE
|
||||||
|
from .bleacherreport import (
|
||||||
|
BleacherReportIE,
|
||||||
|
BleacherReportCMSIE,
|
||||||
|
)
|
||||||
from .blinkx import BlinkxIE
|
from .blinkx import BlinkxIE
|
||||||
from .bliptv import BlipTVIE, BlipTVUserIE
|
|
||||||
from .bloomberg import BloombergIE
|
from .bloomberg import BloombergIE
|
||||||
from .bpb import BpbIE
|
from .bpb import BpbIE
|
||||||
from .br import BRIE
|
from .br import BRIE
|
||||||
from .breakcom import BreakIE
|
from .breakcom import BreakIE
|
||||||
from .brightcove import BrightcoveIE
|
from .brightcove import (
|
||||||
|
BrightcoveLegacyIE,
|
||||||
|
BrightcoveNewIE,
|
||||||
|
)
|
||||||
from .buzzfeed import BuzzFeedIE
|
from .buzzfeed import BuzzFeedIE
|
||||||
from .byutv import BYUtvIE
|
from .byutv import BYUtvIE
|
||||||
from .c56 import C56IE
|
from .c56 import C56IE
|
||||||
@@ -53,21 +86,32 @@ from .camdemy import (
|
|||||||
CamdemyIE,
|
CamdemyIE,
|
||||||
CamdemyFolderIE
|
CamdemyFolderIE
|
||||||
)
|
)
|
||||||
from .canal13cl import Canal13clIE
|
|
||||||
from .canalplus import CanalplusIE
|
from .canalplus import CanalplusIE
|
||||||
from .canalc2 import Canalc2IE
|
from .canalc2 import Canalc2IE
|
||||||
|
from .canvas import CanvasIE
|
||||||
from .cbs import CBSIE
|
from .cbs import CBSIE
|
||||||
from .cbsnews import CBSNewsIE
|
from .cbsnews import (
|
||||||
|
CBSNewsIE,
|
||||||
|
CBSNewsLiveVideoIE,
|
||||||
|
)
|
||||||
|
from .cbssports import CBSSportsIE
|
||||||
from .ccc import CCCIE
|
from .ccc import CCCIE
|
||||||
from .ceskatelevize import CeskaTelevizeIE
|
from .ceskatelevize import CeskaTelevizeIE
|
||||||
from .channel9 import Channel9IE
|
from .channel9 import Channel9IE
|
||||||
|
from .chaturbate import ChaturbateIE
|
||||||
from .chilloutzone import ChilloutzoneIE
|
from .chilloutzone import ChilloutzoneIE
|
||||||
|
from .chirbit import (
|
||||||
|
ChirbitIE,
|
||||||
|
ChirbitProfileIE,
|
||||||
|
)
|
||||||
from .cinchcast import CinchcastIE
|
from .cinchcast import CinchcastIE
|
||||||
|
from .cinemassacre import CinemassacreIE
|
||||||
from .clipfish import ClipfishIE
|
from .clipfish import ClipfishIE
|
||||||
from .cliphunter import CliphunterIE
|
from .cliphunter import CliphunterIE
|
||||||
from .clipsyndicate import ClipsyndicateIE
|
from .clipsyndicate import ClipsyndicateIE
|
||||||
from .cloudy import CloudyIE
|
from .cloudy import CloudyIE
|
||||||
from .clubic import ClubicIE
|
from .clubic import ClubicIE
|
||||||
|
from .clyp import ClypIE
|
||||||
from .cmt import CMTIE
|
from .cmt import CMTIE
|
||||||
from .cnet import CNETIE
|
from .cnet import CNETIE
|
||||||
from .cnn import (
|
from .cnn import (
|
||||||
@@ -83,33 +127,57 @@ from .commonmistakes import CommonMistakesIE, UnicodeBOMIE
|
|||||||
from .condenast import CondeNastIE
|
from .condenast import CondeNastIE
|
||||||
from .cracked import CrackedIE
|
from .cracked import CrackedIE
|
||||||
from .criterion import CriterionIE
|
from .criterion import CriterionIE
|
||||||
|
from .crooksandliars import CrooksAndLiarsIE
|
||||||
from .crunchyroll import (
|
from .crunchyroll import (
|
||||||
CrunchyrollIE,
|
CrunchyrollIE,
|
||||||
CrunchyrollShowPlaylistIE
|
CrunchyrollShowPlaylistIE
|
||||||
)
|
)
|
||||||
from .cspan import CSpanIE
|
from .cspan import CSpanIE
|
||||||
from .ctsnews import CtsNewsIE
|
from .ctsnews import CtsNewsIE
|
||||||
|
from .cultureunplugged import CultureUnpluggedIE
|
||||||
|
from .cwtv import CWTVIE
|
||||||
from .dailymotion import (
|
from .dailymotion import (
|
||||||
DailymotionIE,
|
DailymotionIE,
|
||||||
DailymotionPlaylistIE,
|
DailymotionPlaylistIE,
|
||||||
DailymotionUserIE,
|
DailymotionUserIE,
|
||||||
|
DailymotionCloudIE,
|
||||||
|
)
|
||||||
|
from .daum import (
|
||||||
|
DaumIE,
|
||||||
|
DaumClipIE,
|
||||||
|
DaumPlaylistIE,
|
||||||
|
DaumUserIE,
|
||||||
)
|
)
|
||||||
from .daum import DaumIE
|
|
||||||
from .dbtv import DBTVIE
|
from .dbtv import DBTVIE
|
||||||
|
from .dcn import (
|
||||||
|
DCNIE,
|
||||||
|
DCNVideoIE,
|
||||||
|
DCNLiveIE,
|
||||||
|
DCNSeasonIE,
|
||||||
|
)
|
||||||
from .dctp import DctpTvIE
|
from .dctp import DctpTvIE
|
||||||
from .deezer import DeezerPlaylistIE
|
from .deezer import DeezerPlaylistIE
|
||||||
|
from .democracynow import DemocracynowIE
|
||||||
from .dfb import DFBIE
|
from .dfb import DFBIE
|
||||||
|
from .dhm import DHMIE
|
||||||
from .dotsub import DotsubIE
|
from .dotsub import DotsubIE
|
||||||
|
from .douyutv import DouyuTVIE
|
||||||
|
from .dplay import DPlayIE
|
||||||
|
from .dramafever import (
|
||||||
|
DramaFeverIE,
|
||||||
|
DramaFeverSeriesIE,
|
||||||
|
)
|
||||||
from .dreisat import DreiSatIE
|
from .dreisat import DreiSatIE
|
||||||
from .drbonanza import DRBonanzaIE
|
from .drbonanza import DRBonanzaIE
|
||||||
from .drtuber import DrTuberIE
|
from .drtuber import DrTuberIE
|
||||||
from .drtv import DRTVIE
|
from .drtv import DRTVIE
|
||||||
from .dvtv import DVTVIE
|
from .dvtv import DVTVIE
|
||||||
from .dump import DumpIE
|
from .dump import DumpIE
|
||||||
|
from .dumpert import DumpertIE
|
||||||
from .defense import DefenseGouvFrIE
|
from .defense import DefenseGouvFrIE
|
||||||
from .discovery import DiscoveryIE
|
from .discovery import DiscoveryIE
|
||||||
from .divxstage import DivxStageIE
|
|
||||||
from .dropbox import DropboxIE
|
from .dropbox import DropboxIE
|
||||||
|
from .eagleplatform import EaglePlatformIE
|
||||||
from .ebaumsworld import EbaumsWorldIE
|
from .ebaumsworld import EbaumsWorldIE
|
||||||
from .echomsk import EchoMskIE
|
from .echomsk import EchoMskIE
|
||||||
from .ehow import EHowIE
|
from .ehow import EHowIE
|
||||||
@@ -122,32 +190,41 @@ from .ellentv import (
|
|||||||
)
|
)
|
||||||
from .elpais import ElPaisIE
|
from .elpais import ElPaisIE
|
||||||
from .embedly import EmbedlyIE
|
from .embedly import EmbedlyIE
|
||||||
from .empflix import EMPFlixIE
|
|
||||||
from .engadget import EngadgetIE
|
from .engadget import EngadgetIE
|
||||||
from .eporner import EpornerIE
|
from .eporner import EpornerIE
|
||||||
from .eroprofile import EroProfileIE
|
from .eroprofile import EroProfileIE
|
||||||
from .escapist import EscapistIE
|
from .escapist import EscapistIE
|
||||||
|
from .espn import ESPNIE
|
||||||
|
from .esri import EsriVideoIE
|
||||||
|
from .europa import EuropaIE
|
||||||
from .everyonesmixtape import EveryonesMixtapeIE
|
from .everyonesmixtape import EveryonesMixtapeIE
|
||||||
from .exfm import ExfmIE
|
from .exfm import ExfmIE
|
||||||
from .expotv import ExpoTVIE
|
from .expotv import ExpoTVIE
|
||||||
from .extremetube import ExtremeTubeIE
|
from .extremetube import ExtremeTubeIE
|
||||||
from .facebook import FacebookIE
|
from .facebook import (
|
||||||
|
FacebookIE,
|
||||||
|
FacebookPostIE,
|
||||||
|
)
|
||||||
from .faz import FazIE
|
from .faz import FazIE
|
||||||
from .fc2 import FC2IE
|
from .fc2 import FC2IE
|
||||||
from .firedrive import FiredriveIE
|
from .fczenit import FczenitIE
|
||||||
from .firstpost import FirstpostIE
|
from .firstpost import FirstpostIE
|
||||||
from .firsttv import FirstTVIE
|
from .firsttv import FirstTVIE
|
||||||
from .fivemin import FiveMinIE
|
from .fivemin import FiveMinIE
|
||||||
from .fktv import (
|
from .fivetv import FiveTVIE
|
||||||
FKTVIE,
|
from .fktv import FKTVIE
|
||||||
FKTVPosteckeIE,
|
|
||||||
)
|
|
||||||
from .flickr import FlickrIE
|
from .flickr import FlickrIE
|
||||||
from .folketinget import FolketingetIE
|
from .folketinget import FolketingetIE
|
||||||
|
from .footyroom import FootyRoomIE
|
||||||
from .fourtube import FourTubeIE
|
from .fourtube import FourTubeIE
|
||||||
|
from .fox import FOXIE
|
||||||
from .foxgay import FoxgayIE
|
from .foxgay import FoxgayIE
|
||||||
from .foxnews import FoxNewsIE
|
from .foxnews import FoxNewsIE
|
||||||
from .franceculture import FranceCultureIE
|
from .foxsports import FoxSportsIE
|
||||||
|
from .franceculture import (
|
||||||
|
FranceCultureIE,
|
||||||
|
FranceCultureEmissionIE,
|
||||||
|
)
|
||||||
from .franceinter import FranceInterIE
|
from .franceinter import FranceInterIE
|
||||||
from .francetv import (
|
from .francetv import (
|
||||||
PluzzIE,
|
PluzzIE,
|
||||||
@@ -159,29 +236,37 @@ from .francetv import (
|
|||||||
from .freesound import FreesoundIE
|
from .freesound import FreesoundIE
|
||||||
from .freespeech import FreespeechIE
|
from .freespeech import FreespeechIE
|
||||||
from .freevideo import FreeVideoIE
|
from .freevideo import FreeVideoIE
|
||||||
|
from .funimation import FunimationIE
|
||||||
from .funnyordie import FunnyOrDieIE
|
from .funnyordie import FunnyOrDieIE
|
||||||
|
from .gameinformer import GameInformerIE
|
||||||
from .gamekings import GamekingsIE
|
from .gamekings import GamekingsIE
|
||||||
from .gameone import (
|
from .gameone import (
|
||||||
GameOneIE,
|
GameOneIE,
|
||||||
GameOnePlaylistIE,
|
GameOnePlaylistIE,
|
||||||
)
|
)
|
||||||
|
from .gamersyde import GamersydeIE
|
||||||
from .gamespot import GameSpotIE
|
from .gamespot import GameSpotIE
|
||||||
from .gamestar import GameStarIE
|
from .gamestar import GameStarIE
|
||||||
from .gametrailers import GametrailersIE
|
from .gametrailers import GametrailersIE
|
||||||
|
from .gazeta import GazetaIE
|
||||||
from .gdcvault import GDCVaultIE
|
from .gdcvault import GDCVaultIE
|
||||||
from .generic import GenericIE
|
from .generic import GenericIE
|
||||||
|
from .gfycat import GfycatIE
|
||||||
from .giantbomb import GiantBombIE
|
from .giantbomb import GiantBombIE
|
||||||
from .giga import GigaIE
|
from .giga import GigaIE
|
||||||
from .glide import GlideIE
|
from .glide import GlideIE
|
||||||
from .globo import GloboIE
|
from .globo import (
|
||||||
|
GloboIE,
|
||||||
|
GloboArticleIE,
|
||||||
|
)
|
||||||
from .godtube import GodTubeIE
|
from .godtube import GodTubeIE
|
||||||
from .goldenmoustache import GoldenMoustacheIE
|
from .goldenmoustache import GoldenMoustacheIE
|
||||||
from .golem import GolemIE
|
from .golem import GolemIE
|
||||||
|
from .googledrive import GoogleDriveIE
|
||||||
from .googleplus import GooglePlusIE
|
from .googleplus import GooglePlusIE
|
||||||
from .googlesearch import GoogleSearchIE
|
from .googlesearch import GoogleSearchIE
|
||||||
from .gorillavid import GorillaVidIE
|
|
||||||
from .goshgay import GoshgayIE
|
from .goshgay import GoshgayIE
|
||||||
from .grooveshark import GroovesharkIE
|
from .gputechconf import GPUTechConfIE
|
||||||
from .groupon import GrouponIE
|
from .groupon import GrouponIE
|
||||||
from .hark import HarkIE
|
from .hark import HarkIE
|
||||||
from .hearthisat import HearThisAtIE
|
from .hearthisat import HearThisAtIE
|
||||||
@@ -190,48 +275,91 @@ from .hellporno import HellPornoIE
|
|||||||
from .helsinki import HelsinkiIE
|
from .helsinki import HelsinkiIE
|
||||||
from .hentaistigma import HentaiStigmaIE
|
from .hentaistigma import HentaiStigmaIE
|
||||||
from .historicfilms import HistoricFilmsIE
|
from .historicfilms import HistoricFilmsIE
|
||||||
from .history import HistoryIE
|
|
||||||
from .hitbox import HitboxIE, HitboxLiveIE
|
from .hitbox import HitboxIE, HitboxLiveIE
|
||||||
from .hornbunny import HornBunnyIE
|
from .hornbunny import HornBunnyIE
|
||||||
from .hostingbulk import HostingBulkIE
|
|
||||||
from .hotnewhiphop import HotNewHipHopIE
|
from .hotnewhiphop import HotNewHipHopIE
|
||||||
|
from .hotstar import HotStarIE
|
||||||
from .howcast import HowcastIE
|
from .howcast import HowcastIE
|
||||||
from .howstuffworks import HowStuffWorksIE
|
from .howstuffworks import HowStuffWorksIE
|
||||||
from .huffpost import HuffPostIE
|
from .huffpost import HuffPostIE
|
||||||
from .hypem import HypemIE
|
from .hypem import HypemIE
|
||||||
from .iconosquare import IconosquareIE
|
from .iconosquare import IconosquareIE
|
||||||
from .ign import IGNIE, OneUPIE
|
from .ign import (
|
||||||
|
IGNIE,
|
||||||
|
OneUPIE,
|
||||||
|
PCMagIE,
|
||||||
|
)
|
||||||
from .imdb import (
|
from .imdb import (
|
||||||
ImdbIE,
|
ImdbIE,
|
||||||
ImdbListIE
|
ImdbListIE
|
||||||
)
|
)
|
||||||
|
from .imgur import (
|
||||||
|
ImgurIE,
|
||||||
|
ImgurAlbumIE,
|
||||||
|
)
|
||||||
from .ina import InaIE
|
from .ina import InaIE
|
||||||
|
from .indavideo import (
|
||||||
|
IndavideoIE,
|
||||||
|
IndavideoEmbedIE,
|
||||||
|
)
|
||||||
from .infoq import InfoQIE
|
from .infoq import InfoQIE
|
||||||
from .instagram import InstagramIE, InstagramUserIE
|
from .instagram import InstagramIE, InstagramUserIE
|
||||||
from .internetvideoarchive import InternetVideoArchiveIE
|
from .internetvideoarchive import InternetVideoArchiveIE
|
||||||
from .iprima import IPrimaIE
|
from .iprima import IPrimaIE
|
||||||
|
from .iqiyi import IqiyiIE
|
||||||
|
from .ir90tv import Ir90TvIE
|
||||||
from .ivi import (
|
from .ivi import (
|
||||||
IviIE,
|
IviIE,
|
||||||
IviCompilationIE
|
IviCompilationIE
|
||||||
)
|
)
|
||||||
|
from .ivideon import IvideonIE
|
||||||
from .izlesene import IzleseneIE
|
from .izlesene import IzleseneIE
|
||||||
from .jadorecettepub import JadoreCettePubIE
|
from .jadorecettepub import JadoreCettePubIE
|
||||||
from .jeuxvideo import JeuxVideoIE
|
from .jeuxvideo import JeuxVideoIE
|
||||||
from .jove import JoveIE
|
from .jove import JoveIE
|
||||||
from .jukebox import JukeboxIE
|
from .jwplatform import JWPlatformIE
|
||||||
from .jpopsukitv import JpopsukiIE
|
from .jpopsukitv import JpopsukiIE
|
||||||
|
from .kaltura import KalturaIE
|
||||||
|
from .kanalplay import KanalPlayIE
|
||||||
from .kankan import KankanIE
|
from .kankan import KankanIE
|
||||||
from .karaoketv import KaraoketvIE
|
from .karaoketv import KaraoketvIE
|
||||||
|
from .karrierevideos import KarriereVideosIE
|
||||||
from .keezmovies import KeezMoviesIE
|
from .keezmovies import KeezMoviesIE
|
||||||
from .khanacademy import KhanAcademyIE
|
from .khanacademy import KhanAcademyIE
|
||||||
from .kickstarter import KickStarterIE
|
from .kickstarter import KickStarterIE
|
||||||
from .keek import KeekIE
|
from .keek import KeekIE
|
||||||
|
from .konserthusetplay import KonserthusetPlayIE
|
||||||
from .kontrtube import KontrTubeIE
|
from .kontrtube import KontrTubeIE
|
||||||
from .krasview import KrasViewIE
|
from .krasview import KrasViewIE
|
||||||
from .ku6 import Ku6IE
|
from .ku6 import Ku6IE
|
||||||
|
from .kuwo import (
|
||||||
|
KuwoIE,
|
||||||
|
KuwoAlbumIE,
|
||||||
|
KuwoChartIE,
|
||||||
|
KuwoSingerIE,
|
||||||
|
KuwoCategoryIE,
|
||||||
|
KuwoMvIE,
|
||||||
|
)
|
||||||
from .la7 import LA7IE
|
from .la7 import LA7IE
|
||||||
from .laola1tv import Laola1TvIE
|
from .laola1tv import Laola1TvIE
|
||||||
from .lifenews import LifeNewsIE
|
from .lecture2go import Lecture2GoIE
|
||||||
|
from .lemonde import LemondeIE
|
||||||
|
from .letv import (
|
||||||
|
LetvIE,
|
||||||
|
LetvTvIE,
|
||||||
|
LetvPlaylistIE,
|
||||||
|
LetvCloudIE,
|
||||||
|
)
|
||||||
|
from .libsyn import LibsynIE
|
||||||
|
from .lifenews import (
|
||||||
|
LifeNewsIE,
|
||||||
|
LifeEmbedIE,
|
||||||
|
)
|
||||||
|
from .limelight import (
|
||||||
|
LimelightMediaIE,
|
||||||
|
LimelightChannelIE,
|
||||||
|
LimelightChannelListIE,
|
||||||
|
)
|
||||||
from .liveleak import LiveLeakIE
|
from .liveleak import LiveLeakIE
|
||||||
from .livestream import (
|
from .livestream import (
|
||||||
LivestreamIE,
|
LivestreamIE,
|
||||||
@@ -239,6 +367,7 @@ from .livestream import (
|
|||||||
LivestreamShortenerIE,
|
LivestreamShortenerIE,
|
||||||
)
|
)
|
||||||
from .lnkgo import LnkGoIE
|
from .lnkgo import LnkGoIE
|
||||||
|
from .lovehomeporn import LoveHomePornIE
|
||||||
from .lrt import LRTIE
|
from .lrt import LRTIE
|
||||||
from .lynda import (
|
from .lynda import (
|
||||||
LyndaIE,
|
LyndaIE,
|
||||||
@@ -247,13 +376,16 @@ from .lynda import (
|
|||||||
from .m6 import M6IE
|
from .m6 import M6IE
|
||||||
from .macgamestore import MacGameStoreIE
|
from .macgamestore import MacGameStoreIE
|
||||||
from .mailru import MailRuIE
|
from .mailru import MailRuIE
|
||||||
|
from .makertv import MakerTVIE
|
||||||
from .malemotion import MalemotionIE
|
from .malemotion import MalemotionIE
|
||||||
|
from .matchtv import MatchTVIE
|
||||||
from .mdr import MDRIE
|
from .mdr import MDRIE
|
||||||
from .metacafe import MetacafeIE
|
from .metacafe import MetacafeIE
|
||||||
from .metacritic import MetacriticIE
|
from .metacritic import MetacriticIE
|
||||||
from .mgoon import MgoonIE
|
from .mgoon import MgoonIE
|
||||||
from .minhateca import MinhatecaIE
|
from .minhateca import MinhatecaIE
|
||||||
from .ministrygrid import MinistryGridIE
|
from .ministrygrid import MinistryGridIE
|
||||||
|
from .miomio import MioMioIE
|
||||||
from .mit import TechTVMITIE, MITIE, OCWMITIE
|
from .mit import TechTVMITIE, MITIE, OCWMITIE
|
||||||
from .mitele import MiTeleIE
|
from .mitele import MiTeleIE
|
||||||
from .mixcloud import MixcloudIE
|
from .mixcloud import MixcloudIE
|
||||||
@@ -269,39 +401,59 @@ from .motherless import MotherlessIE
|
|||||||
from .motorsport import MotorsportIE
|
from .motorsport import MotorsportIE
|
||||||
from .movieclips import MovieClipsIE
|
from .movieclips import MovieClipsIE
|
||||||
from .moviezine import MoviezineIE
|
from .moviezine import MoviezineIE
|
||||||
from .movshare import MovShareIE
|
|
||||||
from .mtv import (
|
from .mtv import (
|
||||||
MTVIE,
|
MTVIE,
|
||||||
MTVServicesEmbeddedIE,
|
MTVServicesEmbeddedIE,
|
||||||
MTVIggyIE,
|
MTVIggyIE,
|
||||||
|
MTVDEIE,
|
||||||
)
|
)
|
||||||
from .muenchentv import MuenchenTVIE
|
from .muenchentv import MuenchenTVIE
|
||||||
from .musicplayon import MusicPlayOnIE
|
from .musicplayon import MusicPlayOnIE
|
||||||
from .musicvault import MusicVaultIE
|
|
||||||
from .muzu import MuzuTVIE
|
from .muzu import MuzuTVIE
|
||||||
|
from .mwave import MwaveIE
|
||||||
from .myspace import MySpaceIE, MySpaceAlbumIE
|
from .myspace import MySpaceIE, MySpaceAlbumIE
|
||||||
from .myspass import MySpassIE
|
from .myspass import MySpassIE
|
||||||
|
from .myvi import MyviIE
|
||||||
from .myvideo import MyVideoIE
|
from .myvideo import MyVideoIE
|
||||||
from .myvidster import MyVidsterIE
|
from .myvidster import MyVidsterIE
|
||||||
|
from .nationalgeographic import NationalGeographicIE
|
||||||
from .naver import NaverIE
|
from .naver import NaverIE
|
||||||
from .nba import NBAIE
|
from .nba import NBAIE
|
||||||
from .nbc import (
|
from .nbc import (
|
||||||
NBCIE,
|
NBCIE,
|
||||||
NBCNewsIE,
|
NBCNewsIE,
|
||||||
|
NBCSportsIE,
|
||||||
|
NBCSportsVPlayerIE,
|
||||||
|
MSNBCIE,
|
||||||
|
)
|
||||||
|
from .ndr import (
|
||||||
|
NDRIE,
|
||||||
|
NJoyIE,
|
||||||
|
NDREmbedBaseIE,
|
||||||
|
NDREmbedIE,
|
||||||
|
NJoyEmbedIE,
|
||||||
)
|
)
|
||||||
from .ndr import NDRIE
|
|
||||||
from .ndtv import NDTVIE
|
from .ndtv import NDTVIE
|
||||||
from .netzkino import NetzkinoIE
|
from .netzkino import NetzkinoIE
|
||||||
from .nerdcubed import NerdCubedFeedIE
|
from .nerdcubed import NerdCubedFeedIE
|
||||||
from .nerdist import NerdistIE
|
from .nerdist import NerdistIE
|
||||||
|
from .neteasemusic import (
|
||||||
|
NetEaseMusicIE,
|
||||||
|
NetEaseMusicAlbumIE,
|
||||||
|
NetEaseMusicSingerIE,
|
||||||
|
NetEaseMusicListIE,
|
||||||
|
NetEaseMusicMvIE,
|
||||||
|
NetEaseMusicProgramIE,
|
||||||
|
NetEaseMusicDjRadioIE,
|
||||||
|
)
|
||||||
from .newgrounds import NewgroundsIE
|
from .newgrounds import NewgroundsIE
|
||||||
from .newstube import NewstubeIE
|
from .newstube import NewstubeIE
|
||||||
from .nextmedia import (
|
from .nextmedia import (
|
||||||
NextMediaIE,
|
NextMediaIE,
|
||||||
NextMediaActionNewsIE,
|
NextMediaActionNewsIE,
|
||||||
AppleDailyRealtimeNewsIE,
|
AppleDailyIE,
|
||||||
AppleDailyAnimationNewsIE
|
|
||||||
)
|
)
|
||||||
|
from .nextmovie import NextMovieIE
|
||||||
from .nfb import NFBIE
|
from .nfb import NFBIE
|
||||||
from .nfl import NFLIE
|
from .nfl import NFLIE
|
||||||
from .nhl import (
|
from .nhl import (
|
||||||
@@ -309,76 +461,138 @@ from .nhl import (
|
|||||||
NHLNewsIE,
|
NHLNewsIE,
|
||||||
NHLVideocenterIE,
|
NHLVideocenterIE,
|
||||||
)
|
)
|
||||||
|
from .nick import NickIE
|
||||||
from .niconico import NiconicoIE, NiconicoPlaylistIE
|
from .niconico import NiconicoIE, NiconicoPlaylistIE
|
||||||
from .ninegag import NineGagIE
|
from .ninegag import NineGagIE
|
||||||
from .noco import NocoIE
|
from .noco import NocoIE
|
||||||
from .normalboots import NormalbootsIE
|
from .normalboots import NormalbootsIE
|
||||||
from .nosvideo import NosVideoIE
|
from .nosvideo import NosVideoIE
|
||||||
from .novamov import NovaMovIE
|
from .nova import NovaIE
|
||||||
from .nowness import NownessIE
|
from .novamov import (
|
||||||
from .nowvideo import NowVideoIE
|
NovaMovIE,
|
||||||
|
WholeCloudIE,
|
||||||
|
NowVideoIE,
|
||||||
|
VideoWeedIE,
|
||||||
|
CloudTimeIE,
|
||||||
|
)
|
||||||
|
from .nowness import (
|
||||||
|
NownessIE,
|
||||||
|
NownessPlaylistIE,
|
||||||
|
NownessSeriesIE,
|
||||||
|
)
|
||||||
|
from .nowtv import (
|
||||||
|
NowTVIE,
|
||||||
|
NowTVListIE,
|
||||||
|
)
|
||||||
from .npo import (
|
from .npo import (
|
||||||
NPOIE,
|
NPOIE,
|
||||||
NPOLiveIE,
|
NPOLiveIE,
|
||||||
NPORadioIE,
|
NPORadioIE,
|
||||||
NPORadioFragmentIE,
|
NPORadioFragmentIE,
|
||||||
TegenlichtVproIE,
|
SchoolTVIE,
|
||||||
|
VPROIE,
|
||||||
|
WNLIE
|
||||||
)
|
)
|
||||||
|
from .npr import NprIE
|
||||||
from .nrk import (
|
from .nrk import (
|
||||||
NRKIE,
|
NRKIE,
|
||||||
|
NRKPlaylistIE,
|
||||||
NRKTVIE,
|
NRKTVIE,
|
||||||
)
|
)
|
||||||
from .ntvde import NTVDeIE
|
from .ntvde import NTVDeIE
|
||||||
from .ntvru import NTVRuIE
|
from .ntvru import NTVRuIE
|
||||||
from .nytimes import NYTimesIE
|
from .nytimes import (
|
||||||
|
NYTimesIE,
|
||||||
|
NYTimesArticleIE,
|
||||||
|
)
|
||||||
from .nuvid import NuvidIE
|
from .nuvid import NuvidIE
|
||||||
|
from .odnoklassniki import OdnoklassnikiIE
|
||||||
from .oktoberfesttv import OktoberfestTVIE
|
from .oktoberfesttv import OktoberfestTVIE
|
||||||
from .ooyala import OoyalaIE
|
from .onionstudios import OnionStudiosIE
|
||||||
from .openfilm import OpenFilmIE
|
from .ooyala import (
|
||||||
|
OoyalaIE,
|
||||||
|
OoyalaExternalIE,
|
||||||
|
)
|
||||||
|
from .ora import OraTVIE
|
||||||
from .orf import (
|
from .orf import (
|
||||||
ORFTVthekIE,
|
ORFTVthekIE,
|
||||||
ORFOE1IE,
|
ORFOE1IE,
|
||||||
ORFFM4IE,
|
ORFFM4IE,
|
||||||
|
ORFIPTVIE,
|
||||||
)
|
)
|
||||||
|
from .pandoratv import PandoraTVIE
|
||||||
from .parliamentliveuk import ParliamentLiveUKIE
|
from .parliamentliveuk import ParliamentLiveUKIE
|
||||||
from .patreon import PatreonIE
|
from .patreon import PatreonIE
|
||||||
from .pbs import PBSIE
|
from .pbs import PBSIE
|
||||||
|
from .periscope import PeriscopeIE
|
||||||
|
from .philharmoniedeparis import PhilharmonieDeParisIE
|
||||||
from .phoenix import PhoenixIE
|
from .phoenix import PhoenixIE
|
||||||
from .photobucket import PhotobucketIE
|
from .photobucket import PhotobucketIE
|
||||||
|
from .pinkbike import PinkbikeIE
|
||||||
from .planetaplay import PlanetaPlayIE
|
from .planetaplay import PlanetaPlayIE
|
||||||
|
from .pladform import PladformIE
|
||||||
from .played import PlayedIE
|
from .played import PlayedIE
|
||||||
from .playfm import PlayFMIE
|
from .playfm import PlayFMIE
|
||||||
|
from .playtvak import PlaytvakIE
|
||||||
from .playvid import PlayvidIE
|
from .playvid import PlayvidIE
|
||||||
|
from .playwire import PlaywireIE
|
||||||
|
from .pluralsight import (
|
||||||
|
PluralsightIE,
|
||||||
|
PluralsightCourseIE,
|
||||||
|
)
|
||||||
from .podomatic import PodomaticIE
|
from .podomatic import PodomaticIE
|
||||||
|
from .porn91 import Porn91IE
|
||||||
from .pornhd import PornHdIE
|
from .pornhd import PornHdIE
|
||||||
from .pornhub import PornHubIE
|
from .pornhub import (
|
||||||
|
PornHubIE,
|
||||||
|
PornHubPlaylistIE,
|
||||||
|
)
|
||||||
from .pornotube import PornotubeIE
|
from .pornotube import PornotubeIE
|
||||||
|
from .pornovoisines import PornoVoisinesIE
|
||||||
from .pornoxo import PornoXOIE
|
from .pornoxo import PornoXOIE
|
||||||
|
from .primesharetv import PrimeShareTVIE
|
||||||
from .promptfile import PromptFileIE
|
from .promptfile import PromptFileIE
|
||||||
from .prosiebensat1 import ProSiebenSat1IE
|
from .prosiebensat1 import ProSiebenSat1IE
|
||||||
|
from .puls4 import Puls4IE
|
||||||
from .pyvideo import PyvideoIE
|
from .pyvideo import PyvideoIE
|
||||||
|
from .qqmusic import (
|
||||||
|
QQMusicIE,
|
||||||
|
QQMusicSingerIE,
|
||||||
|
QQMusicAlbumIE,
|
||||||
|
QQMusicToplistIE,
|
||||||
|
QQMusicPlaylistIE,
|
||||||
|
)
|
||||||
from .quickvid import QuickVidIE
|
from .quickvid import QuickVidIE
|
||||||
|
from .r7 import R7IE
|
||||||
from .radiode import RadioDeIE
|
from .radiode import RadioDeIE
|
||||||
|
from .radiojavan import RadioJavanIE
|
||||||
from .radiobremen import RadioBremenIE
|
from .radiobremen import RadioBremenIE
|
||||||
from .radiofrance import RadioFranceIE
|
from .radiofrance import RadioFranceIE
|
||||||
from .rai import RaiIE
|
from .rai import (
|
||||||
|
RaiTVIE,
|
||||||
|
RaiIE,
|
||||||
|
)
|
||||||
from .rbmaradio import RBMARadioIE
|
from .rbmaradio import RBMARadioIE
|
||||||
|
from .rds import RDSIE
|
||||||
from .redtube import RedTubeIE
|
from .redtube import RedTubeIE
|
||||||
|
from .regiotv import RegioTVIE
|
||||||
from .restudy import RestudyIE
|
from .restudy import RestudyIE
|
||||||
from .reverbnation import ReverbNationIE
|
from .reverbnation import ReverbNationIE
|
||||||
|
from .revision3 import Revision3IE
|
||||||
from .ringtv import RingTVIE
|
from .ringtv import RingTVIE
|
||||||
from .ro220 import Ro220IE
|
from .ro220 import Ro220IE
|
||||||
from .rottentomatoes import RottenTomatoesIE
|
from .rottentomatoes import RottenTomatoesIE
|
||||||
from .roxwel import RoxwelIE
|
from .roxwel import RoxwelIE
|
||||||
from .rtbf import RTBFIE
|
from .rtbf import RTBFIE
|
||||||
from .rte import RteIE
|
from .rte import RteIE, RteRadioIE
|
||||||
from .rtlnl import RtlNlIE
|
from .rtlnl import RtlNlIE
|
||||||
from .rtlnow import RTLnowIE
|
|
||||||
from .rtl2 import RTL2IE
|
from .rtl2 import RTL2IE
|
||||||
from .rtp import RTPIE
|
from .rtp import RTPIE
|
||||||
from .rts import RTSIE
|
from .rts import RTSIE
|
||||||
from .rtve import RTVEALaCartaIE, RTVELiveIE
|
from .rtve import RTVEALaCartaIE, RTVELiveIE, RTVEInfantilIE
|
||||||
|
from .rtvnh import RTVNHIE
|
||||||
from .ruhd import RUHDIE
|
from .ruhd import RUHDIE
|
||||||
|
from .ruleporn import RulePornIE
|
||||||
from .rutube import (
|
from .rutube import (
|
||||||
RutubeIE,
|
RutubeIE,
|
||||||
RutubeChannelIE,
|
RutubeChannelIE,
|
||||||
@@ -387,20 +601,31 @@ from .rutube import (
|
|||||||
RutubePersonIE,
|
RutubePersonIE,
|
||||||
)
|
)
|
||||||
from .rutv import RUTVIE
|
from .rutv import RUTVIE
|
||||||
|
from .ruutu import RuutuIE
|
||||||
from .sandia import SandiaIE
|
from .sandia import SandiaIE
|
||||||
|
from .safari import (
|
||||||
|
SafariIE,
|
||||||
|
SafariCourseIE,
|
||||||
|
)
|
||||||
from .sapo import SapoIE
|
from .sapo import SapoIE
|
||||||
from .savefrom import SaveFromIE
|
from .savefrom import SaveFromIE
|
||||||
from .sbs import SBSIE
|
from .sbs import SBSIE
|
||||||
from .scivee import SciVeeIE
|
from .scivee import SciVeeIE
|
||||||
from .screencast import ScreencastIE
|
from .screencast import ScreencastIE
|
||||||
from .screencastomatic import ScreencastOMaticIE
|
from .screencastomatic import ScreencastOMaticIE
|
||||||
from .screenwavemedia import CinemassacreIE, ScreenwaveMediaIE, TeamFourIE
|
from .screenwavemedia import ScreenwaveMediaIE, TeamFourIE
|
||||||
|
from .senateisvp import SenateISVPIE
|
||||||
from .servingsys import ServingSysIE
|
from .servingsys import ServingSysIE
|
||||||
from .sexu import SexuIE
|
from .sexu import SexuIE
|
||||||
from .sexykarma import SexyKarmaIE
|
from .sexykarma import SexyKarmaIE
|
||||||
|
from .shahid import ShahidIE
|
||||||
from .shared import SharedIE
|
from .shared import SharedIE
|
||||||
from .sharesix import ShareSixIE
|
from .sharesix import ShareSixIE
|
||||||
from .sina import SinaIE
|
from .sina import SinaIE
|
||||||
|
from .skynewsarabia import (
|
||||||
|
SkyNewsArabiaIE,
|
||||||
|
SkyNewsArabiaArticleIE,
|
||||||
|
)
|
||||||
from .slideshare import SlideshareIE
|
from .slideshare import SlideshareIE
|
||||||
from .slutload import SlutloadIE
|
from .slutload import SlutloadIE
|
||||||
from .smotri import (
|
from .smotri import (
|
||||||
@@ -409,36 +634,59 @@ from .smotri import (
|
|||||||
SmotriUserIE,
|
SmotriUserIE,
|
||||||
SmotriBroadcastIE,
|
SmotriBroadcastIE,
|
||||||
)
|
)
|
||||||
|
from .snagfilms import (
|
||||||
|
SnagFilmsIE,
|
||||||
|
SnagFilmsEmbedIE,
|
||||||
|
)
|
||||||
from .snotr import SnotrIE
|
from .snotr import SnotrIE
|
||||||
from .sockshare import SockshareIE
|
|
||||||
from .sohu import SohuIE
|
from .sohu import SohuIE
|
||||||
from .soundcloud import (
|
from .soundcloud import (
|
||||||
SoundcloudIE,
|
SoundcloudIE,
|
||||||
SoundcloudSetIE,
|
SoundcloudSetIE,
|
||||||
SoundcloudUserIE,
|
SoundcloudUserIE,
|
||||||
SoundcloudPlaylistIE
|
SoundcloudPlaylistIE,
|
||||||
|
SoundcloudSearchIE
|
||||||
|
)
|
||||||
|
from .soundgasm import (
|
||||||
|
SoundgasmIE,
|
||||||
|
SoundgasmProfileIE
|
||||||
)
|
)
|
||||||
from .soundgasm import SoundgasmIE
|
|
||||||
from .southpark import (
|
from .southpark import (
|
||||||
SouthParkIE,
|
SouthParkIE,
|
||||||
SouthparkDeIE,
|
SouthParkDeIE,
|
||||||
|
SouthParkDkIE,
|
||||||
|
SouthParkEsIE,
|
||||||
|
SouthParkNlIE
|
||||||
)
|
)
|
||||||
from .space import SpaceIE
|
from .space import SpaceIE
|
||||||
|
from .spankbang import SpankBangIE
|
||||||
from .spankwire import SpankwireIE
|
from .spankwire import SpankwireIE
|
||||||
from .spiegel import SpiegelIE, SpiegelArticleIE
|
from .spiegel import SpiegelIE, SpiegelArticleIE
|
||||||
from .spiegeltv import SpiegeltvIE
|
from .spiegeltv import SpiegeltvIE
|
||||||
from .spike import SpikeIE
|
from .spike import SpikeIE
|
||||||
|
from .stitcher import StitcherIE
|
||||||
from .sport5 import Sport5IE
|
from .sport5 import Sport5IE
|
||||||
from .sportbox import SportBoxIE
|
from .sportbox import (
|
||||||
|
SportBoxIE,
|
||||||
|
SportBoxEmbedIE,
|
||||||
|
)
|
||||||
from .sportdeutschland import SportDeutschlandIE
|
from .sportdeutschland import SportDeutschlandIE
|
||||||
|
from .srgssr import (
|
||||||
|
SRGSSRIE,
|
||||||
|
SRGSSRPlayIE,
|
||||||
|
)
|
||||||
from .srmediathek import SRMediathekIE
|
from .srmediathek import SRMediathekIE
|
||||||
|
from .ssa import SSAIE
|
||||||
from .stanfordoc import StanfordOpenClassroomIE
|
from .stanfordoc import StanfordOpenClassroomIE
|
||||||
from .steam import SteamIE
|
from .steam import SteamIE
|
||||||
from .streamcloud import StreamcloudIE
|
from .streamcloud import StreamcloudIE
|
||||||
from .streamcz import StreamCZIE
|
from .streamcz import StreamCZIE
|
||||||
from .streetvoice import StreetVoiceIE
|
from .streetvoice import StreetVoiceIE
|
||||||
from .sunporno import SunPornoIE
|
from .sunporno import SunPornoIE
|
||||||
from .svtplay import SVTPlayIE
|
from .svt import (
|
||||||
|
SVTIE,
|
||||||
|
SVTPlayIE,
|
||||||
|
)
|
||||||
from .swrmediathek import SWRMediathekIE
|
from .swrmediathek import SWRMediathekIE
|
||||||
from .syfy import SyfyIE
|
from .syfy import SyfyIE
|
||||||
from .sztvhu import SztvHuIE
|
from .sztvhu import SztvHuIE
|
||||||
@@ -453,22 +701,36 @@ from .teachingchannel import TeachingChannelIE
|
|||||||
from .teamcoco import TeamcocoIE
|
from .teamcoco import TeamcocoIE
|
||||||
from .techtalks import TechTalksIE
|
from .techtalks import TechTalksIE
|
||||||
from .ted import TEDIE
|
from .ted import TEDIE
|
||||||
|
from .tele13 import Tele13IE
|
||||||
from .telebruxelles import TeleBruxellesIE
|
from .telebruxelles import TeleBruxellesIE
|
||||||
from .telecinco import TelecincoIE
|
from .telecinco import TelecincoIE
|
||||||
|
from .telegraaf import TelegraafIE
|
||||||
from .telemb import TeleMBIE
|
from .telemb import TeleMBIE
|
||||||
from .teletask import TeleTaskIE
|
from .teletask import TeleTaskIE
|
||||||
from .tenplay import TenPlayIE
|
from .tenplay import TenPlayIE
|
||||||
from .testurl import TestURLIE
|
from .testurl import TestURLIE
|
||||||
from .testtube import TestTubeIE
|
|
||||||
from .tf1 import TF1IE
|
from .tf1 import TF1IE
|
||||||
|
from .theintercept import TheInterceptIE
|
||||||
from .theonion import TheOnionIE
|
from .theonion import TheOnionIE
|
||||||
from .theplatform import ThePlatformIE
|
from .theplatform import (
|
||||||
|
ThePlatformIE,
|
||||||
|
ThePlatformFeedIE,
|
||||||
|
)
|
||||||
from .thesixtyone import TheSixtyOneIE
|
from .thesixtyone import TheSixtyOneIE
|
||||||
|
from .thisamericanlife import ThisAmericanLifeIE
|
||||||
from .thisav import ThisAVIE
|
from .thisav import ThisAVIE
|
||||||
from .tinypic import TinyPicIE
|
from .tinypic import TinyPicIE
|
||||||
from .tlc import TlcIE, TlcDeIE
|
from .tlc import TlcDeIE
|
||||||
from .tmz import TMZIE
|
from .tmz import (
|
||||||
from .tnaflix import TNAFlixIE
|
TMZIE,
|
||||||
|
TMZArticleIE,
|
||||||
|
)
|
||||||
|
from .tnaflix import (
|
||||||
|
TNAFlixIE,
|
||||||
|
EMPFlixIE,
|
||||||
|
MovieFapIE,
|
||||||
|
)
|
||||||
|
from .toggle import ToggleIE
|
||||||
from .thvideo import (
|
from .thvideo import (
|
||||||
THVideoIE,
|
THVideoIE,
|
||||||
THVideoPlaylistIE
|
THVideoPlaylistIE
|
||||||
@@ -477,18 +739,45 @@ from .toutv import TouTvIE
|
|||||||
from .toypics import ToypicsUserIE, ToypicsIE
|
from .toypics import ToypicsUserIE, ToypicsIE
|
||||||
from .traileraddict import TrailerAddictIE
|
from .traileraddict import TrailerAddictIE
|
||||||
from .trilulilu import TriluliluIE
|
from .trilulilu import TriluliluIE
|
||||||
|
from .trollvids import TrollvidsIE
|
||||||
from .trutube import TruTubeIE
|
from .trutube import TruTubeIE
|
||||||
from .tube8 import Tube8IE
|
from .tube8 import Tube8IE
|
||||||
from .tudou import TudouIE
|
from .tubitv import TubiTvIE
|
||||||
|
from .tudou import (
|
||||||
|
TudouIE,
|
||||||
|
TudouPlaylistIE,
|
||||||
|
TudouAlbumIE,
|
||||||
|
)
|
||||||
from .tumblr import TumblrIE
|
from .tumblr import TumblrIE
|
||||||
from .tunein import TuneInIE
|
from .tunein import (
|
||||||
|
TuneInClipIE,
|
||||||
|
TuneInStationIE,
|
||||||
|
TuneInProgramIE,
|
||||||
|
TuneInTopicIE,
|
||||||
|
TuneInShortenerIE,
|
||||||
|
)
|
||||||
from .turbo import TurboIE
|
from .turbo import TurboIE
|
||||||
from .tutv import TutvIE
|
from .tutv import TutvIE
|
||||||
|
from .tv2 import (
|
||||||
|
TV2IE,
|
||||||
|
TV2ArticleIE,
|
||||||
|
)
|
||||||
|
from .tv4 import TV4IE
|
||||||
|
from .tvc import (
|
||||||
|
TVCIE,
|
||||||
|
TVCArticleIE,
|
||||||
|
)
|
||||||
from .tvigle import TvigleIE
|
from .tvigle import TvigleIE
|
||||||
|
from .tvland import TVLandIE
|
||||||
from .tvp import TvpIE, TvpSeriesIE
|
from .tvp import TvpIE, TvpSeriesIE
|
||||||
from .tvplay import TVPlayIE
|
from .tvplay import TVPlayIE
|
||||||
from .tweakers import TweakersIE
|
from .tweakers import TweakersIE
|
||||||
from .twentyfourvideo import TwentyFourVideoIE
|
from .twentyfourvideo import TwentyFourVideoIE
|
||||||
|
from .twentymin import TwentyMinutenIE
|
||||||
|
from .twentytwotracks import (
|
||||||
|
TwentyTwoTracksIE,
|
||||||
|
TwentyTwoTracksGenreIE
|
||||||
|
)
|
||||||
from .twitch import (
|
from .twitch import (
|
||||||
TwitchVideoIE,
|
TwitchVideoIE,
|
||||||
TwitchChapterIE,
|
TwitchChapterIE,
|
||||||
@@ -498,34 +787,51 @@ from .twitch import (
|
|||||||
TwitchBookmarksIE,
|
TwitchBookmarksIE,
|
||||||
TwitchStreamIE,
|
TwitchStreamIE,
|
||||||
)
|
)
|
||||||
|
from .twitter import TwitterCardIE, TwitterIE
|
||||||
from .ubu import UbuIE
|
from .ubu import UbuIE
|
||||||
from .udemy import (
|
from .udemy import (
|
||||||
UdemyIE,
|
UdemyIE,
|
||||||
UdemyCourseIE
|
UdemyCourseIE
|
||||||
)
|
)
|
||||||
|
from .udn import UDNEmbedIE
|
||||||
|
from .digiteka import DigitekaIE
|
||||||
from .unistra import UnistraIE
|
from .unistra import UnistraIE
|
||||||
from .urort import UrortIE
|
from .urort import UrortIE
|
||||||
from .ustream import UstreamIE, UstreamChannelIE
|
from .ustream import UstreamIE, UstreamChannelIE
|
||||||
|
from .varzesh3 import Varzesh3IE
|
||||||
from .vbox7 import Vbox7IE
|
from .vbox7 import Vbox7IE
|
||||||
from .veehd import VeeHDIE
|
from .veehd import VeeHDIE
|
||||||
from .veoh import VeohIE
|
from .veoh import VeohIE
|
||||||
|
from .vessel import VesselIE
|
||||||
from .vesti import VestiIE
|
from .vesti import VestiIE
|
||||||
from .vevo import VevoIE
|
from .vevo import VevoIE
|
||||||
from .vgtv import VGTVIE
|
from .vgtv import (
|
||||||
|
BTArticleIE,
|
||||||
|
BTVestlendingenIE,
|
||||||
|
VGTVIE,
|
||||||
|
)
|
||||||
from .vh1 import VH1IE
|
from .vh1 import VH1IE
|
||||||
from .vice import ViceIE
|
from .vice import ViceIE
|
||||||
from .viddler import ViddlerIE
|
from .viddler import ViddlerIE
|
||||||
from .videobam import VideoBamIE
|
|
||||||
from .videodetective import VideoDetectiveIE
|
from .videodetective import VideoDetectiveIE
|
||||||
from .videolecturesnet import VideoLecturesNetIE
|
|
||||||
from .videofyme import VideofyMeIE
|
from .videofyme import VideofyMeIE
|
||||||
from .videomega import VideoMegaIE
|
from .videomega import VideoMegaIE
|
||||||
|
from .videomore import (
|
||||||
|
VideomoreIE,
|
||||||
|
VideomoreVideoIE,
|
||||||
|
VideomoreSeasonIE,
|
||||||
|
)
|
||||||
from .videopremium import VideoPremiumIE
|
from .videopremium import VideoPremiumIE
|
||||||
from .videott import VideoTtIE
|
from .videott import VideoTtIE
|
||||||
from .videoweed import VideoWeedIE
|
from .vidme import (
|
||||||
from .vidme import VidmeIE
|
VidmeIE,
|
||||||
|
VidmeUserIE,
|
||||||
|
VidmeUserLikesIE,
|
||||||
|
)
|
||||||
from .vidzi import VidziIE
|
from .vidzi import VidziIE
|
||||||
from .vier import VierIE, VierVideosIE
|
from .vier import VierIE, VierVideosIE
|
||||||
|
from .viewster import ViewsterIE
|
||||||
|
from .viidea import ViideaIE
|
||||||
from .vimeo import (
|
from .vimeo import (
|
||||||
VimeoIE,
|
VimeoIE,
|
||||||
VimeoAlbumIE,
|
VimeoAlbumIE,
|
||||||
@@ -541,12 +847,17 @@ from .vine import (
|
|||||||
VineIE,
|
VineIE,
|
||||||
VineUserIE,
|
VineUserIE,
|
||||||
)
|
)
|
||||||
from .viki import VikiIE
|
from .viki import (
|
||||||
|
VikiIE,
|
||||||
|
VikiChannelIE,
|
||||||
|
)
|
||||||
from .vk import (
|
from .vk import (
|
||||||
VKIE,
|
VKIE,
|
||||||
VKUserVideosIE,
|
VKUserVideosIE,
|
||||||
)
|
)
|
||||||
|
from .vlive import VLiveIE
|
||||||
from .vodlocker import VodlockerIE
|
from .vodlocker import VodlockerIE
|
||||||
|
from .voicerepublic import VoiceRepublicIE
|
||||||
from .vporn import VpornIE
|
from .vporn import VpornIE
|
||||||
from .vrt import VRTIE
|
from .vrt import VRTIE
|
||||||
from .vube import VubeIE
|
from .vube import VubeIE
|
||||||
@@ -561,8 +872,12 @@ from .wdr import (
|
|||||||
WDRMobileIE,
|
WDRMobileIE,
|
||||||
WDRMausIE,
|
WDRMausIE,
|
||||||
)
|
)
|
||||||
from .webofstories import WebOfStoriesIE
|
from .webofstories import (
|
||||||
|
WebOfStoriesIE,
|
||||||
|
WebOfStoriesPlaylistIE,
|
||||||
|
)
|
||||||
from .weibo import WeiboIE
|
from .weibo import WeiboIE
|
||||||
|
from .weiqitv import WeiqiTVIE
|
||||||
from .wimp import WimpIE
|
from .wimp import WimpIE
|
||||||
from .wistia import WistiaIE
|
from .wistia import WistiaIE
|
||||||
from .worldstarhiphop import WorldStarHipHopIE
|
from .worldstarhiphop import WorldStarHipHopIE
|
||||||
@@ -570,19 +885,30 @@ from .wrzuta import WrzutaIE
|
|||||||
from .wsj import WSJIE
|
from .wsj import WSJIE
|
||||||
from .xbef import XBefIE
|
from .xbef import XBefIE
|
||||||
from .xboxclips import XboxClipsIE
|
from .xboxclips import XboxClipsIE
|
||||||
from .xhamster import XHamsterIE
|
from .xfileshare import XFileShareIE
|
||||||
|
from .xhamster import (
|
||||||
|
XHamsterIE,
|
||||||
|
XHamsterEmbedIE,
|
||||||
|
)
|
||||||
from .xminus import XMinusIE
|
from .xminus import XMinusIE
|
||||||
from .xnxx import XNXXIE
|
from .xnxx import XNXXIE
|
||||||
from .xvideos import XVideosIE
|
from .xstream import XstreamIE
|
||||||
from .xtube import XTubeUserIE, XTubeIE
|
from .xtube import XTubeUserIE, XTubeIE
|
||||||
from .xuite import XuiteIE
|
from .xuite import XuiteIE
|
||||||
|
from .xvideos import XVideosIE
|
||||||
from .xxxymovies import XXXYMoviesIE
|
from .xxxymovies import XXXYMoviesIE
|
||||||
from .yahoo import (
|
from .yahoo import (
|
||||||
YahooIE,
|
YahooIE,
|
||||||
YahooSearchIE,
|
YahooSearchIE,
|
||||||
)
|
)
|
||||||
from .yam import YamIE
|
from .yam import YamIE
|
||||||
|
from .yandexmusic import (
|
||||||
|
YandexMusicTrackIE,
|
||||||
|
YandexMusicAlbumIE,
|
||||||
|
YandexMusicPlaylistIE,
|
||||||
|
)
|
||||||
from .yesjapan import YesJapanIE
|
from .yesjapan import YesJapanIE
|
||||||
|
from .yinyuetai import YinYueTaiIE
|
||||||
from .ynet import YnetIE
|
from .ynet import YnetIE
|
||||||
from .youjizz import YouJizzIE
|
from .youjizz import YouJizzIE
|
||||||
from .youku import YoukuIE
|
from .youku import YoukuIE
|
||||||
@@ -603,13 +929,16 @@ from .youtube import (
|
|||||||
YoutubeTruncatedIDIE,
|
YoutubeTruncatedIDIE,
|
||||||
YoutubeTruncatedURLIE,
|
YoutubeTruncatedURLIE,
|
||||||
YoutubeUserIE,
|
YoutubeUserIE,
|
||||||
|
YoutubePlaylistsIE,
|
||||||
YoutubeWatchLaterIE,
|
YoutubeWatchLaterIE,
|
||||||
)
|
)
|
||||||
|
from .zapiks import ZapiksIE
|
||||||
from .zdf import ZDFIE, ZDFChannelIE
|
from .zdf import ZDFIE, ZDFChannelIE
|
||||||
from .zingmp3 import (
|
from .zingmp3 import (
|
||||||
ZingMp3SongIE,
|
ZingMp3SongIE,
|
||||||
ZingMp3AlbumIE,
|
ZingMp3AlbumIE,
|
||||||
)
|
)
|
||||||
|
from .zippcast import ZippCastIE
|
||||||
|
|
||||||
_ALL_CLASSES = [
|
_ALL_CLASSES = [
|
||||||
klass
|
klass
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import json
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
js_to_json,
|
||||||
|
int_or_none,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ABCIE(InfoExtractor):
|
class ABCIE(InfoExtractor):
|
||||||
IE_NAME = 'abc.net.au'
|
IE_NAME = 'abc.net.au'
|
||||||
_VALID_URL = r'http://www\.abc\.net\.au/news/[^/]+/[^/]+/(?P<id>\d+)'
|
_VALID_URL = r'http://www\.abc\.net\.au/news/(?:[^/]+/){1,2}(?P<id>\d+)'
|
||||||
|
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'http://www.abc.net.au/news/2014-11-05/australia-to-staff-ebola-treatment-centre-in-sierra-leone/5868334',
|
'url': 'http://www.abc.net.au/news/2014-11-05/australia-to-staff-ebola-treatment-centre-in-sierra-leone/5868334',
|
||||||
'md5': 'cb3dd03b18455a661071ee1e28344d9f',
|
'md5': 'cb3dd03b18455a661071ee1e28344d9f',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -19,23 +23,67 @@ class ABCIE(InfoExtractor):
|
|||||||
'title': 'Australia to help staff Ebola treatment centre in Sierra Leone',
|
'title': 'Australia to help staff Ebola treatment centre in Sierra Leone',
|
||||||
'description': 'md5:809ad29c67a05f54eb41f2a105693a67',
|
'description': 'md5:809ad29c67a05f54eb41f2a105693a67',
|
||||||
},
|
},
|
||||||
}
|
'skip': 'this video has expired',
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.abc.net.au/news/2015-08-17/warren-entsch-introduces-same-sex-marriage-bill/6702326',
|
||||||
|
'md5': 'db2a5369238b51f9811ad815b69dc086',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'NvqvPeNZsHU',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'upload_date': '20150816',
|
||||||
|
'uploader': 'ABC News (Australia)',
|
||||||
|
'description': 'Government backbencher Warren Entsch introduces a cross-party sponsored bill to legalise same-sex marriage, saying the bill is designed to promote "an inclusive Australia, not a divided one.". Read more here: http://ab.co/1Mwc6ef',
|
||||||
|
'uploader_id': 'NewsOnABC',
|
||||||
|
'title': 'Marriage Equality: Warren Entsch introduces same sex marriage bill',
|
||||||
|
},
|
||||||
|
'add_ie': ['Youtube'],
|
||||||
|
'skip': 'Not accessible from Travis CI server',
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.abc.net.au/news/2015-10-23/nab-lifts-interest-rates-following-westpac-and-cba/6880080',
|
||||||
|
'md5': 'b96eee7c9edf4fc5a358a0252881cc1f',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '6880080',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'title': 'NAB lifts interest rates, following Westpac and CBA',
|
||||||
|
'description': 'md5:f13d8edc81e462fce4a0437c7dc04728',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.abc.net.au/news/2015-10-19/6866214',
|
||||||
|
'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(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
urls_info_json = self._search_regex(
|
mobj = re.search(
|
||||||
r'inlineVideoData\.push\((.*?)\);', webpage, 'video urls',
|
r'inline(?P<type>Video|Audio|YouTube)Data\.push\((?P<json_data>[^)]+)\);',
|
||||||
flags=re.DOTALL)
|
webpage)
|
||||||
urls_info = json.loads(urls_info_json.replace('\'', '"'))
|
if mobj is None:
|
||||||
|
expired = self._html_search_regex(r'(?s)class="expired-(?:video|audio)".+?<span>(.+?)</span>', webpage, 'expired', None)
|
||||||
|
if expired:
|
||||||
|
raise ExtractorError('%s said: %s' % (self.IE_NAME, expired), expected=True)
|
||||||
|
raise ExtractorError('Unable to extract video urls')
|
||||||
|
|
||||||
|
urls_info = self._parse_json(
|
||||||
|
mobj.group('json_data'), video_id, transform_source=js_to_json)
|
||||||
|
|
||||||
|
if not isinstance(urls_info, list):
|
||||||
|
urls_info = [urls_info]
|
||||||
|
|
||||||
|
if mobj.group('type') == 'YouTube':
|
||||||
|
return self.playlist_result([
|
||||||
|
self.url_result(url_info['url']) for url_info in urls_info])
|
||||||
|
|
||||||
formats = [{
|
formats = [{
|
||||||
'url': url_info['url'],
|
'url': url_info['url'],
|
||||||
'width': int(url_info['width']),
|
'vcodec': url_info.get('codec') if mobj.group('type') == 'Video' else 'none',
|
||||||
'height': int(url_info['height']),
|
'width': int_or_none(url_info.get('width')),
|
||||||
'tbr': int(url_info['bitrate']),
|
'height': int_or_none(url_info.get('height')),
|
||||||
'filesize': int(url_info['filesize']),
|
'tbr': int_or_none(url_info.get('bitrate')),
|
||||||
|
'filesize': int_or_none(url_info.get('filesize')),
|
||||||
} for url_info in urls_info]
|
} for url_info in urls_info]
|
||||||
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ class Abc7NewsIE(InfoExtractor):
|
|||||||
'contentURL', webpage, 'm3u8 url', fatal=True)
|
'contentURL', webpage, 'm3u8 url', fatal=True)
|
||||||
|
|
||||||
formats = self._extract_m3u8_formats(m3u8, display_id, 'mp4')
|
formats = self._extract_m3u8_formats(m3u8, display_id, 'mp4')
|
||||||
self._sort_formats(formats)
|
|
||||||
|
|
||||||
title = self._og_search_title(webpage).strip()
|
title = self._og_search_title(webpage).strip()
|
||||||
description = self._og_search_description(webpage).strip()
|
description = self._og_search_description(webpage).strip()
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class AcademicEarthCourseIE(InfoExtractor):
|
|||||||
'title': 'Laws of Nature',
|
'title': 'Laws of Nature',
|
||||||
'description': 'Introduce yourself to the laws of nature with these free online college lectures from Yale, Harvard, and MIT.',
|
'description': 'Introduce yourself to the laws of nature with these free online college lectures from Yale, Harvard, and MIT.',
|
||||||
},
|
},
|
||||||
'playlist_count': 4,
|
'playlist_count': 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
|||||||
72
youtube_dl/extractor/acast.py
Normal file
72
youtube_dl/extractor/acast.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
class ACastIE(InfoExtractor):
|
||||||
|
IE_NAME = 'acast'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?acast\.com/(?P<channel>[^/]+)/(?P<id>[^/#?]+)'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'https://www.acast.com/condenasttraveler/-where-are-you-taipei-101-taiwan',
|
||||||
|
'md5': 'ada3de5a1e3a2a381327d749854788bb',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '57de3baa-4bb0-487e-9418-2692c1277a34',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'title': '"Where Are You?": Taipei 101, Taiwan',
|
||||||
|
'timestamp': 1196172000000,
|
||||||
|
'description': 'md5:a0b4ef3634e63866b542e5b1199a1a0e',
|
||||||
|
'duration': 211,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
channel, display_id = re.match(self._VALID_URL, url).groups()
|
||||||
|
|
||||||
|
embed_page = self._download_webpage(
|
||||||
|
re.sub('(?:www\.)?acast\.com', 'embedcdn.acast.com', url), display_id)
|
||||||
|
cast_data = self._parse_json(self._search_regex(
|
||||||
|
r'window\[\'acast/queries\'\]\s*=\s*([^;]+);', embed_page, 'acast data'),
|
||||||
|
display_id)['GetAcast/%s/%s' % (channel, display_id)]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': compat_str(cast_data['id']),
|
||||||
|
'display_id': display_id,
|
||||||
|
'url': cast_data['blings'][0]['audio'],
|
||||||
|
'title': cast_data['name'],
|
||||||
|
'description': cast_data.get('description'),
|
||||||
|
'thumbnail': cast_data.get('image'),
|
||||||
|
'timestamp': int_or_none(cast_data.get('publishingDate')),
|
||||||
|
'duration': int_or_none(cast_data.get('duration')),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ACastChannelIE(InfoExtractor):
|
||||||
|
IE_NAME = 'acast:channel'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?acast\.com/(?P<id>[^/#?]+)'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'https://www.acast.com/condenasttraveler',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '50544219-29bb-499e-a083-6087f4cb7797',
|
||||||
|
'title': 'Condé Nast Traveler Podcast',
|
||||||
|
'description': 'md5:98646dee22a5b386626ae31866638fbd',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 20,
|
||||||
|
}
|
||||||
|
_API_BASE_URL = 'https://www.acast.com/api/'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def suitable(cls, url):
|
||||||
|
return False if ACastIE.suitable(url) else super(ACastChannelIE, cls).suitable(url)
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id = self._match_id(url)
|
||||||
|
channel_data = self._download_json(self._API_BASE_URL + 'channels/%s' % display_id, display_id)
|
||||||
|
casts = self._download_json(self._API_BASE_URL + 'channels/%s/acasts' % display_id, display_id)
|
||||||
|
entries = [self.url_result('https://www.acast.com/%s/%s' % (display_id, cast['url']), 'ACast') for cast in casts]
|
||||||
|
|
||||||
|
return self.playlist_result(entries, compat_str(channel_data['id']), channel_data['name'], channel_data.get('description'))
|
||||||
@@ -11,12 +11,13 @@ from ..compat import (
|
|||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
qualities,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AddAnimeIE(InfoExtractor):
|
class AddAnimeIE(InfoExtractor):
|
||||||
_VALID_URL = r'^http://(?:\w+\.)?add-anime\.net/watch_video\.php\?(?:.*?)v=(?P<id>[\w_]+)(?:.*)'
|
_VALID_URL = r'http://(?:\w+\.)?add-anime\.net/(?:watch_video\.php\?(?:.*?)v=|video/)(?P<id>[\w_]+)'
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'http://www.add-anime.net/watch_video.php?v=24MR3YO5SAS9',
|
'url': 'http://www.add-anime.net/watch_video.php?v=24MR3YO5SAS9',
|
||||||
'md5': '72954ea10bc979ab5e2eb288b21425a0',
|
'md5': '72954ea10bc979ab5e2eb288b21425a0',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -25,7 +26,10 @@ class AddAnimeIE(InfoExtractor):
|
|||||||
'description': 'One Piece 606',
|
'description': 'One Piece 606',
|
||||||
'title': 'One Piece 606',
|
'title': 'One Piece 606',
|
||||||
}
|
}
|
||||||
}
|
}, {
|
||||||
|
'url': 'http://add-anime.net/video/MDUGWYKNGBD8/One-Piece-687',
|
||||||
|
'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)
|
||||||
@@ -63,8 +67,10 @@ class AddAnimeIE(InfoExtractor):
|
|||||||
note='Confirming after redirect')
|
note='Confirming after redirect')
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
FORMATS = ('normal', 'hq')
|
||||||
|
quality = qualities(FORMATS)
|
||||||
formats = []
|
formats = []
|
||||||
for format_id in ('normal', 'hq'):
|
for format_id in FORMATS:
|
||||||
rex = r"var %s_video_file = '(.*?)';" % re.escape(format_id)
|
rex = r"var %s_video_file = '(.*?)';" % re.escape(format_id)
|
||||||
video_url = self._search_regex(rex, webpage, 'video file URLx',
|
video_url = self._search_regex(rex, webpage, 'video file URLx',
|
||||||
fatal=False)
|
fatal=False)
|
||||||
@@ -73,6 +79,7 @@ class AddAnimeIE(InfoExtractor):
|
|||||||
formats.append({
|
formats.append({
|
||||||
'format_id': format_id,
|
'format_id': format_id,
|
||||||
'url': video_url,
|
'url': video_url,
|
||||||
|
'quality': quality(format_id),
|
||||||
})
|
})
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
video_title = self._og_search_title(webpage)
|
video_title = self._og_search_title(webpage)
|
||||||
|
|||||||
@@ -1,21 +1,32 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
from ..compat import compat_str
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
parse_duration,
|
parse_duration,
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
str_to_int,
|
str_to_int,
|
||||||
|
int_or_none,
|
||||||
|
float_or_none,
|
||||||
|
ISO639Utils,
|
||||||
|
determine_ext,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AdobeTVIE(InfoExtractor):
|
class AdobeTVBaseIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://tv\.adobe\.com/watch/[^/]+/(?P<id>[^/]+)'
|
_API_BASE_URL = 'http://tv.adobe.com/api/v4/'
|
||||||
|
|
||||||
|
|
||||||
|
class AdobeTVIE(AdobeTVBaseIE):
|
||||||
|
_VALID_URL = r'https?://tv\.adobe\.com/(?:(?P<language>fr|de|es|jp)/)?watch/(?P<show_urlname>[^/]+)/(?P<id>[^/]+)'
|
||||||
|
|
||||||
_TEST = {
|
_TEST = {
|
||||||
'url': 'http://tv.adobe.com/watch/the-complete-picture-with-julieanne-kost/quick-tip-how-to-draw-a-circle-around-an-object-in-photoshop/',
|
'url': 'http://tv.adobe.com/watch/the-complete-picture-with-julieanne-kost/quick-tip-how-to-draw-a-circle-around-an-object-in-photoshop/',
|
||||||
'md5': '9bc5727bcdd55251f35ad311ca74fa1e',
|
'md5': '9bc5727bcdd55251f35ad311ca74fa1e',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'quick-tip-how-to-draw-a-circle-around-an-object-in-photoshop',
|
'id': '10981',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Quick Tip - How to Draw a Circle Around an Object in Photoshop',
|
'title': 'Quick Tip - How to Draw a Circle Around an Object in Photoshop',
|
||||||
'description': 'md5:99ec318dc909d7ba2a1f2b038f7d2311',
|
'description': 'md5:99ec318dc909d7ba2a1f2b038f7d2311',
|
||||||
@@ -27,44 +38,157 @@ class AdobeTVIE(InfoExtractor):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
language, show_urlname, urlname = re.match(self._VALID_URL, url).groups()
|
||||||
|
if not language:
|
||||||
|
language = 'en'
|
||||||
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
video_data = self._download_json(
|
||||||
|
self._API_BASE_URL + 'episode/get/?language=%s&show_urlname=%s&urlname=%s&disclosure=standard' % (language, show_urlname, urlname),
|
||||||
player = self._parse_json(
|
urlname)['data'][0]
|
||||||
self._search_regex(r'html5player:\s*({.+?})\s*\n', webpage, 'player'),
|
|
||||||
video_id)
|
|
||||||
|
|
||||||
title = player.get('title') or self._search_regex(
|
|
||||||
r'data-title="([^"]+)"', webpage, 'title')
|
|
||||||
description = self._og_search_description(webpage)
|
|
||||||
thumbnail = self._og_search_thumbnail(webpage)
|
|
||||||
|
|
||||||
upload_date = unified_strdate(
|
|
||||||
self._html_search_meta('datepublished', webpage, 'upload date'))
|
|
||||||
|
|
||||||
duration = parse_duration(
|
|
||||||
self._html_search_meta('duration', webpage, 'duration')
|
|
||||||
or self._search_regex(r'Runtime:\s*(\d{2}:\d{2}:\d{2})', webpage, 'duration'))
|
|
||||||
|
|
||||||
view_count = str_to_int(self._search_regex(
|
|
||||||
r'<div class="views">\s*Views?:\s*([\d,.]+)\s*</div>',
|
|
||||||
webpage, 'view count'))
|
|
||||||
|
|
||||||
formats = [{
|
formats = [{
|
||||||
'url': source['src'],
|
'url': source['url'],
|
||||||
'format_id': source.get('quality') or source['src'].split('-')[-1].split('.')[0] or None,
|
'format_id': source.get('quality_level') or source['url'].split('-')[-1].split('.')[0] or None,
|
||||||
'tbr': source.get('bitrate'),
|
'width': int_or_none(source.get('width')),
|
||||||
} for source in player['sources']]
|
'height': int_or_none(source.get('height')),
|
||||||
|
'tbr': int_or_none(source.get('video_data_rate')),
|
||||||
|
} for source in video_data['videos']]
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': compat_str(video_data['id']),
|
||||||
'title': title,
|
'title': video_data['title'],
|
||||||
'description': description,
|
'description': video_data.get('description'),
|
||||||
'thumbnail': thumbnail,
|
'thumbnail': video_data.get('thumbnail'),
|
||||||
'upload_date': upload_date,
|
'upload_date': unified_strdate(video_data.get('start_date')),
|
||||||
'duration': duration,
|
'duration': parse_duration(video_data.get('duration')),
|
||||||
'view_count': view_count,
|
'view_count': str_to_int(video_data.get('playcount')),
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AdobeTVPlaylistBaseIE(AdobeTVBaseIE):
|
||||||
|
def _parse_page_data(self, page_data):
|
||||||
|
return [self.url_result(self._get_element_url(element_data)) for element_data in page_data]
|
||||||
|
|
||||||
|
def _extract_playlist_entries(self, url, display_id):
|
||||||
|
page = self._download_json(url, display_id)
|
||||||
|
entries = self._parse_page_data(page['data'])
|
||||||
|
for page_num in range(2, page['paging']['pages'] + 1):
|
||||||
|
entries.extend(self._parse_page_data(
|
||||||
|
self._download_json(url + '&page=%d' % page_num, display_id)['data']))
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
class AdobeTVShowIE(AdobeTVPlaylistBaseIE):
|
||||||
|
_VALID_URL = r'https?://tv\.adobe\.com/(?:(?P<language>fr|de|es|jp)/)?show/(?P<id>[^/]+)'
|
||||||
|
|
||||||
|
_TEST = {
|
||||||
|
'url': 'http://tv.adobe.com/show/the-complete-picture-with-julieanne-kost',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '36',
|
||||||
|
'title': 'The Complete Picture with Julieanne Kost',
|
||||||
|
'description': 'md5:fa50867102dcd1aa0ddf2ab039311b27',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 136,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_element_url(self, element_data):
|
||||||
|
return element_data['urls'][0]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
language, show_urlname = re.match(self._VALID_URL, url).groups()
|
||||||
|
if not language:
|
||||||
|
language = 'en'
|
||||||
|
query = 'language=%s&show_urlname=%s' % (language, show_urlname)
|
||||||
|
|
||||||
|
show_data = self._download_json(self._API_BASE_URL + 'show/get/?%s' % query, show_urlname)['data'][0]
|
||||||
|
|
||||||
|
return self.playlist_result(
|
||||||
|
self._extract_playlist_entries(self._API_BASE_URL + 'episode/?%s' % query, show_urlname),
|
||||||
|
compat_str(show_data['id']),
|
||||||
|
show_data['show_name'],
|
||||||
|
show_data['show_description'])
|
||||||
|
|
||||||
|
|
||||||
|
class AdobeTVChannelIE(AdobeTVPlaylistBaseIE):
|
||||||
|
_VALID_URL = r'https?://tv\.adobe\.com/(?:(?P<language>fr|de|es|jp)/)?channel/(?P<id>[^/]+)(?:/(?P<category_urlname>[^/]+))?'
|
||||||
|
|
||||||
|
_TEST = {
|
||||||
|
'url': 'http://tv.adobe.com/channel/development',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'development',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 96,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_element_url(self, element_data):
|
||||||
|
return element_data['url']
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
language, channel_urlname, category_urlname = re.match(self._VALID_URL, url).groups()
|
||||||
|
if not language:
|
||||||
|
language = 'en'
|
||||||
|
query = 'language=%s&channel_urlname=%s' % (language, channel_urlname)
|
||||||
|
if category_urlname:
|
||||||
|
query += '&category_urlname=%s' % category_urlname
|
||||||
|
|
||||||
|
return self.playlist_result(
|
||||||
|
self._extract_playlist_entries(self._API_BASE_URL + 'show/?%s' % query, channel_urlname),
|
||||||
|
channel_urlname)
|
||||||
|
|
||||||
|
|
||||||
|
class AdobeTVVideoIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://video\.tv\.adobe\.com/v/(?P<id>\d+)'
|
||||||
|
|
||||||
|
_TEST = {
|
||||||
|
# From https://helpx.adobe.com/acrobat/how-to/new-experience-acrobat-dc.html?set=acrobat--get-started--essential-beginners
|
||||||
|
'url': 'https://video.tv.adobe.com/v/2456/',
|
||||||
|
'md5': '43662b577c018ad707a63766462b1e87',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '2456',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'New experience with Acrobat DC',
|
||||||
|
'description': 'New experience with Acrobat DC',
|
||||||
|
'duration': 248.667,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
video_data = self._download_json(url + '?format=json', video_id)
|
||||||
|
|
||||||
|
formats = [{
|
||||||
|
'format_id': '%s-%s' % (determine_ext(source['src']), source.get('height')),
|
||||||
|
'url': source['src'],
|
||||||
|
'width': int_or_none(source.get('width')),
|
||||||
|
'height': int_or_none(source.get('height')),
|
||||||
|
'tbr': int_or_none(source.get('bitrate')),
|
||||||
|
} for source in video_data['sources']]
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
# For both metadata and downloaded files the duration varies among
|
||||||
|
# formats. I just pick the max one
|
||||||
|
duration = max(filter(None, [
|
||||||
|
float_or_none(source.get('duration'), scale=1000)
|
||||||
|
for source in video_data['sources']]))
|
||||||
|
|
||||||
|
subtitles = {}
|
||||||
|
for translation in video_data.get('translations', []):
|
||||||
|
lang_id = translation.get('language_w3c') or ISO639Utils.long2short(translation['language_medium'])
|
||||||
|
if lang_id not in subtitles:
|
||||||
|
subtitles[lang_id] = []
|
||||||
|
subtitles[lang_id].append({
|
||||||
|
'url': translation['vttPath'],
|
||||||
|
'ext': 'vtt',
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'formats': formats,
|
||||||
|
'title': video_data['title'],
|
||||||
|
'description': video_data.get('description'),
|
||||||
|
'thumbnail': video_data['video'].get('poster'),
|
||||||
|
'duration': duration,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import json
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
determine_ext,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
xpath_text,
|
|
||||||
float_or_none,
|
float_or_none,
|
||||||
|
xpath_text,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -41,7 +41,8 @@ class AdultSwimIE(InfoExtractor):
|
|||||||
'id': 'rQxZvXQ4ROaSOqq-or2Mow',
|
'id': 'rQxZvXQ4ROaSOqq-or2Mow',
|
||||||
'title': 'Rick and Morty - Pilot',
|
'title': 'Rick and Morty - Pilot',
|
||||||
'description': "Rick moves in with his daughter's family and establishes himself as a bad influence on his grandson, Morty. "
|
'description': "Rick moves in with his daughter's family and establishes himself as a bad influence on his grandson, Morty. "
|
||||||
}
|
},
|
||||||
|
'skip': 'This video is only available for registered users',
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.adultswim.com/videos/playlists/american-parenting/putting-francine-out-of-business/',
|
'url': 'http://www.adultswim.com/videos/playlists/american-parenting/putting-francine-out-of-business/',
|
||||||
'playlist': [
|
'playlist': [
|
||||||
@@ -60,6 +61,28 @@ class AdultSwimIE(InfoExtractor):
|
|||||||
'title': 'American Dad - Putting Francine Out of Business',
|
'title': 'American Dad - Putting Francine Out of Business',
|
||||||
'description': 'Stan hatches a plan to get Francine out of the real estate business.Watch more American Dad on [adult swim].'
|
'description': 'Stan hatches a plan to get Francine out of the real estate business.Watch more American Dad on [adult swim].'
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.adultswim.com/videos/tim-and-eric-awesome-show-great-job/dr-steve-brule-for-your-wine/',
|
||||||
|
'playlist': [
|
||||||
|
{
|
||||||
|
'md5': '3e346a2ab0087d687a05e1e7f3b3e529',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'sY3cMUR_TbuE4YmdjzbIcQ-0',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Tim and Eric Awesome Show Great Job! - Dr. Steve Brule, For Your Wine',
|
||||||
|
'description': 'Dr. Brule reports live from Wine Country with a special report on wines. \r\nWatch Tim and Eric Awesome Show Great Job! episode #20, "Embarrassed" on Adult Swim.\r\n\r\n',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'sY3cMUR_TbuE4YmdjzbIcQ',
|
||||||
|
'title': 'Tim and Eric Awesome Show Great Job! - Dr. Steve Brule, For Your Wine',
|
||||||
|
'description': 'Dr. Brule reports live from Wine Country with a special report on wines. \r\nWatch Tim and Eric Awesome Show Great Job! episode #20, "Embarrassed" on Adult Swim.\r\n\r\n',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# m3u8 download
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
}]
|
}]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -80,6 +103,7 @@ class AdultSwimIE(InfoExtractor):
|
|||||||
for video in collection.get('videos'):
|
for video in collection.get('videos'):
|
||||||
if video.get('slug') == slug:
|
if video.get('slug') == slug:
|
||||||
return collection, video
|
return collection, video
|
||||||
|
return None, None
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
@@ -90,30 +114,39 @@ class AdultSwimIE(InfoExtractor):
|
|||||||
webpage = self._download_webpage(url, episode_path)
|
webpage = self._download_webpage(url, episode_path)
|
||||||
|
|
||||||
# Extract the value of `bootstrappedData` from the Javascript in the page.
|
# Extract the value of `bootstrappedData` from the Javascript in the page.
|
||||||
bootstrappedDataJS = self._search_regex(r'var bootstrappedData = ({.*});', webpage, episode_path)
|
bootstrapped_data = self._parse_json(self._search_regex(
|
||||||
|
r'var bootstrappedData = ({.*});', webpage, 'bootstraped data'), episode_path)
|
||||||
try:
|
|
||||||
bootstrappedData = json.loads(bootstrappedDataJS)
|
|
||||||
except ValueError as ve:
|
|
||||||
errmsg = '%s: Failed to parse JSON ' % episode_path
|
|
||||||
raise ExtractorError(errmsg, cause=ve)
|
|
||||||
|
|
||||||
# Downloading videos from a /videos/playlist/ URL needs to be handled differently.
|
# Downloading videos from a /videos/playlist/ URL needs to be handled differently.
|
||||||
# NOTE: We are only downloading one video (the current one) not the playlist
|
# NOTE: We are only downloading one video (the current one) not the playlist
|
||||||
if is_playlist:
|
if is_playlist:
|
||||||
collections = bootstrappedData['playlists']['collections']
|
collections = bootstrapped_data['playlists']['collections']
|
||||||
collection = self.find_collection_by_linkURL(collections, show_path)
|
collection = self.find_collection_by_linkURL(collections, show_path)
|
||||||
video_info = self.find_video_info(collection, episode_path)
|
video_info = self.find_video_info(collection, episode_path)
|
||||||
|
|
||||||
show_title = video_info['showTitle']
|
show_title = video_info['showTitle']
|
||||||
segment_ids = [video_info['videoPlaybackID']]
|
segment_ids = [video_info['videoPlaybackID']]
|
||||||
else:
|
else:
|
||||||
collections = bootstrappedData['show']['collections']
|
collections = bootstrapped_data['show']['collections']
|
||||||
collection, video_info = self.find_collection_containing_video(collections, episode_path)
|
collection, video_info = self.find_collection_containing_video(collections, episode_path)
|
||||||
|
# Video wasn't found in the collections, let's try `slugged_video`.
|
||||||
|
if video_info is None:
|
||||||
|
if bootstrapped_data.get('slugged_video', {}).get('slug') == episode_path:
|
||||||
|
video_info = bootstrapped_data['slugged_video']
|
||||||
|
else:
|
||||||
|
raise ExtractorError('Unable to find video info')
|
||||||
|
|
||||||
show = bootstrappedData['show']
|
show = bootstrapped_data['show']
|
||||||
show_title = show['title']
|
show_title = show['title']
|
||||||
segment_ids = [clip['videoPlaybackID'] for clip in video_info['clips']]
|
stream = video_info.get('stream')
|
||||||
|
clips = [stream] if stream else video_info.get('clips')
|
||||||
|
if not clips:
|
||||||
|
raise ExtractorError(
|
||||||
|
'This video is only available via cable service provider subscription that'
|
||||||
|
' is not currently supported. You may want to use --cookies.'
|
||||||
|
if video_info.get('auth') is True else 'Unable to find stream or clips',
|
||||||
|
expected=True)
|
||||||
|
segment_ids = [clip['videoPlaybackID'] for clip in clips]
|
||||||
|
|
||||||
episode_id = video_info['id']
|
episode_id = video_info['id']
|
||||||
episode_title = video_info['title']
|
episode_title = video_info['title']
|
||||||
@@ -122,7 +155,7 @@ class AdultSwimIE(InfoExtractor):
|
|||||||
|
|
||||||
entries = []
|
entries = []
|
||||||
for part_num, segment_id in enumerate(segment_ids):
|
for part_num, segment_id in enumerate(segment_ids):
|
||||||
segment_url = 'http://www.adultswim.com/videos/api/v0/assets?id=%s&platform=mobile' % segment_id
|
segment_url = 'http://www.adultswim.com/videos/api/v0/assets?id=%s&platform=desktop' % segment_id
|
||||||
|
|
||||||
segment_title = '%s - %s' % (show_title, episode_title)
|
segment_title = '%s - %s' % (show_title, episode_title)
|
||||||
if len(segment_ids) > 1:
|
if len(segment_ids) > 1:
|
||||||
@@ -136,19 +169,33 @@ class AdultSwimIE(InfoExtractor):
|
|||||||
xpath_text(idoc, './/trt', 'segment duration').strip())
|
xpath_text(idoc, './/trt', 'segment duration').strip())
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
file_els = idoc.findall('.//files/file')
|
file_els = idoc.findall('.//files/file') or idoc.findall('./files/file')
|
||||||
|
|
||||||
|
unique_urls = []
|
||||||
|
unique_file_els = []
|
||||||
for file_el in file_els:
|
for file_el in file_els:
|
||||||
|
media_url = file_el.text
|
||||||
|
if not media_url or determine_ext(media_url) == 'f4m':
|
||||||
|
continue
|
||||||
|
if file_el.text not in unique_urls:
|
||||||
|
unique_urls.append(file_el.text)
|
||||||
|
unique_file_els.append(file_el)
|
||||||
|
|
||||||
|
for file_el in unique_file_els:
|
||||||
bitrate = file_el.attrib.get('bitrate')
|
bitrate = file_el.attrib.get('bitrate')
|
||||||
ftype = file_el.attrib.get('type')
|
ftype = file_el.attrib.get('type')
|
||||||
|
media_url = file_el.text
|
||||||
formats.append({
|
if determine_ext(media_url) == 'm3u8':
|
||||||
'format_id': '%s_%s' % (bitrate, ftype),
|
formats.extend(self._extract_m3u8_formats(
|
||||||
'url': file_el.text.strip(),
|
media_url, segment_title, 'mp4', preference=0,
|
||||||
# The bitrate may not be a number (for example: 'iphone')
|
m3u8_id='hls', fatal=False))
|
||||||
'tbr': int(bitrate) if bitrate.isdigit() else None,
|
else:
|
||||||
'quality': 1 if ftype == 'hd' else -1
|
formats.append({
|
||||||
})
|
'format_id': '%s_%s' % (bitrate, ftype),
|
||||||
|
'url': file_el.text.strip(),
|
||||||
|
# The bitrate may not be a number (for example: 'iphone')
|
||||||
|
'tbr': int(bitrate) if bitrate.isdigit() else None,
|
||||||
|
})
|
||||||
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
|||||||
66
youtube_dl/extractor/aenetworks.py
Normal file
66
youtube_dl/extractor/aenetworks.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import smuggle_url
|
||||||
|
|
||||||
|
|
||||||
|
class AENetworksIE(InfoExtractor):
|
||||||
|
IE_NAME = 'aenetworks'
|
||||||
|
IE_DESC = 'A+E Networks: A&E, Lifetime, History.com, FYI Network'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?(?:(?:history|aetv|mylifetime)\.com|fyi\.tv)/(?:[^/]+/)+(?P<id>[^/]+?)(?:$|[?#])'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://www.history.com/topics/valentines-day/history-of-valentines-day/videos/bet-you-didnt-know-valentines-day?m=528e394da93ae&s=undefined&f=1&free=false',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'g12m5Gyt3fdR',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': "Bet You Didn't Know: Valentine's Day",
|
||||||
|
'description': 'md5:7b57ea4829b391995b405fa60bd7b5f7',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# m3u8 download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
'add_ie': ['ThePlatform'],
|
||||||
|
'expected_warnings': ['JSON-LD'],
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.history.com/shows/mountain-men/season-1/episode-1',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'eg47EERs_JsZ',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': "Winter Is Coming",
|
||||||
|
'description': 'md5:641f424b7a19d8e24f26dea22cf59d74',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# m3u8 download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
'add_ie': ['ThePlatform'],
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.aetv.com/shows/duck-dynasty/video/inlawful-entry',
|
||||||
|
'only_matching': True
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.fyi.tv/shows/tiny-house-nation/videos/207-sq-ft-minnesota-prairie-cottage',
|
||||||
|
'only_matching': True
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.mylifetime.com/shows/project-runway-junior/video/season-1/episode-6/superstar-clients',
|
||||||
|
'only_matching': True
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
video_url_re = [
|
||||||
|
r'data-href="[^"]*/%s"[^>]+data-release-url="([^"]+)"' % video_id,
|
||||||
|
r"media_url\s*=\s*'([^']+)'"
|
||||||
|
]
|
||||||
|
video_url = self._search_regex(video_url_re, webpage, 'video url')
|
||||||
|
|
||||||
|
info = self._search_json_ld(webpage, video_id, fatal=False)
|
||||||
|
info.update({
|
||||||
|
'_type': 'url_transparent',
|
||||||
|
'url': smuggle_url(video_url, {'sig': {'key': 'crazyjava', 'secret': 's3cr3t'}}),
|
||||||
|
})
|
||||||
|
return info
|
||||||
@@ -2,14 +2,15 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
from ..utils import int_or_none
|
||||||
|
|
||||||
|
|
||||||
class AftonbladetIE(InfoExtractor):
|
class AftonbladetIE(InfoExtractor):
|
||||||
_VALID_URL = r'^http://tv\.aftonbladet\.se/webbtv.+?(?P<video_id>article[0-9]+)\.ab(?:$|[?#])'
|
_VALID_URL = r'http://tv\.aftonbladet\.se/abtv/articles/(?P<id>[0-9]+)'
|
||||||
_TEST = {
|
_TEST = {
|
||||||
'url': 'http://tv.aftonbladet.se/webbtv/nyheter/vetenskap/rymden/article36015.ab',
|
'url': 'http://tv.aftonbladet.se/abtv/articles/36015',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'article36015',
|
'id': '36015',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Vulkanutbrott i rymden - nu släpper NASA bilderna',
|
'title': 'Vulkanutbrott i rymden - nu släpper NASA bilderna',
|
||||||
'description': 'Jupiters måne mest aktiv av alla himlakroppar',
|
'description': 'Jupiters måne mest aktiv av alla himlakroppar',
|
||||||
@@ -24,8 +25,9 @@ class AftonbladetIE(InfoExtractor):
|
|||||||
|
|
||||||
# find internal video meta data
|
# find internal video meta data
|
||||||
meta_url = 'http://aftonbladet-play.drlib.aptoma.no/video/%s.json'
|
meta_url = 'http://aftonbladet-play.drlib.aptoma.no/video/%s.json'
|
||||||
internal_meta_id = self._html_search_regex(
|
player_config = self._parse_json(self._html_search_regex(
|
||||||
r'data-aptomaId="([\w\d]+)"', webpage, 'internal_meta_id')
|
r'data-player-config="([^"]+)"', webpage, 'player config'), video_id)
|
||||||
|
internal_meta_id = player_config['videoId']
|
||||||
internal_meta_url = meta_url % internal_meta_id
|
internal_meta_url = meta_url % internal_meta_id
|
||||||
internal_meta_json = self._download_json(
|
internal_meta_json = self._download_json(
|
||||||
internal_meta_url, video_id, 'Downloading video meta data')
|
internal_meta_url, video_id, 'Downloading video meta data')
|
||||||
@@ -43,9 +45,9 @@ class AftonbladetIE(InfoExtractor):
|
|||||||
formats.append({
|
formats.append({
|
||||||
'url': 'http://%s:%d/%s/%s' % (p['address'], p['port'], p['path'], p['filename']),
|
'url': 'http://%s:%d/%s/%s' % (p['address'], p['port'], p['path'], p['filename']),
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'width': fmt['width'],
|
'width': int_or_none(fmt.get('width')),
|
||||||
'height': fmt['height'],
|
'height': int_or_none(fmt.get('height')),
|
||||||
'tbr': fmt['bitrate'],
|
'tbr': int_or_none(fmt.get('bitrate')),
|
||||||
'protocol': 'http',
|
'protocol': 'http',
|
||||||
})
|
})
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
@@ -54,9 +56,9 @@ class AftonbladetIE(InfoExtractor):
|
|||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': internal_meta_json['title'],
|
'title': internal_meta_json['title'],
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'thumbnail': internal_meta_json['imageUrl'],
|
'thumbnail': internal_meta_json.get('imageUrl'),
|
||||||
'description': internal_meta_json['shortPreamble'],
|
'description': internal_meta_json.get('shortPreamble'),
|
||||||
'timestamp': internal_meta_json['timePublished'],
|
'timestamp': int_or_none(internal_meta_json.get('timePublished')),
|
||||||
'duration': internal_meta_json['duration'],
|
'duration': int_or_none(internal_meta_json.get('duration')),
|
||||||
'view_count': internal_meta_json['views'],
|
'view_count': int_or_none(internal_meta_json.get('views')),
|
||||||
}
|
}
|
||||||
|
|||||||
74
youtube_dl/extractor/airmozilla.py
Normal file
74
youtube_dl/extractor/airmozilla.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
int_or_none,
|
||||||
|
parse_duration,
|
||||||
|
parse_iso8601,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AirMozillaIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://air\.mozilla\.org/(?P<id>[0-9a-z-]+)/?'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'https://air.mozilla.org/privacy-lab-a-meetup-for-privacy-minded-people-in-san-francisco/',
|
||||||
|
'md5': '2e3e7486ba5d180e829d453875b9b8bf',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '6x4q2w',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Privacy Lab - a meetup for privacy minded people in San Francisco',
|
||||||
|
'thumbnail': 're:https?://vid\.ly/(?P<id>[0-9a-z-]+)/poster',
|
||||||
|
'description': 'Brings together privacy professionals and others interested in privacy at for-profits, non-profits, and NGOs in an effort to contribute to the state of the ecosystem...',
|
||||||
|
'timestamp': 1422487800,
|
||||||
|
'upload_date': '20150128',
|
||||||
|
'location': 'SFO Commons',
|
||||||
|
'duration': 3780,
|
||||||
|
'view_count': int,
|
||||||
|
'categories': ['Main', 'Privacy'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
video_id = self._html_search_regex(r'//vid.ly/(.*?)/embed', webpage, 'id')
|
||||||
|
|
||||||
|
embed_script = self._download_webpage('https://vid.ly/{0}/embed'.format(video_id), video_id)
|
||||||
|
jwconfig = self._search_regex(r'\svar jwconfig = (\{.*?\});\s', embed_script, 'metadata')
|
||||||
|
metadata = self._parse_json(jwconfig, video_id)
|
||||||
|
|
||||||
|
formats = [{
|
||||||
|
'url': source['file'],
|
||||||
|
'ext': source['type'],
|
||||||
|
'format_id': self._search_regex(r'&format=(.*)$', source['file'], 'video format'),
|
||||||
|
'format': source['label'],
|
||||||
|
'height': int(source['label'].rstrip('p')),
|
||||||
|
} for source in metadata['playlist'][0]['sources']]
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
view_count = int_or_none(self._html_search_regex(
|
||||||
|
r'Views since archived: ([0-9]+)',
|
||||||
|
webpage, 'view count', fatal=False))
|
||||||
|
timestamp = parse_iso8601(self._html_search_regex(
|
||||||
|
r'<time datetime="(.*?)"', webpage, 'timestamp', fatal=False))
|
||||||
|
duration = parse_duration(self._search_regex(
|
||||||
|
r'Duration:\s*(\d+\s*hours?\s*\d+\s*minutes?)',
|
||||||
|
webpage, 'duration', fatal=False))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': self._og_search_title(webpage),
|
||||||
|
'formats': formats,
|
||||||
|
'url': self._og_search_url(webpage),
|
||||||
|
'display_id': display_id,
|
||||||
|
'thumbnail': metadata['playlist'][0].get('image'),
|
||||||
|
'description': self._og_search_description(webpage),
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'location': self._html_search_regex(r'Location: (.*)', webpage, 'location', default=None),
|
||||||
|
'duration': duration,
|
||||||
|
'view_count': view_count,
|
||||||
|
'categories': re.findall(r'<a href=".*?" class="channel">(.*?)</a>', webpage),
|
||||||
|
}
|
||||||
@@ -15,7 +15,8 @@ class AlJazeeraIE(InfoExtractor):
|
|||||||
'description': 'As a birth attendant advocating for family planning, Remy is on the frontline of Tondo\'s battle with overcrowding.',
|
'description': 'As a birth attendant advocating for family planning, Remy is on the frontline of Tondo\'s battle with overcrowding.',
|
||||||
'uploader': 'Al Jazeera English',
|
'uploader': 'Al Jazeera English',
|
||||||
},
|
},
|
||||||
'add_ie': ['Brightcove'],
|
'add_ie': ['BrightcoveLegacy'],
|
||||||
|
'skip': 'Not accessible from Travis CI server',
|
||||||
}
|
}
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@@ -31,5 +32,5 @@ class AlJazeeraIE(InfoExtractor):
|
|||||||
'playerKey=AQ~~%2CAAAAmtVJIFk~%2CTVGOQ5ZTwJbeMWnq5d_H4MOM57xfzApc'
|
'playerKey=AQ~~%2CAAAAmtVJIFk~%2CTVGOQ5ZTwJbeMWnq5d_H4MOM57xfzApc'
|
||||||
'&%40videoPlayer={0}'.format(brightcove_id)
|
'&%40videoPlayer={0}'.format(brightcove_id)
|
||||||
),
|
),
|
||||||
'ie_key': 'Brightcove',
|
'ie_key': 'BrightcoveLegacy',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ from .common import InfoExtractor
|
|||||||
from ..compat import compat_str
|
from ..compat import compat_str
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
qualities,
|
qualities,
|
||||||
|
unescapeHTML,
|
||||||
|
xpath_element,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -31,7 +33,7 @@ class AllocineIE(InfoExtractor):
|
|||||||
'id': '19540403',
|
'id': '19540403',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Planes 2 Bande-annonce VF',
|
'title': 'Planes 2 Bande-annonce VF',
|
||||||
'description': 'md5:eeaffe7c2d634525e21159b93acf3b1e',
|
'description': 'Regardez la bande annonce du film Planes 2 (Planes 2 Bande-annonce VF). Planes 2, un film de Roberts Gannaway',
|
||||||
'thumbnail': 're:http://.*\.jpg',
|
'thumbnail': 're:http://.*\.jpg',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
@@ -41,7 +43,7 @@ class AllocineIE(InfoExtractor):
|
|||||||
'id': '19544709',
|
'id': '19544709',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Dragons 2 - Bande annonce finale VF',
|
'title': 'Dragons 2 - Bande annonce finale VF',
|
||||||
'description': 'md5:71742e3a74b0d692c7fce0dd2017a4ac',
|
'description': 'md5:601d15393ac40f249648ef000720e7e3',
|
||||||
'thumbnail': 're:http://.*\.jpg',
|
'thumbnail': 're:http://.*\.jpg',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
@@ -59,14 +61,18 @@ class AllocineIE(InfoExtractor):
|
|||||||
if typ == 'film':
|
if typ == 'film':
|
||||||
video_id = self._search_regex(r'href="/video/player_gen_cmedia=([0-9]+).+"', webpage, 'video id')
|
video_id = self._search_regex(r'href="/video/player_gen_cmedia=([0-9]+).+"', webpage, 'video id')
|
||||||
else:
|
else:
|
||||||
player = self._search_regex(r'data-player=\'([^\']+)\'>', webpage, 'data player')
|
player = self._search_regex(r'data-player=\'([^\']+)\'>', webpage, 'data player', default=None)
|
||||||
|
if player:
|
||||||
player_data = json.loads(player)
|
player_data = json.loads(player)
|
||||||
video_id = compat_str(player_data['refMedia'])
|
video_id = compat_str(player_data['refMedia'])
|
||||||
|
else:
|
||||||
|
model = self._search_regex(r'data-model="([^"]+)">', webpage, 'data model')
|
||||||
|
model_data = self._parse_json(unescapeHTML(model), display_id)
|
||||||
|
video_id = compat_str(model_data['id'])
|
||||||
|
|
||||||
xml = self._download_xml('http://www.allocine.fr/ws/AcVisiondataV4.ashx?media=%s' % video_id, display_id)
|
xml = self._download_xml('http://www.allocine.fr/ws/AcVisiondataV4.ashx?media=%s' % video_id, display_id)
|
||||||
|
|
||||||
video = xml.find('.//AcVisionVideo').attrib
|
video = xpath_element(xml, './/AcVisionVideo').attrib
|
||||||
quality = qualities(['ld', 'md', 'hd'])
|
quality = qualities(['ld', 'md', 'hd'])
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
|
|||||||
81
youtube_dl/extractor/amp.py
Normal file
81
youtube_dl/extractor/amp.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
int_or_none,
|
||||||
|
parse_iso8601,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AMPIE(InfoExtractor):
|
||||||
|
# parse Akamai Adaptive Media Player feed
|
||||||
|
def _extract_feed_info(self, url):
|
||||||
|
item = self._download_json(
|
||||||
|
url, None, 'Downloading Akamai AMP feed',
|
||||||
|
'Unable to download Akamai AMP feed')['channel']['item']
|
||||||
|
|
||||||
|
video_id = item['guid']
|
||||||
|
|
||||||
|
def get_media_node(name, default=None):
|
||||||
|
media_name = 'media-%s' % name
|
||||||
|
media_group = item.get('media-group') or item
|
||||||
|
return media_group.get(media_name) or item.get(media_name) or item.get(name, default)
|
||||||
|
|
||||||
|
thumbnails = []
|
||||||
|
media_thumbnail = get_media_node('thumbnail')
|
||||||
|
if media_thumbnail:
|
||||||
|
if isinstance(media_thumbnail, dict):
|
||||||
|
media_thumbnail = [media_thumbnail]
|
||||||
|
for thumbnail_data in media_thumbnail:
|
||||||
|
thumbnail = thumbnail_data['@attributes']
|
||||||
|
thumbnails.append({
|
||||||
|
'url': self._proto_relative_url(thumbnail['url'], 'http:'),
|
||||||
|
'width': int_or_none(thumbnail.get('width')),
|
||||||
|
'height': int_or_none(thumbnail.get('height')),
|
||||||
|
})
|
||||||
|
|
||||||
|
subtitles = {}
|
||||||
|
media_subtitle = get_media_node('subTitle')
|
||||||
|
if media_subtitle:
|
||||||
|
if isinstance(media_subtitle, dict):
|
||||||
|
media_subtitle = [media_subtitle]
|
||||||
|
for subtitle_data in media_subtitle:
|
||||||
|
subtitle = subtitle_data['@attributes']
|
||||||
|
lang = subtitle.get('lang') or 'en'
|
||||||
|
subtitles[lang] = [{'url': subtitle['href']}]
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
media_content = get_media_node('content')
|
||||||
|
if isinstance(media_content, dict):
|
||||||
|
media_content = [media_content]
|
||||||
|
for media_data in media_content:
|
||||||
|
media = media_data['@attributes']
|
||||||
|
media_type = media['type']
|
||||||
|
if media_type == 'video/f4m':
|
||||||
|
formats.extend(self._extract_f4m_formats(
|
||||||
|
media['url'] + '?hdcore=3.4.0&plugin=aasp-3.4.0.132.124',
|
||||||
|
video_id, f4m_id='hds', fatal=False))
|
||||||
|
elif media_type == 'application/x-mpegURL':
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
media['url'], video_id, 'mp4', m3u8_id='hls', fatal=False))
|
||||||
|
else:
|
||||||
|
formats.append({
|
||||||
|
'format_id': media_data['media-category']['@attributes']['label'],
|
||||||
|
'url': media['url'],
|
||||||
|
'tbr': int_or_none(media.get('bitrate')),
|
||||||
|
'filesize': int_or_none(media.get('fileSize')),
|
||||||
|
})
|
||||||
|
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': get_media_node('title'),
|
||||||
|
'description': get_media_node('description'),
|
||||||
|
'thumbnails': thumbnails,
|
||||||
|
'timestamp': parse_iso8601(item.get('pubDate'), ' '),
|
||||||
|
'duration': int_or_none(media_content[0].get('@attributes', {}).get('duration')),
|
||||||
|
'subtitles': subtitles,
|
||||||
|
'formats': formats,
|
||||||
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import re
|
from .nuevo import NuevoBaseIE
|
||||||
|
|
||||||
from .common import InfoExtractor
|
|
||||||
|
|
||||||
|
|
||||||
class AnitubeIE(InfoExtractor):
|
class AnitubeIE(NuevoBaseIE):
|
||||||
IE_NAME = 'anitube.se'
|
IE_NAME = 'anitube.se'
|
||||||
_VALID_URL = r'https?://(?:www\.)?anitube\.se/video/(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:www\.)?anitube\.se/video/(?P<id>\d+)'
|
||||||
|
|
||||||
@@ -22,38 +20,11 @@ class AnitubeIE(InfoExtractor):
|
|||||||
}
|
}
|
||||||
|
|
||||||
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('id')
|
|
||||||
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
key = self._html_search_regex(
|
key = self._search_regex(
|
||||||
r'http://www\.anitube\.se/embed/([A-Za-z0-9_-]*)', webpage, 'key')
|
r'src=["\']https?://[^/]+/embed/([A-Za-z0-9_-]+)', webpage, 'key')
|
||||||
|
|
||||||
config_xml = self._download_xml(
|
return self._extract_nuevo(
|
||||||
'http://www.anitube.se/nuevo/econfig.php?key=%s' % key, key)
|
'http://www.anitube.se/nuevo/econfig.php?key=%s' % key, video_id)
|
||||||
|
|
||||||
video_title = config_xml.find('title').text
|
|
||||||
thumbnail = config_xml.find('image').text
|
|
||||||
duration = float(config_xml.find('duration').text)
|
|
||||||
|
|
||||||
formats = []
|
|
||||||
video_url = config_xml.find('file')
|
|
||||||
if video_url is not None:
|
|
||||||
formats.append({
|
|
||||||
'format_id': 'sd',
|
|
||||||
'url': video_url.text,
|
|
||||||
})
|
|
||||||
video_url = config_xml.find('filehd')
|
|
||||||
if video_url is not None:
|
|
||||||
formats.append({
|
|
||||||
'format_id': 'hd',
|
|
||||||
'url': video_url.text,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': video_id,
|
|
||||||
'title': video_title,
|
|
||||||
'thumbnail': thumbnail,
|
|
||||||
'duration': duration,
|
|
||||||
'formats': formats
|
|
||||||
}
|
|
||||||
|
|||||||
50
youtube_dl/extractor/appleconnect.py
Normal file
50
youtube_dl/extractor/appleconnect.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
str_to_int,
|
||||||
|
ExtractorError
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AppleConnectIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://itunes\.apple\.com/\w{0,2}/?post/idsa\.(?P<id>[\w-]+)'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'https://itunes.apple.com/us/post/idsa.4ab17a39-2720-11e5-96c5-a5b38f6c42d3',
|
||||||
|
'md5': '10d0f2799111df4cb1c924520ca78f98',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '4ab17a39-2720-11e5-96c5-a5b38f6c42d3',
|
||||||
|
'ext': 'm4v',
|
||||||
|
'title': 'Energy',
|
||||||
|
'uploader': 'Drake',
|
||||||
|
'thumbnail': 'http://is5.mzstatic.com/image/thumb/Video5/v4/78/61/c5/7861c5fa-ad6d-294b-1464-cf7605b911d6/source/1920x1080sr.jpg',
|
||||||
|
'upload_date': '20150710',
|
||||||
|
'timestamp': 1436545535,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
video_json = self._html_search_regex(
|
||||||
|
r'class="auc-video-data">(\{.*?\})', webpage, 'json')
|
||||||
|
except ExtractorError:
|
||||||
|
raise ExtractorError('This post doesn\'t contain a video', expected=True)
|
||||||
|
|
||||||
|
video_data = self._parse_json(video_json, video_id)
|
||||||
|
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'))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'url': video_data['sslSrc'],
|
||||||
|
'title': video_data['title'],
|
||||||
|
'description': video_data['description'],
|
||||||
|
'uploader': video_data['artistName'],
|
||||||
|
'thumbnail': video_data['artworkUrl'],
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'like_count': like_count,
|
||||||
|
}
|
||||||
@@ -11,59 +11,69 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class AppleTrailersIE(InfoExtractor):
|
class AppleTrailersIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?trailers\.apple\.com/trailers/(?P<company>[^/]+)/(?P<movie>[^/]+)'
|
IE_NAME = 'appletrailers'
|
||||||
_TEST = {
|
_VALID_URL = r'https?://(?:www\.)?trailers\.apple\.com/(?:trailers|ca)/(?P<company>[^/]+)/(?P<movie>[^/]+)'
|
||||||
"url": "http://trailers.apple.com/trailers/wb/manofsteel/",
|
_TESTS = [{
|
||||||
|
'url': 'http://trailers.apple.com/trailers/wb/manofsteel/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'manofsteel',
|
'id': 'manofsteel',
|
||||||
},
|
},
|
||||||
"playlist": [
|
'playlist': [
|
||||||
{
|
{
|
||||||
"md5": "d97a8e575432dbcb81b7c3acb741f8a8",
|
'md5': 'd97a8e575432dbcb81b7c3acb741f8a8',
|
||||||
"info_dict": {
|
'info_dict': {
|
||||||
"id": "manofsteel-trailer4",
|
'id': 'manofsteel-trailer4',
|
||||||
"ext": "mov",
|
'ext': 'mov',
|
||||||
"duration": 111,
|
'duration': 111,
|
||||||
"title": "Trailer 4",
|
'title': 'Trailer 4',
|
||||||
"upload_date": "20130523",
|
'upload_date': '20130523',
|
||||||
"uploader_id": "wb",
|
'uploader_id': 'wb',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"md5": "b8017b7131b721fb4e8d6f49e1df908c",
|
'md5': 'b8017b7131b721fb4e8d6f49e1df908c',
|
||||||
"info_dict": {
|
'info_dict': {
|
||||||
"id": "manofsteel-trailer3",
|
'id': 'manofsteel-trailer3',
|
||||||
"ext": "mov",
|
'ext': 'mov',
|
||||||
"duration": 182,
|
'duration': 182,
|
||||||
"title": "Trailer 3",
|
'title': 'Trailer 3',
|
||||||
"upload_date": "20130417",
|
'upload_date': '20130417',
|
||||||
"uploader_id": "wb",
|
'uploader_id': 'wb',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"md5": "d0f1e1150989b9924679b441f3404d48",
|
'md5': 'd0f1e1150989b9924679b441f3404d48',
|
||||||
"info_dict": {
|
'info_dict': {
|
||||||
"id": "manofsteel-trailer",
|
'id': 'manofsteel-trailer',
|
||||||
"ext": "mov",
|
'ext': 'mov',
|
||||||
"duration": 148,
|
'duration': 148,
|
||||||
"title": "Trailer",
|
'title': 'Trailer',
|
||||||
"upload_date": "20121212",
|
'upload_date': '20121212',
|
||||||
"uploader_id": "wb",
|
'uploader_id': 'wb',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"md5": "5fe08795b943eb2e757fa95cb6def1cb",
|
'md5': '5fe08795b943eb2e757fa95cb6def1cb',
|
||||||
"info_dict": {
|
'info_dict': {
|
||||||
"id": "manofsteel-teaser",
|
'id': 'manofsteel-teaser',
|
||||||
"ext": "mov",
|
'ext': 'mov',
|
||||||
"duration": 93,
|
'duration': 93,
|
||||||
"title": "Teaser",
|
'title': 'Teaser',
|
||||||
"upload_date": "20120721",
|
'upload_date': '20120721',
|
||||||
"uploader_id": "wb",
|
'uploader_id': 'wb',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}, {
|
||||||
|
'url': 'http://trailers.apple.com/trailers/magnolia/blackthorn/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'blackthorn',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 2,
|
||||||
|
}, {
|
||||||
|
'url': 'http://trailers.apple.com/ca/metropole/autrui/',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
_JSON_RE = r'iTunes.playURL\((.*?)\);'
|
_JSON_RE = r'iTunes.playURL\((.*?)\);'
|
||||||
|
|
||||||
@@ -76,7 +86,7 @@ class AppleTrailersIE(InfoExtractor):
|
|||||||
|
|
||||||
def fix_html(s):
|
def fix_html(s):
|
||||||
s = re.sub(r'(?s)<script[^<]*?>.*?</script>', '', s)
|
s = re.sub(r'(?s)<script[^<]*?>.*?</script>', '', s)
|
||||||
s = re.sub(r'<img ([^<]*?)>', r'<img \1/>', s)
|
s = re.sub(r'<img ([^<]*?)/?>', r'<img \1/>', s)
|
||||||
# The ' in the onClick attributes are not escaped, it couldn't be parsed
|
# The ' in the onClick attributes are not escaped, it couldn't be parsed
|
||||||
# like: http://trailers.apple.com/trailers/wb/gravity/
|
# like: http://trailers.apple.com/trailers/wb/gravity/
|
||||||
|
|
||||||
@@ -93,6 +103,9 @@ class AppleTrailersIE(InfoExtractor):
|
|||||||
trailer_info_json = self._search_regex(self._JSON_RE,
|
trailer_info_json = self._search_regex(self._JSON_RE,
|
||||||
on_click, 'trailer info')
|
on_click, 'trailer info')
|
||||||
trailer_info = json.loads(trailer_info_json)
|
trailer_info = json.loads(trailer_info_json)
|
||||||
|
first_url = trailer_info.get('url')
|
||||||
|
if not first_url:
|
||||||
|
continue
|
||||||
title = trailer_info['title']
|
title = trailer_info['title']
|
||||||
video_id = movie + '-' + re.sub(r'[^a-zA-Z0-9]', '', title).lower()
|
video_id = movie + '-' + re.sub(r'[^a-zA-Z0-9]', '', title).lower()
|
||||||
thumbnail = li.find('.//img').attrib['src']
|
thumbnail = li.find('.//img').attrib['src']
|
||||||
@@ -104,7 +117,6 @@ class AppleTrailersIE(InfoExtractor):
|
|||||||
if m:
|
if m:
|
||||||
duration = 60 * int(m.group('minutes')) + int(m.group('seconds'))
|
duration = 60 * int(m.group('minutes')) + int(m.group('seconds'))
|
||||||
|
|
||||||
first_url = trailer_info['url']
|
|
||||||
trailer_id = first_url.split('/')[-1].rpartition('_')[0].lower()
|
trailer_id = first_url.split('/')[-1].rpartition('_')[0].lower()
|
||||||
settings_json_url = compat_urlparse.urljoin(url, 'includes/settings/%s.json' % trailer_id)
|
settings_json_url = compat_urlparse.urljoin(url, 'includes/settings/%s.json' % trailer_id)
|
||||||
settings = self._download_json(settings_json_url, trailer_id, 'Downloading settings json')
|
settings = self._download_json(settings_json_url, trailer_id, 'Downloading settings json')
|
||||||
@@ -141,3 +153,76 @@ class AppleTrailersIE(InfoExtractor):
|
|||||||
'id': movie,
|
'id': movie,
|
||||||
'entries': playlist,
|
'entries': playlist,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AppleTrailersSectionIE(InfoExtractor):
|
||||||
|
IE_NAME = 'appletrailers:section'
|
||||||
|
_SECTIONS = {
|
||||||
|
'justadded': {
|
||||||
|
'feed_path': 'just_added',
|
||||||
|
'title': 'Just Added',
|
||||||
|
},
|
||||||
|
'exclusive': {
|
||||||
|
'feed_path': 'exclusive',
|
||||||
|
'title': 'Exclusive',
|
||||||
|
},
|
||||||
|
'justhd': {
|
||||||
|
'feed_path': 'just_hd',
|
||||||
|
'title': 'Just HD',
|
||||||
|
},
|
||||||
|
'mostpopular': {
|
||||||
|
'feed_path': 'most_pop',
|
||||||
|
'title': 'Most Popular',
|
||||||
|
},
|
||||||
|
'moviestudios': {
|
||||||
|
'feed_path': 'studios',
|
||||||
|
'title': 'Movie Studios',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?trailers\.apple\.com/#section=(?P<id>%s)' % '|'.join(_SECTIONS)
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://trailers.apple.com/#section=justadded',
|
||||||
|
'info_dict': {
|
||||||
|
'title': 'Just Added',
|
||||||
|
'id': 'justadded',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 80,
|
||||||
|
}, {
|
||||||
|
'url': 'http://trailers.apple.com/#section=exclusive',
|
||||||
|
'info_dict': {
|
||||||
|
'title': 'Exclusive',
|
||||||
|
'id': 'exclusive',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 80,
|
||||||
|
}, {
|
||||||
|
'url': 'http://trailers.apple.com/#section=justhd',
|
||||||
|
'info_dict': {
|
||||||
|
'title': 'Just HD',
|
||||||
|
'id': 'justhd',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 80,
|
||||||
|
}, {
|
||||||
|
'url': 'http://trailers.apple.com/#section=mostpopular',
|
||||||
|
'info_dict': {
|
||||||
|
'title': 'Most Popular',
|
||||||
|
'id': 'mostpopular',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 80,
|
||||||
|
}, {
|
||||||
|
'url': 'http://trailers.apple.com/#section=moviestudios',
|
||||||
|
'info_dict': {
|
||||||
|
'title': 'Movie Studios',
|
||||||
|
'id': 'moviestudios',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 80,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
section = self._match_id(url)
|
||||||
|
section_data = self._download_json(
|
||||||
|
'http://trailers.apple.com/trailers/home/feeds/%s.json' % self._SECTIONS[section]['feed_path'],
|
||||||
|
section)
|
||||||
|
entries = [
|
||||||
|
self.url_result('http://trailers.apple.com' + e['location'])
|
||||||
|
for e in section_data]
|
||||||
|
return self.playlist_result(entries, section, self._SECTIONS[section]['title'])
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class ArchiveOrgIE(InfoExtractor):
|
|||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
json_url = url + ('?' if '?' in url else '&') + 'output=json'
|
json_url = url + ('&' if '?' in url else '?') + 'output=json'
|
||||||
data = self._download_json(json_url, video_id)
|
data = self._download_json(json_url, video_id)
|
||||||
|
|
||||||
def get_optional(data_dict, field):
|
def get_optional(data_dict, field):
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ from .generic import GenericIE
|
|||||||
from ..utils import (
|
from ..utils import (
|
||||||
determine_ext,
|
determine_ext,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
get_element_by_attribute,
|
||||||
qualities,
|
qualities,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_duration,
|
parse_duration,
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
xpath_text,
|
xpath_text,
|
||||||
parse_xml,
|
|
||||||
)
|
)
|
||||||
|
from ..compat import compat_etree_fromstring
|
||||||
|
|
||||||
|
|
||||||
class ARDMediathekIE(InfoExtractor):
|
class ARDMediathekIE(InfoExtractor):
|
||||||
@@ -22,19 +23,127 @@ class ARDMediathekIE(InfoExtractor):
|
|||||||
_VALID_URL = r'^https?://(?:(?:www\.)?ardmediathek\.de|mediathek\.daserste\.de)/(?:.*/)(?P<video_id>[0-9]+|[^0-9][^/\?]+)[^/\?]*(?:\?.*)?'
|
_VALID_URL = r'^https?://(?:(?:www\.)?ardmediathek\.de|mediathek\.daserste\.de)/(?:.*/)(?P<video_id>[0-9]+|[^0-9][^/\?]+)[^/\?]*(?:\?.*)?'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
'url': 'http://www.ardmediathek.de/tv/Dokumentation-und-Reportage/Ich-liebe-das-Leben-trotzdem/rbb-Fernsehen/Video?documentId=29582122&bcastId=3822114',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '29582122',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Ich liebe das Leben trotzdem',
|
||||||
|
'description': 'md5:45e4c225c72b27993314b31a84a5261c',
|
||||||
|
'duration': 4557,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# m3u8 download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.ardmediathek.de/tv/Tatort/Tatort-Scheinwelten-H%C3%B6rfassung-Video/Das-Erste/Video?documentId=29522730&bcastId=602916',
|
||||||
|
'md5': 'f4d98b10759ac06c0072bbcd1f0b9e3e',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '29522730',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Tatort: Scheinwelten - Hörfassung (Video tgl. ab 20 Uhr)',
|
||||||
|
'description': 'md5:196392e79876d0ac94c94e8cdb2875f1',
|
||||||
|
'duration': 5252,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# audio
|
||||||
|
'url': 'http://www.ardmediathek.de/tv/WDR-H%C3%B6rspiel-Speicher/Tod-eines-Fu%C3%9Fballers/WDR-3/Audio-Podcast?documentId=28488308&bcastId=23074086',
|
||||||
|
'md5': '219d94d8980b4f538c7fcb0865eb7f2c',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '28488308',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'title': 'Tod eines Fußballers',
|
||||||
|
'description': 'md5:f6e39f3461f0e1f54bfa48c8875c86ef',
|
||||||
|
'duration': 3240,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
'url': 'http://mediathek.daserste.de/sendungen_a-z/328454_anne-will/22429276_vertrauen-ist-gut-spionieren-ist-besser-geht',
|
'url': 'http://mediathek.daserste.de/sendungen_a-z/328454_anne-will/22429276_vertrauen-ist-gut-spionieren-ist-besser-geht',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
|
||||||
'url': 'http://www.ardmediathek.de/tv/Tatort/Das-Wunder-von-Wolbeck-Video-tgl-ab-20/Das-Erste/Video?documentId=22490580&bcastId=602916',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '22490580',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Das Wunder von Wolbeck (Video tgl. ab 20 Uhr)',
|
|
||||||
'description': 'Auf einem restaurierten Hof bei Wolbeck wird der Heilpraktiker Raffael Lembeck eines morgens von seiner Frau Stella tot aufgefunden. Das Opfer war offensichtlich in seiner Praxis zu Fall gekommen und ist dann verblutet, erklärt Prof. Boerne am Tatort.',
|
|
||||||
},
|
|
||||||
'skip': 'Blocked outside of Germany',
|
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
def _extract_media_info(self, media_info_url, webpage, video_id):
|
||||||
|
media_info = self._download_json(
|
||||||
|
media_info_url, video_id, 'Downloading media JSON')
|
||||||
|
|
||||||
|
formats = self._extract_formats(media_info, video_id)
|
||||||
|
|
||||||
|
if not formats:
|
||||||
|
if '"fsk"' in webpage:
|
||||||
|
raise ExtractorError(
|
||||||
|
'This video is only available after 20:00', expected=True)
|
||||||
|
elif media_info.get('_geoblocked'):
|
||||||
|
raise ExtractorError('This video is not available due to geo restriction', expected=True)
|
||||||
|
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
duration = int_or_none(media_info.get('_duration'))
|
||||||
|
thumbnail = media_info.get('_previewImage')
|
||||||
|
|
||||||
|
subtitles = {}
|
||||||
|
subtitle_url = media_info.get('_subtitleUrl')
|
||||||
|
if subtitle_url:
|
||||||
|
subtitles['de'] = [{
|
||||||
|
'ext': 'srt',
|
||||||
|
'url': subtitle_url,
|
||||||
|
}]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'duration': duration,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _extract_formats(self, media_info, video_id):
|
||||||
|
type_ = media_info.get('_type')
|
||||||
|
media_array = media_info.get('_mediaArray', [])
|
||||||
|
formats = []
|
||||||
|
for num, media in enumerate(media_array):
|
||||||
|
for stream in media.get('_mediaStreamArray', []):
|
||||||
|
stream_urls = stream.get('_stream')
|
||||||
|
if not stream_urls:
|
||||||
|
continue
|
||||||
|
if not isinstance(stream_urls, list):
|
||||||
|
stream_urls = [stream_urls]
|
||||||
|
quality = stream.get('_quality')
|
||||||
|
server = stream.get('_server')
|
||||||
|
for stream_url in stream_urls:
|
||||||
|
ext = determine_ext(stream_url)
|
||||||
|
if quality != 'auto' and ext in ('f4m', 'm3u8'):
|
||||||
|
continue
|
||||||
|
if ext == 'f4m':
|
||||||
|
formats.extend(self._extract_f4m_formats(
|
||||||
|
stream_url + '?hdcore=3.1.1&plugin=aasp-3.1.1.69.124',
|
||||||
|
video_id, preference=-1, f4m_id='hds', fatal=False))
|
||||||
|
elif ext == 'm3u8':
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
stream_url, video_id, 'mp4', preference=1, m3u8_id='hls', fatal=False))
|
||||||
|
else:
|
||||||
|
if server and server.startswith('rtmp'):
|
||||||
|
f = {
|
||||||
|
'url': server,
|
||||||
|
'play_path': stream_url,
|
||||||
|
'format_id': 'a%s-rtmp-%s' % (num, quality),
|
||||||
|
}
|
||||||
|
elif stream_url.startswith('http'):
|
||||||
|
f = {
|
||||||
|
'url': stream_url,
|
||||||
|
'format_id': 'a%s-%s-%s' % (num, ext, quality)
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
m = re.search(r'_(?P<width>\d+)x(?P<height>\d+)\.mp4$', stream_url)
|
||||||
|
if m:
|
||||||
|
f.update({
|
||||||
|
'width': int(m.group('width')),
|
||||||
|
'height': int(m.group('height')),
|
||||||
|
})
|
||||||
|
if type_ == 'audio':
|
||||||
|
f['vcodec'] = 'none'
|
||||||
|
formats.append(f)
|
||||||
|
return formats
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
# determine video id from url
|
# determine video id from url
|
||||||
m = re.match(self._VALID_URL, url)
|
m = re.match(self._VALID_URL, url)
|
||||||
@@ -50,8 +159,11 @@ class ARDMediathekIE(InfoExtractor):
|
|||||||
if '>Der gewünschte Beitrag ist nicht mehr verfügbar.<' in webpage:
|
if '>Der gewünschte Beitrag ist nicht mehr verfügbar.<' in webpage:
|
||||||
raise ExtractorError('Video %s is no longer available' % video_id, expected=True)
|
raise ExtractorError('Video %s is no longer available' % video_id, expected=True)
|
||||||
|
|
||||||
|
if 'Diese Sendung ist für Jugendliche unter 12 Jahren nicht geeignet. Der Clip ist deshalb nur von 20 bis 6 Uhr verfügbar.' in webpage:
|
||||||
|
raise ExtractorError('This program is only suitable for those aged 12 and older. Video %s is therefore only available between 20 pm and 6 am.' % video_id, expected=True)
|
||||||
|
|
||||||
if re.search(r'[\?&]rss($|[=&])', url):
|
if re.search(r'[\?&]rss($|[=&])', url):
|
||||||
doc = parse_xml(webpage)
|
doc = compat_etree_fromstring(webpage.encode('utf-8'))
|
||||||
if doc.tag == 'rss':
|
if doc.tag == 'rss':
|
||||||
return GenericIE()._extract_rss(url, video_id, doc)
|
return GenericIE()._extract_rss(url, video_id, doc)
|
||||||
|
|
||||||
@@ -89,46 +201,22 @@ class ARDMediathekIE(InfoExtractor):
|
|||||||
'format_id': fid,
|
'format_id': fid,
|
||||||
'url': furl,
|
'url': furl,
|
||||||
})
|
})
|
||||||
|
self._sort_formats(formats)
|
||||||
|
info = {
|
||||||
|
'formats': formats,
|
||||||
|
}
|
||||||
else: # request JSON file
|
else: # request JSON file
|
||||||
media_info = self._download_json(
|
info = self._extract_media_info(
|
||||||
'http://www.ardmediathek.de/play/media/%s' % video_id, video_id)
|
'http://www.ardmediathek.de/play/media/%s' % video_id, webpage, video_id)
|
||||||
# The second element of the _mediaArray contains the standard http urls
|
|
||||||
streams = media_info['_mediaArray'][1]['_mediaStreamArray']
|
|
||||||
if not streams:
|
|
||||||
if '"fsk"' in webpage:
|
|
||||||
raise ExtractorError('This video is only available after 20:00')
|
|
||||||
|
|
||||||
formats = []
|
info.update({
|
||||||
for s in streams:
|
|
||||||
if type(s['_stream']) == list:
|
|
||||||
for index, url in enumerate(s['_stream'][::-1]):
|
|
||||||
quality = s['_quality'] + index
|
|
||||||
formats.append({
|
|
||||||
'quality': quality,
|
|
||||||
'url': url,
|
|
||||||
'format_id': '%s-%s' % (determine_ext(url), quality)
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
|
|
||||||
format = {
|
|
||||||
'quality': s['_quality'],
|
|
||||||
'url': s['_stream'],
|
|
||||||
}
|
|
||||||
|
|
||||||
format['format_id'] = '%s-%s' % (
|
|
||||||
determine_ext(format['url']), format['quality'])
|
|
||||||
|
|
||||||
formats.append(format)
|
|
||||||
|
|
||||||
self._sort_formats(formats)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': description,
|
'description': description,
|
||||||
'formats': formats,
|
|
||||||
'thumbnail': thumbnail,
|
'thumbnail': thumbnail,
|
||||||
}
|
})
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
class ARDIE(InfoExtractor):
|
class ARDIE(InfoExtractor):
|
||||||
@@ -186,3 +274,41 @@ class ARDIE(InfoExtractor):
|
|||||||
'upload_date': upload_date,
|
'upload_date': upload_date,
|
||||||
'thumbnail': thumbnail,
|
'thumbnail': thumbnail,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SportschauIE(ARDMediathekIE):
|
||||||
|
IE_NAME = 'Sportschau'
|
||||||
|
_VALID_URL = r'(?P<baseurl>https?://(?:www\.)?sportschau\.de/(?:[^/]+/)+video(?P<id>[^/#?]+))\.html'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://www.sportschau.de/tourdefrance/videoseppeltkokainhatnichtsmitklassischemdopingzutun100.html',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'seppeltkokainhatnichtsmitklassischemdopingzutun100',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Seppelt: "Kokain hat nichts mit klassischem Doping zu tun"',
|
||||||
|
'thumbnail': 're:^https?://.*\.jpg$',
|
||||||
|
'description': 'Der ARD-Doping Experte Hajo Seppelt gibt seine Einschätzung zum ersten Dopingfall der diesjährigen Tour de France um den Italiener Luca Paolini ab.',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# m3u8 download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
mobj = re.match(self._VALID_URL, url)
|
||||||
|
video_id = mobj.group('id')
|
||||||
|
base_url = mobj.group('baseurl')
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
title = get_element_by_attribute('class', 'headline', webpage)
|
||||||
|
description = self._html_search_meta('description', webpage, 'description')
|
||||||
|
|
||||||
|
info = self._extract_media_info(
|
||||||
|
base_url + '-mc_defaultQuality-h.json', webpage, video_id)
|
||||||
|
|
||||||
|
info.update({
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
})
|
||||||
|
|
||||||
|
return info
|
||||||
|
|||||||
@@ -4,12 +4,16 @@ from __future__ import unicode_literals
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
from ..compat import (
|
||||||
|
compat_parse_qs,
|
||||||
|
compat_urllib_parse_urlparse,
|
||||||
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
find_xpath_attr,
|
find_xpath_attr,
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
get_element_by_id,
|
|
||||||
get_element_by_attribute,
|
get_element_by_attribute,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
|
NO_DEFAULT,
|
||||||
qualities,
|
qualities,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -65,9 +69,13 @@ class ArteTVPlus7IE(InfoExtractor):
|
|||||||
def _extract_url_info(cls, url):
|
def _extract_url_info(cls, url):
|
||||||
mobj = re.match(cls._VALID_URL, url)
|
mobj = re.match(cls._VALID_URL, url)
|
||||||
lang = mobj.group('lang')
|
lang = mobj.group('lang')
|
||||||
# This is not a real id, it can be for example AJT for the news
|
query = compat_parse_qs(compat_urllib_parse_urlparse(url).query)
|
||||||
# http://www.arte.tv/guide/fr/emissions/AJT/arte-journal
|
if 'vid' in query:
|
||||||
video_id = mobj.group('id')
|
video_id = query['vid'][0]
|
||||||
|
else:
|
||||||
|
# This is not a real id, it can be for example AJT for the news
|
||||||
|
# http://www.arte.tv/guide/fr/emissions/AJT/arte-journal
|
||||||
|
video_id = mobj.group('id')
|
||||||
return video_id, lang
|
return video_id, lang
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@@ -76,9 +84,30 @@ class ArteTVPlus7IE(InfoExtractor):
|
|||||||
return self._extract_from_webpage(webpage, video_id, lang)
|
return self._extract_from_webpage(webpage, video_id, lang)
|
||||||
|
|
||||||
def _extract_from_webpage(self, webpage, video_id, lang):
|
def _extract_from_webpage(self, webpage, video_id, lang):
|
||||||
|
patterns_templates = (r'arte_vp_url=["\'](.*?%s.*?)["\']', r'data-url=["\']([^"]+%s[^"]+)["\']')
|
||||||
|
ids = (video_id, '')
|
||||||
|
# some pages contain multiple videos (like
|
||||||
|
# http://www.arte.tv/guide/de/sendungen/XEN/xenius/?vid=055918-015_PLUS7-D),
|
||||||
|
# so we first try to look for json URLs that contain the video id from
|
||||||
|
# the 'vid' parameter.
|
||||||
|
patterns = [t % re.escape(_id) for _id in ids for t in patterns_templates]
|
||||||
json_url = self._html_search_regex(
|
json_url = self._html_search_regex(
|
||||||
[r'arte_vp_url=["\'](.*?)["\']', r'data-url=["\']([^"]+)["\']'],
|
patterns, webpage, 'json vp url', default=None)
|
||||||
webpage, 'json vp url')
|
if not json_url:
|
||||||
|
def find_iframe_url(webpage, default=NO_DEFAULT):
|
||||||
|
return self._html_search_regex(
|
||||||
|
r'<iframe[^>]+src=(["\'])(?P<url>.+\bjson_url=.+?)\1',
|
||||||
|
webpage, 'iframe url', group='url', default=default)
|
||||||
|
|
||||||
|
iframe_url = find_iframe_url(webpage, None)
|
||||||
|
if not iframe_url:
|
||||||
|
embed_url = self._html_search_regex(
|
||||||
|
r'arte_vp_url_oembed=\'([^\']+?)\'', webpage, 'embed url')
|
||||||
|
player = self._download_json(
|
||||||
|
embed_url, video_id, 'Downloading player page')
|
||||||
|
iframe_url = find_iframe_url(player['html'])
|
||||||
|
json_url = compat_parse_qs(
|
||||||
|
compat_urllib_parse_urlparse(iframe_url).query)['json_url'][0]
|
||||||
return self._extract_from_json_url(json_url, video_id, lang)
|
return self._extract_from_json_url(json_url, video_id, lang)
|
||||||
|
|
||||||
def _extract_from_json_url(self, json_url, video_id, lang):
|
def _extract_from_json_url(self, json_url, video_id, lang):
|
||||||
@@ -146,6 +175,7 @@ class ArteTVPlus7IE(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
|
info_dict['formats'] = formats
|
||||||
@@ -179,23 +209,19 @@ class ArteTVCreativeIE(ArteTVPlus7IE):
|
|||||||
|
|
||||||
class ArteTVFutureIE(ArteTVPlus7IE):
|
class ArteTVFutureIE(ArteTVPlus7IE):
|
||||||
IE_NAME = 'arte.tv:future'
|
IE_NAME = 'arte.tv:future'
|
||||||
_VALID_URL = r'https?://future\.arte\.tv/(?P<lang>fr|de)/(thema|sujet)/.*?#article-anchor-(?P<id>\d+)'
|
_VALID_URL = r'https?://future\.arte\.tv/(?P<lang>fr|de)/(?P<id>.+)'
|
||||||
|
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'http://future.arte.tv/fr/sujet/info-sciences#article-anchor-7081',
|
'url': 'http://future.arte.tv/fr/info-sciences/les-ecrevisses-aussi-sont-anxieuses',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '5201',
|
'id': '050940-028-A',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Les champignons au secours de la planète',
|
'title': 'Les écrevisses aussi peuvent être anxieuses',
|
||||||
'upload_date': '20131101',
|
|
||||||
},
|
},
|
||||||
}
|
}, {
|
||||||
|
'url': 'http://future.arte.tv/fr/la-science-est-elle-responsable',
|
||||||
def _real_extract(self, url):
|
'only_matching': True,
|
||||||
anchor_id, lang = self._extract_url_info(url)
|
}]
|
||||||
webpage = self._download_webpage(url, anchor_id)
|
|
||||||
row = get_element_by_id(anchor_id, webpage)
|
|
||||||
return self._extract_from_webpage(row, anchor_id, lang)
|
|
||||||
|
|
||||||
|
|
||||||
class ArteTVDDCIE(ArteTVPlus7IE):
|
class ArteTVDDCIE(ArteTVPlus7IE):
|
||||||
@@ -233,6 +259,23 @@ class ArteTVConcertIE(ArteTVPlus7IE):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ArteTVCinemaIE(ArteTVPlus7IE):
|
||||||
|
IE_NAME = 'arte.tv:cinema'
|
||||||
|
_VALID_URL = r'https?://cinema\.arte\.tv/(?P<lang>de|fr)/(?P<id>.+)'
|
||||||
|
|
||||||
|
_TEST = {
|
||||||
|
'url': 'http://cinema.arte.tv/de/node/38291',
|
||||||
|
'md5': '6b275511a5107c60bacbeeda368c3aa1',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '055876-000_PWA12025-D',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Tod auf dem Nil',
|
||||||
|
'upload_date': '20160122',
|
||||||
|
'description': 'md5:7f749bbb77d800ef2be11d54529b96bc',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ArteTVEmbedIE(ArteTVPlus7IE):
|
class ArteTVEmbedIE(ArteTVPlus7IE):
|
||||||
IE_NAME = 'arte.tv:embed'
|
IE_NAME = 'arte.tv:embed'
|
||||||
_VALID_URL = r'''(?x)
|
_VALID_URL = r'''(?x)
|
||||||
|
|||||||
@@ -2,23 +2,26 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import time
|
import time
|
||||||
import hmac
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
import re
|
||||||
|
|
||||||
from .subtitles import SubtitlesInfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
compat_str,
|
compat_str,
|
||||||
compat_urllib_parse,
|
compat_urllib_parse,
|
||||||
compat_urllib_request,
|
|
||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
int_or_none,
|
int_or_none,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
|
sanitized_Request,
|
||||||
xpath_text,
|
xpath_text,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AtresPlayerIE(SubtitlesInfoExtractor):
|
class AtresPlayerIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?atresplayer\.com/television/[^/]+/[^/]+/[^/]+/(?P<id>.+?)_\d+\.html'
|
_VALID_URL = r'https?://(?:www\.)?atresplayer\.com/television/[^/]+/[^/]+/[^/]+/(?P<id>.+?)_\d+\.html'
|
||||||
|
_NETRC_MACHINE = 'atresplayer'
|
||||||
_TESTS = [
|
_TESTS = [
|
||||||
{
|
{
|
||||||
'url': 'http://www.atresplayer.com/television/programas/el-club-de-la-comedia/temporada-4/capitulo-10-especial-solidario-nochebuena_2014122100174.html',
|
'url': 'http://www.atresplayer.com/television/programas/el-club-de-la-comedia/temporada-4/capitulo-10-especial-solidario-nochebuena_2014122100174.html',
|
||||||
@@ -31,6 +34,19 @@ class AtresPlayerIE(SubtitlesInfoExtractor):
|
|||||||
'duration': 5527.6,
|
'duration': 5527.6,
|
||||||
'thumbnail': 're:^https?://.*\.jpg$',
|
'thumbnail': 're:^https?://.*\.jpg$',
|
||||||
},
|
},
|
||||||
|
'skip': 'This video is only available for registered users'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'http://www.atresplayer.com/television/especial/videoencuentros/temporada-1/capitulo-112-david-bustamante_2014121600375.html',
|
||||||
|
'md5': '0d0e918533bbd4b263f2de4d197d4aac',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'capitulo-112-david-bustamante',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': 'David Bustamante',
|
||||||
|
'description': 'md5:f33f1c0a05be57f6708d4dd83a3b81c6',
|
||||||
|
'duration': 1439.0,
|
||||||
|
'thumbnail': 're:^https?://.*\.jpg$',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'http://www.atresplayer.com/television/series/el-secreto-de-puente-viejo/el-chico-de-los-tres-lunares/capitulo-977-29-12-14_2014122400174.html',
|
'url': 'http://www.atresplayer.com/television/series/el-secreto-de-puente-viejo/el-chico-de-los-tres-lunares/capitulo-977-29-12-14_2014122400174.html',
|
||||||
@@ -49,6 +65,13 @@ class AtresPlayerIE(SubtitlesInfoExtractor):
|
|||||||
|
|
||||||
_LOGIN_URL = 'https://servicios.atresplayer.com/j_spring_security_check'
|
_LOGIN_URL = 'https://servicios.atresplayer.com/j_spring_security_check'
|
||||||
|
|
||||||
|
_ERRORS = {
|
||||||
|
'UNPUBLISHED': 'We\'re sorry, but this video is not yet available.',
|
||||||
|
'DELETED': 'This video has expired and is no longer available for online streaming.',
|
||||||
|
'GEOUNPUBLISHED': 'We\'re sorry, but this video is not available in your region due to right restrictions.',
|
||||||
|
# 'PREMIUM': 'PREMIUM',
|
||||||
|
}
|
||||||
|
|
||||||
def _real_initialize(self):
|
def _real_initialize(self):
|
||||||
self._login()
|
self._login()
|
||||||
|
|
||||||
@@ -62,7 +85,7 @@ class AtresPlayerIE(SubtitlesInfoExtractor):
|
|||||||
'j_password': password,
|
'j_password': password,
|
||||||
}
|
}
|
||||||
|
|
||||||
request = compat_urllib_request.Request(
|
request = sanitized_Request(
|
||||||
self._LOGIN_URL, compat_urllib_parse.urlencode(login_form).encode('utf-8'))
|
self._LOGIN_URL, compat_urllib_parse.urlencode(login_form).encode('utf-8'))
|
||||||
request.add_header('Content-Type', 'application/x-www-form-urlencoded')
|
request.add_header('Content-Type', 'application/x-www-form-urlencoded')
|
||||||
response = self._download_webpage(
|
response = self._download_webpage(
|
||||||
@@ -82,58 +105,72 @@ class AtresPlayerIE(SubtitlesInfoExtractor):
|
|||||||
episode_id = self._search_regex(
|
episode_id = self._search_regex(
|
||||||
r'episode="([^"]+)"', webpage, 'episode id')
|
r'episode="([^"]+)"', webpage, 'episode id')
|
||||||
|
|
||||||
|
request = sanitized_Request(
|
||||||
|
self._PLAYER_URL_TEMPLATE % episode_id,
|
||||||
|
headers={'User-Agent': self._USER_AGENT})
|
||||||
|
player = self._download_json(request, episode_id, 'Downloading player JSON')
|
||||||
|
|
||||||
|
episode_type = player.get('typeOfEpisode')
|
||||||
|
error_message = self._ERRORS.get(episode_type)
|
||||||
|
if error_message:
|
||||||
|
raise ExtractorError(
|
||||||
|
'%s returned error: %s' % (self.IE_NAME, error_message), expected=True)
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
video_url = player.get('urlVideo')
|
||||||
|
if video_url:
|
||||||
|
format_info = {
|
||||||
|
'url': video_url,
|
||||||
|
'format_id': 'http',
|
||||||
|
}
|
||||||
|
mobj = re.search(r'(?P<bitrate>\d+)K_(?P<width>\d+)x(?P<height>\d+)', video_url)
|
||||||
|
if mobj:
|
||||||
|
format_info.update({
|
||||||
|
'width': int_or_none(mobj.group('width')),
|
||||||
|
'height': int_or_none(mobj.group('height')),
|
||||||
|
'tbr': int_or_none(mobj.group('bitrate')),
|
||||||
|
})
|
||||||
|
formats.append(format_info)
|
||||||
|
|
||||||
timestamp = int_or_none(self._download_webpage(
|
timestamp = int_or_none(self._download_webpage(
|
||||||
self._TIME_API_URL,
|
self._TIME_API_URL,
|
||||||
video_id, 'Downloading timestamp', fatal=False), 1000, time.time())
|
video_id, 'Downloading timestamp', fatal=False), 1000, time.time())
|
||||||
timestamp_shifted = compat_str(timestamp + self._TIMESTAMP_SHIFT)
|
timestamp_shifted = compat_str(timestamp + self._TIMESTAMP_SHIFT)
|
||||||
token = hmac.new(
|
token = hmac.new(
|
||||||
self._MAGIC.encode('ascii'),
|
self._MAGIC.encode('ascii'),
|
||||||
(episode_id + timestamp_shifted).encode('utf-8')
|
(episode_id + timestamp_shifted).encode('utf-8'), hashlib.md5
|
||||||
).hexdigest()
|
).hexdigest()
|
||||||
|
|
||||||
formats = []
|
request = sanitized_Request(
|
||||||
for fmt in ['windows', 'android_tablet']:
|
self._URL_VIDEO_TEMPLATE.format('windows', episode_id, timestamp_shifted, token),
|
||||||
request = compat_urllib_request.Request(
|
headers={'User-Agent': self._USER_AGENT})
|
||||||
self._URL_VIDEO_TEMPLATE.format(fmt, episode_id, timestamp_shifted, token))
|
|
||||||
request.add_header('User-Agent', self._USER_AGENT)
|
|
||||||
|
|
||||||
fmt_json = self._download_json(
|
fmt_json = self._download_json(
|
||||||
request, video_id, 'Downloading %s video JSON' % fmt)
|
request, video_id, 'Downloading windows video JSON')
|
||||||
|
|
||||||
result = fmt_json.get('resultDes')
|
result = fmt_json.get('resultDes')
|
||||||
if result.lower() != 'ok':
|
if result.lower() != 'ok':
|
||||||
raise ExtractorError(
|
raise ExtractorError(
|
||||||
'%s returned error: %s' % (self.IE_NAME, result), expected=True)
|
'%s returned error: %s' % (self.IE_NAME, result), expected=True)
|
||||||
|
|
||||||
for format_id, video_url in fmt_json['resultObject'].items():
|
for format_id, video_url in fmt_json['resultObject'].items():
|
||||||
if format_id == 'token' or not video_url.startswith('http'):
|
if format_id == 'token' or not video_url.startswith('http'):
|
||||||
continue
|
continue
|
||||||
if video_url.endswith('/Manifest'):
|
if 'geodeswowsmpra3player' in video_url:
|
||||||
if 'geodeswowsmpra3player' in video_url:
|
f4m_path = video_url.split('smil:', 1)[-1].split('free_', 1)[0]
|
||||||
f4m_path = video_url.split('smil:', 1)[-1].split('free_', 1)[0]
|
f4m_url = 'http://drg.antena3.com/{0}hds/es/sd.f4m'.format(f4m_path)
|
||||||
f4m_url = 'http://drg.antena3.com/{0}hds/es/sd.f4m'.format(f4m_path)
|
# this videos are protected by DRM, the f4m downloader doesn't support them
|
||||||
# this videos are protected by DRM, the f4m downloader doesn't support them
|
continue
|
||||||
continue
|
else:
|
||||||
else:
|
f4m_url = video_url[:-9] + '/manifest.f4m'
|
||||||
f4m_url = video_url[:-9] + '/manifest.f4m'
|
formats.extend(self._extract_f4m_formats(f4m_url, video_id, f4m_id='hds', fatal=False))
|
||||||
formats.extend(self._extract_f4m_formats(f4m_url, video_id))
|
|
||||||
else:
|
|
||||||
formats.append({
|
|
||||||
'url': video_url,
|
|
||||||
'format_id': 'android-%s' % format_id,
|
|
||||||
'preference': 1,
|
|
||||||
})
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
player = self._download_json(
|
|
||||||
self._PLAYER_URL_TEMPLATE % episode_id,
|
|
||||||
episode_id)
|
|
||||||
|
|
||||||
path_data = player.get('pathData')
|
path_data = player.get('pathData')
|
||||||
|
|
||||||
episode = self._download_xml(
|
episode = self._download_xml(
|
||||||
self._EPISODE_URL_TEMPLATE % path_data,
|
self._EPISODE_URL_TEMPLATE % path_data, video_id,
|
||||||
video_id, 'Downloading episode XML')
|
'Downloading episode XML')
|
||||||
|
|
||||||
duration = float_or_none(xpath_text(
|
duration = float_or_none(xpath_text(
|
||||||
episode, './media/asset/info/technical/contentDuration', 'duration'))
|
episode, './media/asset/info/technical/contentDuration', 'duration'))
|
||||||
@@ -144,13 +181,12 @@ class AtresPlayerIE(SubtitlesInfoExtractor):
|
|||||||
thumbnail = xpath_text(episode, './media/asset/files/background', 'thumbnail')
|
thumbnail = xpath_text(episode, './media/asset/files/background', 'thumbnail')
|
||||||
|
|
||||||
subtitles = {}
|
subtitles = {}
|
||||||
subtitle = xpath_text(episode, './media/asset/files/subtitle', 'subtitle')
|
subtitle_url = xpath_text(episode, './media/asset/files/subtitle', 'subtitle')
|
||||||
if subtitle:
|
if subtitle_url:
|
||||||
subtitles['es'] = subtitle
|
subtitles['es'] = [{
|
||||||
|
'ext': 'srt',
|
||||||
if self._downloader.params.get('listsubtitles', False):
|
'url': subtitle_url,
|
||||||
self._list_available_subtitles(video_id, subtitles)
|
}]
|
||||||
return
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
@@ -159,5 +195,5 @@ class AtresPlayerIE(SubtitlesInfoExtractor):
|
|||||||
'thumbnail': thumbnail,
|
'thumbnail': thumbnail,
|
||||||
'duration': duration,
|
'duration': duration,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'subtitles': self.extract_subtitles(video_id, subtitles),
|
'subtitles': subtitles,
|
||||||
}
|
}
|
||||||
|
|||||||
80
youtube_dl/extractor/audimedia.py
Normal file
80
youtube_dl/extractor/audimedia.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
int_or_none,
|
||||||
|
parse_iso8601,
|
||||||
|
sanitized_Request,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AudiMediaIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?audimedia\.tv/(?:en|de)/vid/(?P<id>[^/?#]+)'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'https://audimedia.tv/en/vid/60-seconds-of-audi-sport-104-2015-wec-bahrain-rookie-test',
|
||||||
|
'md5': '79a8b71c46d49042609795ab59779b66',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1565',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '60 Seconds of Audi Sport 104/2015 - WEC Bahrain, Rookie Test',
|
||||||
|
'description': 'md5:60e5d30a78ced725f7b8d34370762941',
|
||||||
|
'upload_date': '20151124',
|
||||||
|
'timestamp': 1448354940,
|
||||||
|
'duration': 74022,
|
||||||
|
'view_count': int,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# extracted from https://audimedia.tv/assets/embed/embedded-player.js (dataSourceAuthToken)
|
||||||
|
_AUTH_TOKEN = 'e25b42847dba18c6c8816d5d8ce94c326e06823ebf0859ed164b3ba169be97f2'
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
|
||||||
|
raw_payload = self._search_regex(r'<script[^>]+class="amtv-embed"[^>]+id="([^"]+)"', webpage, 'raw payload')
|
||||||
|
_, stage_mode, video_id, lang = raw_payload.split('-')
|
||||||
|
|
||||||
|
# TODO: handle s and e stage_mode (live streams and ended live streams)
|
||||||
|
if stage_mode not in ('s', 'e'):
|
||||||
|
request = sanitized_Request(
|
||||||
|
'https://audimedia.tv/api/video/v1/videos/%s?embed[]=video_versions&embed[]=thumbnail_image&where[content_language_iso]=%s' % (video_id, lang),
|
||||||
|
headers={'X-Auth-Token': self._AUTH_TOKEN})
|
||||||
|
json_data = self._download_json(request, video_id)['results']
|
||||||
|
formats = []
|
||||||
|
|
||||||
|
stream_url_hls = json_data.get('stream_url_hls')
|
||||||
|
if stream_url_hls:
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
stream_url_hls, video_id, 'mp4',
|
||||||
|
entry_protocol='m3u8_native', m3u8_id='hls', fatal=False))
|
||||||
|
|
||||||
|
stream_url_hds = json_data.get('stream_url_hds')
|
||||||
|
if stream_url_hds:
|
||||||
|
formats.extend(self._extract_f4m_formats(
|
||||||
|
stream_url_hds + '?hdcore=3.4.0',
|
||||||
|
video_id, f4m_id='hds', fatal=False))
|
||||||
|
|
||||||
|
for video_version in json_data.get('video_versions'):
|
||||||
|
video_version_url = video_version.get('download_url') or video_version.get('stream_url')
|
||||||
|
if not video_version_url:
|
||||||
|
continue
|
||||||
|
formats.append({
|
||||||
|
'url': video_version_url,
|
||||||
|
'width': int_or_none(video_version.get('width')),
|
||||||
|
'height': int_or_none(video_version.get('height')),
|
||||||
|
'abr': int_or_none(video_version.get('audio_bitrate')),
|
||||||
|
'vbr': int_or_none(video_version.get('video_bitrate')),
|
||||||
|
})
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': json_data['title'],
|
||||||
|
'description': json_data.get('subtitle'),
|
||||||
|
'thumbnail': json_data.get('thumbnail_image', {}).get('file'),
|
||||||
|
'timestamp': parse_iso8601(json_data.get('publication_date')),
|
||||||
|
'duration': int_or_none(json_data.get('duration')),
|
||||||
|
'view_count': int_or_none(json_data.get('view_count')),
|
||||||
|
'formats': formats,
|
||||||
|
}
|
||||||
@@ -56,7 +56,7 @@ class AudiomackIE(InfoExtractor):
|
|||||||
|
|
||||||
# API is inconsistent with errors
|
# API is inconsistent with errors
|
||||||
if 'url' not in api_response or not api_response['url'] or 'error' in api_response:
|
if 'url' not in api_response or not api_response['url'] or 'error' in api_response:
|
||||||
raise ExtractorError('Invalid url %s', url)
|
raise ExtractorError('Invalid url %s' % url)
|
||||||
|
|
||||||
# Audiomack wraps a lot of soundcloud tracks in their branded wrapper
|
# Audiomack wraps a lot of soundcloud tracks in their branded wrapper
|
||||||
# if so, pass the work off to the soundcloud extractor
|
# if so, pass the work off to the soundcloud extractor
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ from __future__ import unicode_literals
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import float_or_none
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
float_or_none,
|
||||||
|
sanitized_Request,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AzubuIE(InfoExtractor):
|
class AzubuIE(InfoExtractor):
|
||||||
@@ -91,3 +95,37 @@ class AzubuIE(InfoExtractor):
|
|||||||
'view_count': view_count,
|
'view_count': view_count,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AzubuLiveIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'http://www.azubu.tv/(?P<id>[^/]+)$'
|
||||||
|
|
||||||
|
_TEST = {
|
||||||
|
'url': 'http://www.azubu.tv/MarsTVMDLen',
|
||||||
|
'only_matching': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
user = self._match_id(url)
|
||||||
|
|
||||||
|
info = self._download_json(
|
||||||
|
'http://api.azubu.tv/public/modules/last-video/{0}/info'.format(user),
|
||||||
|
user)['data']
|
||||||
|
if info['type'] != 'STREAM':
|
||||||
|
raise ExtractorError('{0} is not streaming live'.format(user), expected=True)
|
||||||
|
|
||||||
|
req = sanitized_Request(
|
||||||
|
'https://edge-elb.api.brightcove.com/playback/v1/accounts/3361910549001/videos/ref:' + info['reference_id'])
|
||||||
|
req.add_header('Accept', 'application/json;pk=BCpkADawqM1gvI0oGWg8dxQHlgT8HkdE2LnAlWAZkOlznO39bSZX726u4JqnDsK3MDXcO01JxXK2tZtJbgQChxgaFzEVdHRjaDoxaOu8hHOO8NYhwdxw9BzvgkvLUlpbDNUuDoc4E4wxDToV')
|
||||||
|
bc_info = self._download_json(req, user)
|
||||||
|
m3u8_url = next(source['src'] for source in bc_info['sources'] if source['container'] == 'M2TS')
|
||||||
|
formats = self._extract_m3u8_formats(m3u8_url, user, ext='mp4')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': info['id'],
|
||||||
|
'title': self._live_title(info['title']),
|
||||||
|
'uploader_id': user,
|
||||||
|
'formats': formats,
|
||||||
|
'is_live': True,
|
||||||
|
'thumbnail': bc_info['poster'],
|
||||||
|
}
|
||||||
|
|||||||
56
youtube_dl/extractor/baidu.py
Normal file
56
youtube_dl/extractor/baidu.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import unescapeHTML
|
||||||
|
|
||||||
|
|
||||||
|
class BaiduVideoIE(InfoExtractor):
|
||||||
|
IE_DESC = '百度视频'
|
||||||
|
_VALID_URL = r'http://v\.baidu\.com/(?P<type>[a-z]+)/(?P<id>\d+)\.htm'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://v.baidu.com/comic/1069.htm?frp=bdbrand&q=%E4%B8%AD%E5%8D%8E%E5%B0%8F%E5%BD%93%E5%AE%B6',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1069',
|
||||||
|
'title': '中华小当家 TV版国语',
|
||||||
|
'description': 'md5:51be07afe461cf99fa61231421b5397c',
|
||||||
|
},
|
||||||
|
'playlist_count': 52,
|
||||||
|
}, {
|
||||||
|
'url': 'http://v.baidu.com/show/11595.htm?frp=bdbrand',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '11595',
|
||||||
|
'title': 're:^奔跑吧兄弟',
|
||||||
|
'description': 'md5:1bf88bad6d850930f542d51547c089b8',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 12,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _call_api(self, path, category, playlist_id, note):
|
||||||
|
return self._download_json('http://app.video.baidu.com/%s/?worktype=adnative%s&id=%s' % (
|
||||||
|
path, category, playlist_id), playlist_id, note)
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
category, playlist_id = re.match(self._VALID_URL, url).groups()
|
||||||
|
if category == 'show':
|
||||||
|
category = 'tvshow'
|
||||||
|
if category == 'tv':
|
||||||
|
category = 'tvplay'
|
||||||
|
|
||||||
|
playlist_detail = self._call_api(
|
||||||
|
'xqinfo', category, playlist_id, 'Download playlist JSON metadata')
|
||||||
|
|
||||||
|
playlist_title = playlist_detail['title']
|
||||||
|
playlist_description = unescapeHTML(playlist_detail.get('intro'))
|
||||||
|
|
||||||
|
episodes_detail = self._call_api(
|
||||||
|
'xqsingle', category, playlist_id, 'Download episodes JSON metadata')
|
||||||
|
|
||||||
|
entries = [self.url_result(
|
||||||
|
episode['url'], video_title=episode['title']
|
||||||
|
) for episode in episodes_detail['videos']]
|
||||||
|
|
||||||
|
return self.playlist_result(
|
||||||
|
entries, playlist_id, playlist_title, playlist_description)
|
||||||
@@ -1,12 +1,18 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import json
|
|
||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
compat_urllib_request,
|
compat_urllib_parse,
|
||||||
|
compat_str,
|
||||||
|
)
|
||||||
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
int_or_none,
|
||||||
|
float_or_none,
|
||||||
|
sanitized_Request,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -14,6 +20,8 @@ class BambuserIE(InfoExtractor):
|
|||||||
IE_NAME = 'bambuser'
|
IE_NAME = 'bambuser'
|
||||||
_VALID_URL = r'https?://bambuser\.com/v/(?P<id>\d+)'
|
_VALID_URL = r'https?://bambuser\.com/v/(?P<id>\d+)'
|
||||||
_API_KEY = '005f64509e19a868399060af746a00aa'
|
_API_KEY = '005f64509e19a868399060af746a00aa'
|
||||||
|
_LOGIN_URL = 'https://bambuser.com/user'
|
||||||
|
_NETRC_MACHINE = 'bambuser'
|
||||||
|
|
||||||
_TEST = {
|
_TEST = {
|
||||||
'url': 'http://bambuser.com/v/4050584',
|
'url': 'http://bambuser.com/v/4050584',
|
||||||
@@ -26,6 +34,9 @@ class BambuserIE(InfoExtractor):
|
|||||||
'duration': 3741,
|
'duration': 3741,
|
||||||
'uploader': 'pixelversity',
|
'uploader': 'pixelversity',
|
||||||
'uploader_id': '344706',
|
'uploader_id': '344706',
|
||||||
|
'timestamp': 1382976692,
|
||||||
|
'upload_date': '20131028',
|
||||||
|
'view_count': int,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
# It doesn't respect the 'Range' header, it would download the whole video
|
# It doesn't respect the 'Range' header, it would download the whole video
|
||||||
@@ -34,23 +45,60 @@ class BambuserIE(InfoExtractor):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _login(self):
|
||||||
|
(username, password) = self._get_login_info()
|
||||||
|
if username is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
login_form = {
|
||||||
|
'form_id': 'user_login',
|
||||||
|
'op': 'Log in',
|
||||||
|
'name': username,
|
||||||
|
'pass': password,
|
||||||
|
}
|
||||||
|
|
||||||
|
request = sanitized_Request(
|
||||||
|
self._LOGIN_URL, compat_urllib_parse.urlencode(login_form).encode('utf-8'))
|
||||||
|
request.add_header('Referer', self._LOGIN_URL)
|
||||||
|
response = self._download_webpage(
|
||||||
|
request, None, 'Logging in as %s' % username)
|
||||||
|
|
||||||
|
login_error = self._html_search_regex(
|
||||||
|
r'(?s)<div class="messages error">(.+?)</div>',
|
||||||
|
response, 'login error', default=None)
|
||||||
|
if login_error:
|
||||||
|
raise ExtractorError(
|
||||||
|
'Unable to login: %s' % login_error, expected=True)
|
||||||
|
|
||||||
|
def _real_initialize(self):
|
||||||
|
self._login()
|
||||||
|
|
||||||
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('id')
|
|
||||||
info_url = ('http://player-c.api.bambuser.com/getVideo.json?'
|
info = self._download_json(
|
||||||
'&api_key=%s&vid=%s' % (self._API_KEY, video_id))
|
'http://player-c.api.bambuser.com/getVideo.json?api_key=%s&vid=%s'
|
||||||
info_json = self._download_webpage(info_url, video_id)
|
% (self._API_KEY, video_id), video_id)
|
||||||
info = json.loads(info_json)['result']
|
|
||||||
|
error = info.get('error')
|
||||||
|
if error:
|
||||||
|
raise ExtractorError(
|
||||||
|
'%s returned error: %s' % (self.IE_NAME, error), expected=True)
|
||||||
|
|
||||||
|
result = info['result']
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': info['title'],
|
'title': result['title'],
|
||||||
'url': info['url'],
|
'url': result['url'],
|
||||||
'thumbnail': info.get('preview'),
|
'thumbnail': result.get('preview'),
|
||||||
'duration': int(info['length']),
|
'duration': int_or_none(result.get('length')),
|
||||||
'view_count': int(info['views_total']),
|
'uploader': result.get('username'),
|
||||||
'uploader': info['username'],
|
'uploader_id': compat_str(result.get('owner', {}).get('uid')),
|
||||||
'uploader_id': info['owner']['uid'],
|
'timestamp': int_or_none(result.get('created')),
|
||||||
|
'fps': float_or_none(result.get('framerate')),
|
||||||
|
'view_count': int_or_none(result.get('views_total')),
|
||||||
|
'comment_count': int_or_none(result.get('comment_count')),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -78,7 +126,7 @@ class BambuserChannelIE(InfoExtractor):
|
|||||||
'&sort=created&access_mode=0%2C1%2C2&limit={count}'
|
'&sort=created&access_mode=0%2C1%2C2&limit={count}'
|
||||||
'&method=broadcast&format=json&vid_older_than={last}'
|
'&method=broadcast&format=json&vid_older_than={last}'
|
||||||
).format(user=user, count=self._STEP, last=last_id)
|
).format(user=user, count=self._STEP, last=last_id)
|
||||||
req = compat_urllib_request.Request(req_url)
|
req = sanitized_Request(req_url)
|
||||||
# Without setting this header, we wouldn't get any result
|
# Without setting this header, we wouldn't get any result
|
||||||
req.add_header('Referer', 'http://bambuser.com/channel/%s' % user)
|
req.add_header('Referer', 'http://bambuser.com/channel/%s' % user)
|
||||||
data = self._download_json(
|
data = self._download_json(
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ from ..compat import (
|
|||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
float_or_none,
|
||||||
|
int_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -52,11 +54,11 @@ class BandcampIE(InfoExtractor):
|
|||||||
ext, abr_str = format_id.split('-', 1)
|
ext, abr_str = format_id.split('-', 1)
|
||||||
formats.append({
|
formats.append({
|
||||||
'format_id': format_id,
|
'format_id': format_id,
|
||||||
'url': format_url,
|
'url': self._proto_relative_url(format_url, 'http:'),
|
||||||
'ext': ext,
|
'ext': ext,
|
||||||
'vcodec': 'none',
|
'vcodec': 'none',
|
||||||
'acodec': ext,
|
'acodec': ext,
|
||||||
'abr': int(abr_str),
|
'abr': int_or_none(abr_str),
|
||||||
})
|
})
|
||||||
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
@@ -65,14 +67,14 @@ class BandcampIE(InfoExtractor):
|
|||||||
'id': compat_str(data['id']),
|
'id': compat_str(data['id']),
|
||||||
'title': data['title'],
|
'title': data['title'],
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'duration': float(data['duration']),
|
'duration': float_or_none(data.get('duration')),
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
raise ExtractorError('No free songs found')
|
raise ExtractorError('No free songs found')
|
||||||
|
|
||||||
download_link = m_download.group(1)
|
download_link = m_download.group(1)
|
||||||
video_id = self._search_regex(
|
video_id = self._search_regex(
|
||||||
r'(?ms)var TralbumData = {.*?id: (?P<id>\d+),?$',
|
r'(?ms)var TralbumData = .*?[{,]\s*id: (?P<id>\d+),?$',
|
||||||
webpage, 'video id')
|
webpage, 'video id')
|
||||||
|
|
||||||
download_webpage = self._download_webpage(download_link, video_id, 'Downloading free downloads page')
|
download_webpage = self._download_webpage(download_link, video_id, 'Downloading free downloads page')
|
||||||
@@ -93,8 +95,8 @@ class BandcampIE(InfoExtractor):
|
|||||||
final_url_webpage = self._download_webpage(request_url, video_id, 'Requesting download url')
|
final_url_webpage = self._download_webpage(request_url, video_id, 'Requesting download url')
|
||||||
# If we could correctly generate the .rand field the url would be
|
# If we could correctly generate the .rand field the url would be
|
||||||
# in the "download_url" key
|
# in the "download_url" key
|
||||||
final_url = self._search_regex(
|
final_url = self._proto_relative_url(self._search_regex(
|
||||||
r'"retry_url":"(.*?)"', final_url_webpage, 'final video URL')
|
r'"retry_url":"(.+?)"', final_url_webpage, 'final video URL'), 'http:')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
|
|||||||
961
youtube_dl/extractor/bbc.py
Normal file
961
youtube_dl/extractor/bbc.py
Normal file
@@ -0,0 +1,961 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
float_or_none,
|
||||||
|
int_or_none,
|
||||||
|
parse_duration,
|
||||||
|
parse_iso8601,
|
||||||
|
remove_end,
|
||||||
|
unescapeHTML,
|
||||||
|
)
|
||||||
|
from ..compat import (
|
||||||
|
compat_etree_fromstring,
|
||||||
|
compat_HTTPError,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BBCCoUkIE(InfoExtractor):
|
||||||
|
IE_NAME = 'bbc.co.uk'
|
||||||
|
IE_DESC = 'BBC iPlayer'
|
||||||
|
_ID_REGEX = r'[pb][\da-z]{7}'
|
||||||
|
_VALID_URL = r'''(?x)
|
||||||
|
https?://
|
||||||
|
(?:www\.)?bbc\.co\.uk/
|
||||||
|
(?:
|
||||||
|
programmes/(?!articles/)|
|
||||||
|
iplayer(?:/[^/]+)?/(?:episode/|playlist/)|
|
||||||
|
music/clips[/#]|
|
||||||
|
radio/player/
|
||||||
|
)
|
||||||
|
(?P<id>%s)
|
||||||
|
''' % _ID_REGEX
|
||||||
|
|
||||||
|
_MEDIASELECTOR_URLS = [
|
||||||
|
# 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.
|
||||||
|
# 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',
|
||||||
|
'http://open.live.bbc.co.uk/mediaselector/5/select/version/2.0/mediaset/pc/vpid/%s',
|
||||||
|
]
|
||||||
|
|
||||||
|
_MEDIASELECTION_NS = 'http://bbc.co.uk/2008/mp/mediaselection'
|
||||||
|
_EMP_PLAYLIST_NS = 'http://bbc.co.uk/2008/emp/playlist'
|
||||||
|
|
||||||
|
_NAMESPACES = (
|
||||||
|
_MEDIASELECTION_NS,
|
||||||
|
_EMP_PLAYLIST_NS,
|
||||||
|
)
|
||||||
|
|
||||||
|
_TESTS = [
|
||||||
|
{
|
||||||
|
'url': 'http://www.bbc.co.uk/programmes/b039g8p7',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'b039d07m',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': 'Leonard Cohen, Kaleidoscope - BBC Radio 4',
|
||||||
|
'description': 'The Canadian poet and songwriter reflects on his musical career.',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# rtmp download
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'http://www.bbc.co.uk/iplayer/episode/b00yng5w/The_Man_in_Black_Series_3_The_Printed_Name/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'b00yng1d',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': 'The Man in Black: Series 3: The Printed Name',
|
||||||
|
'description': "Mark Gatiss introduces Nicholas Pierpan's chilling tale of a writer's devilish pact with a mysterious man. Stars Ewan Bailey.",
|
||||||
|
'duration': 1800,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# rtmp download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
'skip': 'Episode is no longer available on BBC iPlayer Radio',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'http://www.bbc.co.uk/iplayer/episode/b03vhd1f/The_Voice_UK_Series_3_Blind_Auditions_5/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'b00yng1d',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': 'The Voice UK: Series 3: Blind Auditions 5',
|
||||||
|
'description': "Emma Willis and Marvin Humes present the fifth set of blind auditions in the singing competition, as the coaches continue to build their teams based on voice alone.",
|
||||||
|
'duration': 5100,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# rtmp download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
'skip': 'Currently BBC iPlayer TV programmes are available to play in the UK only',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'http://www.bbc.co.uk/iplayer/episode/p026c7jt/tomorrows-worlds-the-unearthly-history-of-science-fiction-2-invasion',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'b03k3pb7',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': "Tomorrow's Worlds: The Unearthly History of Science Fiction",
|
||||||
|
'description': '2. Invasion',
|
||||||
|
'duration': 3600,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# rtmp download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
'skip': 'Currently BBC iPlayer TV programmes are available to play in the UK only',
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.bbc.co.uk/programmes/b04v20dw',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'b04v209v',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': 'Pete Tong, The Essential New Tune Special',
|
||||||
|
'description': "Pete has a very special mix - all of 2014's Essential New Tunes!",
|
||||||
|
'duration': 10800,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# rtmp download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
'skip': 'Episode is no longer available on BBC iPlayer Radio',
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.bbc.co.uk/music/clips/p022h44b',
|
||||||
|
'note': 'Audio',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'p022h44j',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': 'BBC Proms Music Guides, Rachmaninov: Symphonic Dances',
|
||||||
|
'description': "In this Proms Music Guide, Andrew McGregor looks at Rachmaninov's Symphonic Dances.",
|
||||||
|
'duration': 227,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# rtmp download
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.bbc.co.uk/music/clips/p025c0zz',
|
||||||
|
'note': 'Video',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'p025c103',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': 'Reading and Leeds Festival, 2014, Rae Morris - Closer (Live on BBC Three)',
|
||||||
|
'description': 'Rae Morris performs Closer for BBC Three at Reading 2014',
|
||||||
|
'duration': 226,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# rtmp download
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.bbc.co.uk/iplayer/episode/b054fn09/ad/natural-world-20152016-2-super-powered-owls',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'p02n76xf',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': 'Natural World, 2015-2016: 2. Super Powered Owls',
|
||||||
|
'description': 'md5:e4db5c937d0e95a7c6b5e654d429183d',
|
||||||
|
'duration': 3540,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# rtmp download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
'skip': 'geolocation',
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.bbc.co.uk/iplayer/episode/b05zmgwn/royal-academy-summer-exhibition',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'b05zmgw1',
|
||||||
|
'ext': 'flv',
|
||||||
|
'description': 'Kirsty Wark and Morgan Quaintance visit the Royal Academy as it prepares for its annual artistic extravaganza, meeting people who have come together to make the show unique.',
|
||||||
|
'title': 'Royal Academy Summer Exhibition',
|
||||||
|
'duration': 3540,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# rtmp download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
'skip': 'geolocation',
|
||||||
|
}, {
|
||||||
|
# iptv-all mediaset fails with geolocation however there is no geo restriction
|
||||||
|
# for this programme at all
|
||||||
|
'url': 'http://www.bbc.co.uk/programmes/b06rkn85',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'b06rkms3',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': "Best of the Mini-Mixes 2015: Part 3, Annie Mac's Friday Night - BBC Radio 1",
|
||||||
|
'description': "Annie has part three in the Best of the Mini-Mixes 2015, plus the year's Most Played!",
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# rtmp download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# compact player (https://github.com/rg3/youtube-dl/issues/8147)
|
||||||
|
'url': 'http://www.bbc.co.uk/programmes/p028bfkf/player',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'p028bfkj',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': 'Extract from BBC documentary Look Stranger - Giant Leeks and Magic Brews',
|
||||||
|
'description': 'Extract from BBC documentary Look Stranger - Giant Leeks and Magic Brews',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# rtmp download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.bbc.co.uk/iplayer/playlist/p01dvks4',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.bbc.co.uk/music/clips#p02frcc3',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.bbc.co.uk/iplayer/cbeebies/episode/b0480276/bing-14-atchoo',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.bbc.co.uk/radio/player/p03cchwf',
|
||||||
|
'only_matching': True,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
class MediaSelectionError(Exception):
|
||||||
|
def __init__(self, id):
|
||||||
|
self.id = id
|
||||||
|
|
||||||
|
def _extract_asx_playlist(self, connection, programme_id):
|
||||||
|
asx = self._download_xml(connection.get('href'), programme_id, 'Downloading ASX playlist')
|
||||||
|
return [ref.get('href') for ref in asx.findall('./Entry/ref')]
|
||||||
|
|
||||||
|
def _extract_connection(self, connection, programme_id):
|
||||||
|
formats = []
|
||||||
|
kind = connection.get('kind')
|
||||||
|
protocol = connection.get('protocol')
|
||||||
|
supplier = connection.get('supplier')
|
||||||
|
if protocol == 'http':
|
||||||
|
href = connection.get('href')
|
||||||
|
transfer_format = connection.get('transferFormat')
|
||||||
|
# ASX playlist
|
||||||
|
if supplier == 'asx':
|
||||||
|
for i, ref in enumerate(self._extract_asx_playlist(connection, programme_id)):
|
||||||
|
formats.append({
|
||||||
|
'url': ref,
|
||||||
|
'format_id': 'ref%s_%s' % (i, supplier),
|
||||||
|
})
|
||||||
|
# Skip DASH until supported
|
||||||
|
elif transfer_format == 'dash':
|
||||||
|
pass
|
||||||
|
elif transfer_format == 'hls':
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
href, programme_id, ext='mp4', entry_protocol='m3u8_native',
|
||||||
|
m3u8_id=supplier, fatal=False))
|
||||||
|
# Direct link
|
||||||
|
else:
|
||||||
|
formats.append({
|
||||||
|
'url': href,
|
||||||
|
'format_id': supplier or kind or protocol,
|
||||||
|
})
|
||||||
|
elif protocol == 'rtmp':
|
||||||
|
application = connection.get('application', 'ondemand')
|
||||||
|
auth_string = connection.get('authString')
|
||||||
|
identifier = connection.get('identifier')
|
||||||
|
server = connection.get('server')
|
||||||
|
formats.append({
|
||||||
|
'url': '%s://%s/%s?%s' % (protocol, server, application, auth_string),
|
||||||
|
'play_path': identifier,
|
||||||
|
'app': '%s?%s' % (application, auth_string),
|
||||||
|
'page_url': 'http://www.bbc.co.uk',
|
||||||
|
'player_url': 'http://www.bbc.co.uk/emp/releases/iplayer/revisions/617463_618125_4/617463_618125_4_emp.swf',
|
||||||
|
'rtmp_live': False,
|
||||||
|
'ext': 'flv',
|
||||||
|
'format_id': supplier,
|
||||||
|
})
|
||||||
|
return formats
|
||||||
|
|
||||||
|
def _extract_items(self, playlist):
|
||||||
|
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):
|
||||||
|
error = media_selection.find('./{%s}error' % self._MEDIASELECTION_NS)
|
||||||
|
if error is None:
|
||||||
|
media_selection.find('./{%s}error' % self._EMP_PLAYLIST_NS)
|
||||||
|
if error is not None:
|
||||||
|
raise BBCCoUkIE.MediaSelectionError(error.get('id'))
|
||||||
|
return self._findall_ns(media_selection, './{%s}media')
|
||||||
|
|
||||||
|
def _extract_connections(self, media):
|
||||||
|
return self._findall_ns(media, './{%s}connection')
|
||||||
|
|
||||||
|
def _extract_video(self, media, programme_id):
|
||||||
|
formats = []
|
||||||
|
vbr = int_or_none(media.get('bitrate'))
|
||||||
|
vcodec = media.get('encoding')
|
||||||
|
service = media.get('service')
|
||||||
|
width = int_or_none(media.get('width'))
|
||||||
|
height = int_or_none(media.get('height'))
|
||||||
|
file_size = int_or_none(media.get('media_file_size'))
|
||||||
|
for connection in self._extract_connections(media):
|
||||||
|
conn_formats = self._extract_connection(connection, programme_id)
|
||||||
|
for format in conn_formats:
|
||||||
|
format.update({
|
||||||
|
'width': width,
|
||||||
|
'height': height,
|
||||||
|
'vbr': vbr,
|
||||||
|
'vcodec': vcodec,
|
||||||
|
'filesize': file_size,
|
||||||
|
})
|
||||||
|
if service:
|
||||||
|
format['format_id'] = '%s_%s' % (service, format['format_id'])
|
||||||
|
formats.extend(conn_formats)
|
||||||
|
return formats
|
||||||
|
|
||||||
|
def _extract_audio(self, media, programme_id):
|
||||||
|
formats = []
|
||||||
|
abr = int_or_none(media.get('bitrate'))
|
||||||
|
acodec = media.get('encoding')
|
||||||
|
service = media.get('service')
|
||||||
|
for connection in self._extract_connections(media):
|
||||||
|
conn_formats = self._extract_connection(connection, programme_id)
|
||||||
|
for format in conn_formats:
|
||||||
|
format.update({
|
||||||
|
'format_id': '%s_%s' % (service, format['format_id']),
|
||||||
|
'abr': abr,
|
||||||
|
'acodec': acodec,
|
||||||
|
})
|
||||||
|
formats.extend(conn_formats)
|
||||||
|
return formats
|
||||||
|
|
||||||
|
def _get_subtitles(self, media, programme_id):
|
||||||
|
subtitles = {}
|
||||||
|
for connection in self._extract_connections(media):
|
||||||
|
captions = self._download_xml(connection.get('href'), programme_id, 'Downloading captions')
|
||||||
|
lang = captions.get('{http://www.w3.org/XML/1998/namespace}lang', 'en')
|
||||||
|
subtitles[lang] = [
|
||||||
|
{
|
||||||
|
'url': connection.get('href'),
|
||||||
|
'ext': 'ttml',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
return subtitles
|
||||||
|
|
||||||
|
def _raise_extractor_error(self, media_selection_error):
|
||||||
|
raise ExtractorError(
|
||||||
|
'%s returned error: %s' % (self.IE_NAME, media_selection_error.id),
|
||||||
|
expected=True)
|
||||||
|
|
||||||
|
def _download_media_selector(self, programme_id):
|
||||||
|
last_exception = None
|
||||||
|
for mediaselector_url in self._MEDIASELECTOR_URLS:
|
||||||
|
try:
|
||||||
|
return self._download_media_selector_url(
|
||||||
|
mediaselector_url % programme_id, programme_id)
|
||||||
|
except BBCCoUkIE.MediaSelectionError as e:
|
||||||
|
if e.id in ('notukerror', 'geolocation', 'selectionunavailable'):
|
||||||
|
last_exception = e
|
||||||
|
continue
|
||||||
|
self._raise_extractor_error(e)
|
||||||
|
self._raise_extractor_error(last_exception)
|
||||||
|
|
||||||
|
def _download_media_selector_url(self, url, programme_id=None):
|
||||||
|
try:
|
||||||
|
media_selection = self._download_xml(
|
||||||
|
url, programme_id, 'Downloading media selection XML')
|
||||||
|
except ExtractorError as ee:
|
||||||
|
if isinstance(ee.cause, compat_HTTPError) and ee.cause.code in (403, 404):
|
||||||
|
media_selection = compat_etree_fromstring(ee.cause.read().decode('utf-8'))
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
return self._process_media_selector(media_selection, programme_id)
|
||||||
|
|
||||||
|
def _process_media_selector(self, media_selection, programme_id):
|
||||||
|
formats = []
|
||||||
|
subtitles = None
|
||||||
|
|
||||||
|
for media in self._extract_medias(media_selection):
|
||||||
|
kind = media.get('kind')
|
||||||
|
if kind == 'audio':
|
||||||
|
formats.extend(self._extract_audio(media, programme_id))
|
||||||
|
elif kind == 'video':
|
||||||
|
formats.extend(self._extract_video(media, programme_id))
|
||||||
|
elif kind == 'captions':
|
||||||
|
subtitles = self.extract_subtitles(media, programme_id)
|
||||||
|
return formats, subtitles
|
||||||
|
|
||||||
|
def _download_playlist(self, playlist_id):
|
||||||
|
try:
|
||||||
|
playlist = self._download_json(
|
||||||
|
'http://www.bbc.co.uk/programmes/%s/playlist.json' % playlist_id,
|
||||||
|
playlist_id, 'Downloading playlist JSON')
|
||||||
|
|
||||||
|
version = playlist.get('defaultAvailableVersion')
|
||||||
|
if version:
|
||||||
|
smp_config = version['smpConfig']
|
||||||
|
title = smp_config['title']
|
||||||
|
description = smp_config['summary']
|
||||||
|
for item in smp_config['items']:
|
||||||
|
kind = item['kind']
|
||||||
|
if kind != 'programme' and kind != 'radioProgramme':
|
||||||
|
continue
|
||||||
|
programme_id = item.get('vpid')
|
||||||
|
duration = int_or_none(item.get('duration'))
|
||||||
|
formats, subtitles = self._download_media_selector(programme_id)
|
||||||
|
return programme_id, title, description, duration, formats, subtitles
|
||||||
|
except ExtractorError as ee:
|
||||||
|
if not (isinstance(ee.cause, compat_HTTPError) and ee.cause.code == 404):
|
||||||
|
raise
|
||||||
|
|
||||||
|
# fallback to legacy playlist
|
||||||
|
return self._process_legacy_playlist(playlist_id)
|
||||||
|
|
||||||
|
def _process_legacy_playlist_url(self, url, display_id):
|
||||||
|
playlist = self._download_legacy_playlist_url(url, display_id)
|
||||||
|
return self._extract_from_legacy_playlist(playlist, display_id)
|
||||||
|
|
||||||
|
def _process_legacy_playlist(self, playlist_id):
|
||||||
|
return self._process_legacy_playlist_url(
|
||||||
|
'http://www.bbc.co.uk/iplayer/playlist/%s' % playlist_id, playlist_id)
|
||||||
|
|
||||||
|
def _download_legacy_playlist_url(self, url, playlist_id=None):
|
||||||
|
return self._download_xml(
|
||||||
|
url, playlist_id, 'Downloading legacy playlist XML')
|
||||||
|
|
||||||
|
def _extract_from_legacy_playlist(self, playlist, playlist_id):
|
||||||
|
no_items = playlist.find('./{%s}noItems' % self._EMP_PLAYLIST_NS)
|
||||||
|
if no_items is not None:
|
||||||
|
reason = no_items.get('reason')
|
||||||
|
if reason == 'preAvailability':
|
||||||
|
msg = 'Episode %s is not yet available' % playlist_id
|
||||||
|
elif reason == 'postAvailability':
|
||||||
|
msg = 'Episode %s is no longer available' % playlist_id
|
||||||
|
elif reason == 'noMedia':
|
||||||
|
msg = 'Episode %s is not currently available' % playlist_id
|
||||||
|
else:
|
||||||
|
msg = 'Episode %s is not available: %s' % (playlist_id, reason)
|
||||||
|
raise ExtractorError(msg, expected=True)
|
||||||
|
|
||||||
|
for item in self._extract_items(playlist):
|
||||||
|
kind = item.get('kind')
|
||||||
|
if kind != 'programme' and kind != 'radioProgramme':
|
||||||
|
continue
|
||||||
|
title = playlist.find('./{%s}title' % self._EMP_PLAYLIST_NS).text
|
||||||
|
description_el = playlist.find('./{%s}summary' % self._EMP_PLAYLIST_NS)
|
||||||
|
description = description_el.text if description_el is not None else None
|
||||||
|
|
||||||
|
def get_programme_id(item):
|
||||||
|
def get_from_attributes(item):
|
||||||
|
for p in('identifier', 'group'):
|
||||||
|
value = item.get(p)
|
||||||
|
if value and re.match(r'^[pb][\da-z]{7}$', value):
|
||||||
|
return value
|
||||||
|
get_from_attributes(item)
|
||||||
|
mediator = item.find('./{%s}mediator' % self._EMP_PLAYLIST_NS)
|
||||||
|
if mediator is not None:
|
||||||
|
return get_from_attributes(mediator)
|
||||||
|
|
||||||
|
programme_id = get_programme_id(item)
|
||||||
|
duration = int_or_none(item.get('duration'))
|
||||||
|
|
||||||
|
if programme_id:
|
||||||
|
formats, subtitles = self._download_media_selector(programme_id)
|
||||||
|
else:
|
||||||
|
formats, subtitles = self._process_media_selector(item, playlist_id)
|
||||||
|
programme_id = playlist_id
|
||||||
|
|
||||||
|
return programme_id, title, description, duration, formats, subtitles
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
group_id = self._match_id(url)
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, group_id, 'Downloading video page')
|
||||||
|
|
||||||
|
programme_id = None
|
||||||
|
duration = None
|
||||||
|
|
||||||
|
tviplayer = self._search_regex(
|
||||||
|
r'mediator\.bind\(({.+?})\s*,\s*document\.getElementById',
|
||||||
|
webpage, 'player', default=None)
|
||||||
|
|
||||||
|
if tviplayer:
|
||||||
|
player = self._parse_json(tviplayer, group_id).get('player', {})
|
||||||
|
duration = int_or_none(player.get('duration'))
|
||||||
|
programme_id = player.get('vpid')
|
||||||
|
|
||||||
|
if not programme_id:
|
||||||
|
programme_id = self._search_regex(
|
||||||
|
r'"vpid"\s*:\s*"(%s)"' % self._ID_REGEX, webpage, 'vpid', fatal=False, default=None)
|
||||||
|
|
||||||
|
if programme_id:
|
||||||
|
formats, subtitles = self._download_media_selector(programme_id)
|
||||||
|
title = self._og_search_title(webpage, default=None) or self._html_search_regex(
|
||||||
|
(r'<h2[^>]+id="parent-title"[^>]*>(.+?)</h2>',
|
||||||
|
r'<div[^>]+class="info"[^>]*>\s*<h1>(.+?)</h1>'), webpage, 'title')
|
||||||
|
description = self._search_regex(
|
||||||
|
(r'<p class="[^"]*medium-description[^"]*">([^<]+)</p>',
|
||||||
|
r'<div[^>]+class="info_+synopsis"[^>]*>([^<]+)</div>'),
|
||||||
|
webpage, 'description', default=None)
|
||||||
|
if not description:
|
||||||
|
description = self._html_search_meta('description', webpage)
|
||||||
|
else:
|
||||||
|
programme_id, title, description, duration, formats, subtitles = self._download_playlist(group_id)
|
||||||
|
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': programme_id,
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'thumbnail': self._og_search_thumbnail(webpage, default=None),
|
||||||
|
'duration': duration,
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BBCIE(BBCCoUkIE):
|
||||||
|
IE_NAME = 'bbc'
|
||||||
|
IE_DESC = 'BBC'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?bbc\.(?:com|co\.uk)/(?:[^/]+/)+(?P<id>[^/#?]+)'
|
||||||
|
|
||||||
|
_MEDIASELECTOR_URLS = [
|
||||||
|
# Provides HQ HLS streams but fails with geolocation in some cases when it's
|
||||||
|
# even not geo restricted at all
|
||||||
|
'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 = [{
|
||||||
|
# article with multiple videos embedded with data-playable containing vpids
|
||||||
|
'url': 'http://www.bbc.com/news/world-europe-32668511',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'world-europe-32668511',
|
||||||
|
'title': 'Russia stages massive WW2 parade despite Western boycott',
|
||||||
|
'description': 'md5:00ff61976f6081841f759a08bf78cc9c',
|
||||||
|
},
|
||||||
|
'playlist_count': 2,
|
||||||
|
}, {
|
||||||
|
# article with multiple videos embedded with data-playable (more videos)
|
||||||
|
'url': 'http://www.bbc.com/news/business-28299555',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'business-28299555',
|
||||||
|
'title': 'Farnborough Airshow: Video highlights',
|
||||||
|
'description': 'BBC reports and video highlights at the Farnborough Airshow.',
|
||||||
|
},
|
||||||
|
'playlist_count': 9,
|
||||||
|
'skip': 'Save time',
|
||||||
|
}, {
|
||||||
|
# article with multiple videos embedded with `new SMP()`
|
||||||
|
# broken
|
||||||
|
'url': 'http://www.bbc.co.uk/blogs/adamcurtis/entries/3662a707-0af9-3149-963f-47bea720b460',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '3662a707-0af9-3149-963f-47bea720b460',
|
||||||
|
'title': 'BBC Blogs - Adam Curtis - BUGGER',
|
||||||
|
},
|
||||||
|
'playlist_count': 18,
|
||||||
|
}, {
|
||||||
|
# single video embedded with data-playable containing vpid
|
||||||
|
'url': 'http://www.bbc.com/news/world-europe-32041533',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'p02mprgb',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Aerial footage showed the site of the crash in the Alps - courtesy BFM TV',
|
||||||
|
'description': 'md5:2868290467291b37feda7863f7a83f54',
|
||||||
|
'duration': 47,
|
||||||
|
'timestamp': 1427219242,
|
||||||
|
'upload_date': '20150324',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# rtmp download
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
# article with single video embedded with data-playable containing XML playlist
|
||||||
|
# with direct video links as progressiveDownloadUrl (for now these are extracted)
|
||||||
|
# and playlist with f4m and m3u8 as streamingUrl
|
||||||
|
'url': 'http://www.bbc.com/turkce/haberler/2015/06/150615_telabyad_kentin_cogu',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '150615_telabyad_kentin_cogu',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': "YPG: Tel Abyad'ın tamamı kontrolümüzde",
|
||||||
|
'timestamp': 1434397334,
|
||||||
|
'upload_date': '20150615',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
# single video embedded with data-playable containing XML playlists (regional section)
|
||||||
|
'url': 'http://www.bbc.com/mundo/video_fotos/2015/06/150619_video_honduras_militares_hospitales_corrupcion_aw',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '150619_video_honduras_militares_hospitales_corrupcion_aw',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Honduras militariza sus hospitales por nuevo escándalo de corrupción',
|
||||||
|
'timestamp': 1434713142,
|
||||||
|
'upload_date': '20150619',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
# single video from video playlist embedded with vxp-playlist-data JSON
|
||||||
|
'url': 'http://www.bbc.com/news/video_and_audio/must_see/33376376',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'p02w6qjc',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '''Judge Mindy Glazer: "I'm sorry to see you here... I always wondered what happened to you"''',
|
||||||
|
'duration': 56,
|
||||||
|
'description': '''Judge Mindy Glazer: "I'm sorry to see you here... I always wondered what happened to you"''',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
# single video story with digitalData
|
||||||
|
'url': 'http://www.bbc.com/travel/story/20150625-sri-lankas-spicy-secret',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'p02q6gc4',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': 'Sri Lanka’s spicy secret',
|
||||||
|
'description': 'As a new train line to Jaffna opens up the country’s north, travellers can experience a truly distinct slice of Tamil culture.',
|
||||||
|
'timestamp': 1437674293,
|
||||||
|
'upload_date': '20150723',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# rtmp download
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
# single video story without digitalData
|
||||||
|
'url': 'http://www.bbc.com/autos/story/20130513-hyundais-rock-star',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'p018zqqg',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Hyundai Santa Fe Sport: Rock star',
|
||||||
|
'description': 'md5:b042a26142c4154a6e472933cf20793d',
|
||||||
|
'timestamp': 1415867444,
|
||||||
|
'upload_date': '20141113',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# rtmp download
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
# single video with playlist.sxml URL in playlist param
|
||||||
|
'url': 'http://www.bbc.com/sport/0/football/33653409',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'p02xycnp',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Transfers: Cristiano Ronaldo to Man Utd, Arsenal to spend?',
|
||||||
|
'description': 'BBC Sport\'s David Ornstein has the latest transfer gossip, including rumours of a Manchester United return for Cristiano Ronaldo.',
|
||||||
|
'duration': 140,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# rtmp download
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
# article with multiple videos embedded with playlist.sxml in playlist param
|
||||||
|
'url': 'http://www.bbc.com/sport/0/football/34475836',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '34475836',
|
||||||
|
'title': 'What Liverpool can expect from Klopp',
|
||||||
|
},
|
||||||
|
'playlist_count': 3,
|
||||||
|
}, {
|
||||||
|
# single video with playlist URL from weather section
|
||||||
|
'url': 'http://www.bbc.com/weather/features/33601775',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# custom redirection to www.bbc.com
|
||||||
|
'url': 'http://www.bbc.co.uk/news/science-environment-33661876',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def suitable(cls, url):
|
||||||
|
return False if BBCCoUkIE.suitable(url) or BBCCoUkArticleIE.suitable(url) else super(BBCIE, cls).suitable(url)
|
||||||
|
|
||||||
|
def _extract_from_media_meta(self, media_meta, video_id):
|
||||||
|
# Direct links to media in media metadata (e.g.
|
||||||
|
# http://www.bbc.com/turkce/haberler/2015/06/150615_telabyad_kentin_cogu)
|
||||||
|
# TODO: there are also f4m and m3u8 streams incorporated in playlist.sxml
|
||||||
|
source_files = media_meta.get('sourceFiles')
|
||||||
|
if source_files:
|
||||||
|
return [{
|
||||||
|
'url': f['url'],
|
||||||
|
'format_id': format_id,
|
||||||
|
'ext': f.get('encoding'),
|
||||||
|
'tbr': float_or_none(f.get('bitrate'), 1000),
|
||||||
|
'filesize': int_or_none(f.get('filesize')),
|
||||||
|
} for format_id, f in source_files.items() if f.get('url')], []
|
||||||
|
|
||||||
|
programme_id = media_meta.get('externalId')
|
||||||
|
if programme_id:
|
||||||
|
return self._download_media_selector(programme_id)
|
||||||
|
|
||||||
|
# Process playlist.sxml as legacy playlist
|
||||||
|
href = media_meta.get('href')
|
||||||
|
if href:
|
||||||
|
playlist = self._download_legacy_playlist_url(href)
|
||||||
|
_, _, _, _, formats, subtitles = self._extract_from_legacy_playlist(playlist, video_id)
|
||||||
|
return formats, subtitles
|
||||||
|
|
||||||
|
return [], []
|
||||||
|
|
||||||
|
def _extract_from_playlist_sxml(self, url, playlist_id, timestamp):
|
||||||
|
programme_id, title, description, duration, formats, subtitles = \
|
||||||
|
self._process_legacy_playlist_url(url, playlist_id)
|
||||||
|
self._sort_formats(formats)
|
||||||
|
return {
|
||||||
|
'id': programme_id,
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'duration': duration,
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
playlist_id = self._match_id(url)
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, playlist_id)
|
||||||
|
|
||||||
|
json_ld_info = self._search_json_ld(webpage, playlist_id, default=None)
|
||||||
|
timestamp = json_ld_info.get('timestamp')
|
||||||
|
playlist_title = json_ld_info.get('title')
|
||||||
|
playlist_description = json_ld_info.get('description')
|
||||||
|
|
||||||
|
if not timestamp:
|
||||||
|
timestamp = parse_iso8601(self._search_regex(
|
||||||
|
[r'<meta[^>]+property="article:published_time"[^>]+content="([^"]+)"',
|
||||||
|
r'itemprop="datePublished"[^>]+datetime="([^"]+)"',
|
||||||
|
r'"datePublished":\s*"([^"]+)'],
|
||||||
|
webpage, 'date', default=None))
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
|
||||||
|
# article with multiple videos embedded with playlist.sxml (e.g.
|
||||||
|
# http://www.bbc.com/sport/0/football/34475836)
|
||||||
|
playlists = re.findall(r'<param[^>]+name="playlist"[^>]+value="([^"]+)"', webpage)
|
||||||
|
playlists.extend(re.findall(r'data-media-id="([^"]+/playlist\.sxml)"', webpage))
|
||||||
|
if playlists:
|
||||||
|
entries = [
|
||||||
|
self._extract_from_playlist_sxml(playlist_url, playlist_id, timestamp)
|
||||||
|
for playlist_url in playlists]
|
||||||
|
|
||||||
|
# news article with multiple videos embedded with data-playable
|
||||||
|
data_playables = re.findall(r'data-playable=(["\'])({.+?})\1', webpage)
|
||||||
|
if data_playables:
|
||||||
|
for _, data_playable_json in data_playables:
|
||||||
|
data_playable = self._parse_json(
|
||||||
|
unescapeHTML(data_playable_json), playlist_id, fatal=False)
|
||||||
|
if not data_playable:
|
||||||
|
continue
|
||||||
|
settings = data_playable.get('settings', {})
|
||||||
|
if settings:
|
||||||
|
# data-playable with video vpid in settings.playlistObject.items (e.g.
|
||||||
|
# http://www.bbc.com/news/world-us-canada-34473351)
|
||||||
|
playlist_object = settings.get('playlistObject', {})
|
||||||
|
if playlist_object:
|
||||||
|
items = playlist_object.get('items')
|
||||||
|
if items and isinstance(items, list):
|
||||||
|
title = playlist_object['title']
|
||||||
|
description = playlist_object.get('summary')
|
||||||
|
duration = int_or_none(items[0].get('duration'))
|
||||||
|
programme_id = items[0].get('vpid')
|
||||||
|
formats, subtitles = self._download_media_selector(programme_id)
|
||||||
|
self._sort_formats(formats)
|
||||||
|
entries.append({
|
||||||
|
'id': programme_id,
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'duration': duration,
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# data-playable without vpid but with a playlist.sxml URLs
|
||||||
|
# in otherSettings.playlist (e.g.
|
||||||
|
# http://www.bbc.com/turkce/multimedya/2015/10/151010_vid_ankara_patlama_ani)
|
||||||
|
playlist = data_playable.get('otherSettings', {}).get('playlist', {})
|
||||||
|
if playlist:
|
||||||
|
entries.append(self._extract_from_playlist_sxml(
|
||||||
|
playlist.get('progressiveDownloadUrl'), playlist_id, timestamp))
|
||||||
|
|
||||||
|
if entries:
|
||||||
|
playlist_title = playlist_title or remove_end(self._og_search_title(webpage), ' - BBC News')
|
||||||
|
playlist_description = playlist_description or self._og_search_description(webpage, default=None)
|
||||||
|
return self.playlist_result(entries, playlist_id, playlist_title, playlist_description)
|
||||||
|
|
||||||
|
# single video story (e.g. http://www.bbc.com/travel/story/20150625-sri-lankas-spicy-secret)
|
||||||
|
programme_id = self._search_regex(
|
||||||
|
[r'data-video-player-vpid="(%s)"' % self._ID_REGEX,
|
||||||
|
r'<param[^>]+name="externalIdentifier"[^>]+value="(%s)"' % self._ID_REGEX,
|
||||||
|
r'videoId\s*:\s*["\'](%s)["\']' % self._ID_REGEX],
|
||||||
|
webpage, 'vpid', default=None)
|
||||||
|
|
||||||
|
if programme_id:
|
||||||
|
formats, subtitles = self._download_media_selector(programme_id)
|
||||||
|
self._sort_formats(formats)
|
||||||
|
# digitalData may be missing (e.g. http://www.bbc.com/autos/story/20130513-hyundais-rock-star)
|
||||||
|
digital_data = self._parse_json(
|
||||||
|
self._search_regex(
|
||||||
|
r'var\s+digitalData\s*=\s*({.+?});?\n', webpage, 'digital data', default='{}'),
|
||||||
|
programme_id, fatal=False)
|
||||||
|
page_info = digital_data.get('page', {}).get('pageInfo', {})
|
||||||
|
title = page_info.get('pageName') or self._og_search_title(webpage)
|
||||||
|
description = page_info.get('description') or self._og_search_description(webpage)
|
||||||
|
timestamp = parse_iso8601(page_info.get('publicationDate')) or timestamp
|
||||||
|
return {
|
||||||
|
'id': programme_id,
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
}
|
||||||
|
|
||||||
|
playlist_title = self._html_search_regex(
|
||||||
|
r'<title>(.*?)(?:\s*-\s*BBC [^ ]+)?</title>', webpage, 'playlist title')
|
||||||
|
playlist_description = self._og_search_description(webpage, default=None)
|
||||||
|
|
||||||
|
def extract_all(pattern):
|
||||||
|
return list(filter(None, map(
|
||||||
|
lambda s: self._parse_json(s, playlist_id, fatal=False),
|
||||||
|
re.findall(pattern, webpage))))
|
||||||
|
|
||||||
|
# Multiple video article (e.g.
|
||||||
|
# http://www.bbc.co.uk/blogs/adamcurtis/entries/3662a707-0af9-3149-963f-47bea720b460)
|
||||||
|
EMBED_URL = r'https?://(?:www\.)?bbc\.co\.uk/(?:[^/]+/)+%s(?:\b[^"]+)?' % self._ID_REGEX
|
||||||
|
entries = []
|
||||||
|
for match in extract_all(r'new\s+SMP\(({.+?})\)'):
|
||||||
|
embed_url = match.get('playerSettings', {}).get('externalEmbedUrl')
|
||||||
|
if embed_url and re.match(EMBED_URL, embed_url):
|
||||||
|
entries.append(embed_url)
|
||||||
|
entries.extend(re.findall(
|
||||||
|
r'setPlaylist\("(%s)"\)' % EMBED_URL, webpage))
|
||||||
|
if entries:
|
||||||
|
return self.playlist_result(
|
||||||
|
[self.url_result(entry, 'BBCCoUk') for entry in entries],
|
||||||
|
playlist_id, playlist_title, playlist_description)
|
||||||
|
|
||||||
|
# Multiple video article (e.g. http://www.bbc.com/news/world-europe-32668511)
|
||||||
|
medias = extract_all(r"data-media-meta='({[^']+})'")
|
||||||
|
|
||||||
|
if not medias:
|
||||||
|
# Single video article (e.g. http://www.bbc.com/news/video_and_audio/international)
|
||||||
|
media_asset = self._search_regex(
|
||||||
|
r'mediaAssetPage\.init\(\s*({.+?}), "/',
|
||||||
|
webpage, 'media asset', default=None)
|
||||||
|
if media_asset:
|
||||||
|
media_asset_page = self._parse_json(media_asset, playlist_id, fatal=False)
|
||||||
|
medias = []
|
||||||
|
for video in media_asset_page.get('videos', {}).values():
|
||||||
|
medias.extend(video.values())
|
||||||
|
|
||||||
|
if not medias:
|
||||||
|
# Multiple video playlist with single `now playing` entry (e.g.
|
||||||
|
# http://www.bbc.com/news/video_and_audio/must_see/33767813)
|
||||||
|
vxp_playlist = self._parse_json(
|
||||||
|
self._search_regex(
|
||||||
|
r'<script[^>]+class="vxp-playlist-data"[^>]+type="application/json"[^>]*>([^<]+)</script>',
|
||||||
|
webpage, 'playlist data'),
|
||||||
|
playlist_id)
|
||||||
|
playlist_medias = []
|
||||||
|
for item in vxp_playlist:
|
||||||
|
media = item.get('media')
|
||||||
|
if not media:
|
||||||
|
continue
|
||||||
|
playlist_medias.append(media)
|
||||||
|
# Download single video if found media with asset id matching the video id from URL
|
||||||
|
if item.get('advert', {}).get('assetId') == playlist_id:
|
||||||
|
medias = [media]
|
||||||
|
break
|
||||||
|
# Fallback to the whole playlist
|
||||||
|
if not medias:
|
||||||
|
medias = playlist_medias
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
for num, media_meta in enumerate(medias, start=1):
|
||||||
|
formats, subtitles = self._extract_from_media_meta(media_meta, playlist_id)
|
||||||
|
if not formats:
|
||||||
|
continue
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
video_id = media_meta.get('externalId')
|
||||||
|
if not video_id:
|
||||||
|
video_id = playlist_id if len(medias) == 1 else '%s-%s' % (playlist_id, num)
|
||||||
|
|
||||||
|
title = media_meta.get('caption')
|
||||||
|
if not title:
|
||||||
|
title = playlist_title if len(medias) == 1 else '%s - Video %s' % (playlist_title, num)
|
||||||
|
|
||||||
|
duration = int_or_none(media_meta.get('durationInSeconds')) or parse_duration(media_meta.get('duration'))
|
||||||
|
|
||||||
|
images = []
|
||||||
|
for image in media_meta.get('images', {}).values():
|
||||||
|
images.extend(image.values())
|
||||||
|
if 'image' in media_meta:
|
||||||
|
images.append(media_meta['image'])
|
||||||
|
|
||||||
|
thumbnails = [{
|
||||||
|
'url': image.get('href'),
|
||||||
|
'width': int_or_none(image.get('width')),
|
||||||
|
'height': int_or_none(image.get('height')),
|
||||||
|
} for image in images]
|
||||||
|
|
||||||
|
entries.append({
|
||||||
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'thumbnails': thumbnails,
|
||||||
|
'duration': duration,
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
})
|
||||||
|
|
||||||
|
return self.playlist_result(entries, playlist_id, playlist_title, playlist_description)
|
||||||
|
|
||||||
|
|
||||||
|
class BBCCoUkArticleIE(InfoExtractor):
|
||||||
|
_VALID_URL = 'http://www.bbc.co.uk/programmes/articles/(?P<id>[a-zA-Z0-9]+)'
|
||||||
|
IE_NAME = 'bbc.co.uk:article'
|
||||||
|
IE_DESC = 'BBC articles'
|
||||||
|
|
||||||
|
_TEST = {
|
||||||
|
'url': 'http://www.bbc.co.uk/programmes/articles/3jNQLTMrPlYGTBn0WV6M2MS/not-your-typical-role-model-ada-lovelace-the-19th-century-programmer',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '3jNQLTMrPlYGTBn0WV6M2MS',
|
||||||
|
'title': 'Calculating Ada: The Countess of Computing - Not your typical role model: Ada Lovelace the 19th century programmer - BBC Four',
|
||||||
|
'description': 'Hannah Fry reveals some of her surprising discoveries about Ada Lovelace during filming.',
|
||||||
|
},
|
||||||
|
'playlist_count': 4,
|
||||||
|
'add_ie': ['BBCCoUk'],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
playlist_id = self._match_id(url)
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, playlist_id)
|
||||||
|
|
||||||
|
title = self._og_search_title(webpage)
|
||||||
|
description = self._og_search_description(webpage).strip()
|
||||||
|
|
||||||
|
entries = [self.url_result(programme_url) for programme_url in re.findall(
|
||||||
|
r'<div[^>]+typeof="Clip"[^>]+resource="([^"]+)"', webpage)]
|
||||||
|
|
||||||
|
return self.playlist_result(entries, playlist_id, title, description)
|
||||||
@@ -1,340 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import xml.etree.ElementTree
|
|
||||||
|
|
||||||
from .subtitles import SubtitlesInfoExtractor
|
|
||||||
from ..utils import ExtractorError
|
|
||||||
from ..compat import compat_HTTPError
|
|
||||||
|
|
||||||
|
|
||||||
class BBCCoUkIE(SubtitlesInfoExtractor):
|
|
||||||
IE_NAME = 'bbc.co.uk'
|
|
||||||
IE_DESC = 'BBC iPlayer'
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?bbc\.co\.uk/(?:(?:(?:programmes|iplayer(?:/[^/]+)?/(?:episode|playlist))/)|music/clips[/#])(?P<id>[\da-z]{8})'
|
|
||||||
|
|
||||||
_TESTS = [
|
|
||||||
{
|
|
||||||
'url': 'http://www.bbc.co.uk/programmes/b039g8p7',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'b039d07m',
|
|
||||||
'ext': 'flv',
|
|
||||||
'title': 'Kaleidoscope, Leonard Cohen',
|
|
||||||
'description': 'The Canadian poet and songwriter reflects on his musical career.',
|
|
||||||
'duration': 1740,
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
# rtmp download
|
|
||||||
'skip_download': True,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'url': 'http://www.bbc.co.uk/iplayer/episode/b00yng5w/The_Man_in_Black_Series_3_The_Printed_Name/',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'b00yng1d',
|
|
||||||
'ext': 'flv',
|
|
||||||
'title': 'The Man in Black: Series 3: The Printed Name',
|
|
||||||
'description': "Mark Gatiss introduces Nicholas Pierpan's chilling tale of a writer's devilish pact with a mysterious man. Stars Ewan Bailey.",
|
|
||||||
'duration': 1800,
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
# rtmp download
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
'skip': 'Episode is no longer available on BBC iPlayer Radio',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'url': 'http://www.bbc.co.uk/iplayer/episode/b03vhd1f/The_Voice_UK_Series_3_Blind_Auditions_5/',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'b00yng1d',
|
|
||||||
'ext': 'flv',
|
|
||||||
'title': 'The Voice UK: Series 3: Blind Auditions 5',
|
|
||||||
'description': "Emma Willis and Marvin Humes present the fifth set of blind auditions in the singing competition, as the coaches continue to build their teams based on voice alone.",
|
|
||||||
'duration': 5100,
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
# rtmp download
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
'skip': 'Currently BBC iPlayer TV programmes are available to play in the UK only',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'url': 'http://www.bbc.co.uk/iplayer/episode/p026c7jt/tomorrows-worlds-the-unearthly-history-of-science-fiction-2-invasion',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'b03k3pb7',
|
|
||||||
'ext': 'flv',
|
|
||||||
'title': "Tomorrow's Worlds: The Unearthly History of Science Fiction",
|
|
||||||
'description': '2. Invasion',
|
|
||||||
'duration': 3600,
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
# rtmp download
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
'skip': 'Currently BBC iPlayer TV programmes are available to play in the UK only',
|
|
||||||
}, {
|
|
||||||
'url': 'http://www.bbc.co.uk/programmes/b04v20dw',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'b04v209v',
|
|
||||||
'ext': 'flv',
|
|
||||||
'title': 'Pete Tong, The Essential New Tune Special',
|
|
||||||
'description': "Pete has a very special mix - all of 2014's Essential New Tunes!",
|
|
||||||
'duration': 10800,
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
# rtmp download
|
|
||||||
'skip_download': True,
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
'url': 'http://www.bbc.co.uk/music/clips/p02frcc3',
|
|
||||||
'note': 'Audio',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'p02frcch',
|
|
||||||
'ext': 'flv',
|
|
||||||
'title': 'Pete Tong, Past, Present and Future Special, Madeon - After Hours mix',
|
|
||||||
'description': 'French house superstar Madeon takes us out of the club and onto the after party.',
|
|
||||||
'duration': 3507,
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
# rtmp download
|
|
||||||
'skip_download': True,
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
'url': 'http://www.bbc.co.uk/music/clips/p025c0zz',
|
|
||||||
'note': 'Video',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'p025c103',
|
|
||||||
'ext': 'flv',
|
|
||||||
'title': 'Reading and Leeds Festival, 2014, Rae Morris - Closer (Live on BBC Three)',
|
|
||||||
'description': 'Rae Morris performs Closer for BBC Three at Reading 2014',
|
|
||||||
'duration': 226,
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
# rtmp download
|
|
||||||
'skip_download': True,
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
'url': 'http://www.bbc.co.uk/iplayer/playlist/p01dvks4',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'http://www.bbc.co.uk/music/clips#p02frcc3',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'http://www.bbc.co.uk/iplayer/cbeebies/episode/b0480276/bing-14-atchoo',
|
|
||||||
'only_matching': True,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
def _extract_asx_playlist(self, connection, programme_id):
|
|
||||||
asx = self._download_xml(connection.get('href'), programme_id, 'Downloading ASX playlist')
|
|
||||||
return [ref.get('href') for ref in asx.findall('./Entry/ref')]
|
|
||||||
|
|
||||||
def _extract_connection(self, connection, programme_id):
|
|
||||||
formats = []
|
|
||||||
protocol = connection.get('protocol')
|
|
||||||
supplier = connection.get('supplier')
|
|
||||||
if protocol == 'http':
|
|
||||||
href = connection.get('href')
|
|
||||||
# ASX playlist
|
|
||||||
if supplier == 'asx':
|
|
||||||
for i, ref in enumerate(self._extract_asx_playlist(connection, programme_id)):
|
|
||||||
formats.append({
|
|
||||||
'url': ref,
|
|
||||||
'format_id': 'ref%s_%s' % (i, supplier),
|
|
||||||
})
|
|
||||||
# Direct link
|
|
||||||
else:
|
|
||||||
formats.append({
|
|
||||||
'url': href,
|
|
||||||
'format_id': supplier,
|
|
||||||
})
|
|
||||||
elif protocol == 'rtmp':
|
|
||||||
application = connection.get('application', 'ondemand')
|
|
||||||
auth_string = connection.get('authString')
|
|
||||||
identifier = connection.get('identifier')
|
|
||||||
server = connection.get('server')
|
|
||||||
formats.append({
|
|
||||||
'url': '%s://%s/%s?%s' % (protocol, server, application, auth_string),
|
|
||||||
'play_path': identifier,
|
|
||||||
'app': '%s?%s' % (application, auth_string),
|
|
||||||
'page_url': 'http://www.bbc.co.uk',
|
|
||||||
'player_url': 'http://www.bbc.co.uk/emp/releases/iplayer/revisions/617463_618125_4/617463_618125_4_emp.swf',
|
|
||||||
'rtmp_live': False,
|
|
||||||
'ext': 'flv',
|
|
||||||
'format_id': supplier,
|
|
||||||
})
|
|
||||||
return formats
|
|
||||||
|
|
||||||
def _extract_items(self, playlist):
|
|
||||||
return playlist.findall('./{http://bbc.co.uk/2008/emp/playlist}item')
|
|
||||||
|
|
||||||
def _extract_medias(self, media_selection):
|
|
||||||
error = media_selection.find('./{http://bbc.co.uk/2008/mp/mediaselection}error')
|
|
||||||
if error is not None:
|
|
||||||
raise ExtractorError(
|
|
||||||
'%s returned error: %s' % (self.IE_NAME, error.get('id')), expected=True)
|
|
||||||
return media_selection.findall('./{http://bbc.co.uk/2008/mp/mediaselection}media')
|
|
||||||
|
|
||||||
def _extract_connections(self, media):
|
|
||||||
return media.findall('./{http://bbc.co.uk/2008/mp/mediaselection}connection')
|
|
||||||
|
|
||||||
def _extract_video(self, media, programme_id):
|
|
||||||
formats = []
|
|
||||||
vbr = int(media.get('bitrate'))
|
|
||||||
vcodec = media.get('encoding')
|
|
||||||
service = media.get('service')
|
|
||||||
width = int(media.get('width'))
|
|
||||||
height = int(media.get('height'))
|
|
||||||
file_size = int(media.get('media_file_size'))
|
|
||||||
for connection in self._extract_connections(media):
|
|
||||||
conn_formats = self._extract_connection(connection, programme_id)
|
|
||||||
for format in conn_formats:
|
|
||||||
format.update({
|
|
||||||
'format_id': '%s_%s' % (service, format['format_id']),
|
|
||||||
'width': width,
|
|
||||||
'height': height,
|
|
||||||
'vbr': vbr,
|
|
||||||
'vcodec': vcodec,
|
|
||||||
'filesize': file_size,
|
|
||||||
})
|
|
||||||
formats.extend(conn_formats)
|
|
||||||
return formats
|
|
||||||
|
|
||||||
def _extract_audio(self, media, programme_id):
|
|
||||||
formats = []
|
|
||||||
abr = int(media.get('bitrate'))
|
|
||||||
acodec = media.get('encoding')
|
|
||||||
service = media.get('service')
|
|
||||||
for connection in self._extract_connections(media):
|
|
||||||
conn_formats = self._extract_connection(connection, programme_id)
|
|
||||||
for format in conn_formats:
|
|
||||||
format.update({
|
|
||||||
'format_id': '%s_%s' % (service, format['format_id']),
|
|
||||||
'abr': abr,
|
|
||||||
'acodec': acodec,
|
|
||||||
})
|
|
||||||
formats.extend(conn_formats)
|
|
||||||
return formats
|
|
||||||
|
|
||||||
def _extract_captions(self, media, programme_id):
|
|
||||||
subtitles = {}
|
|
||||||
for connection in self._extract_connections(media):
|
|
||||||
captions = self._download_xml(connection.get('href'), programme_id, 'Downloading captions')
|
|
||||||
lang = captions.get('{http://www.w3.org/XML/1998/namespace}lang', 'en')
|
|
||||||
ps = captions.findall('./{0}body/{0}div/{0}p'.format('{http://www.w3.org/2006/10/ttaf1}'))
|
|
||||||
srt = ''
|
|
||||||
for pos, p in enumerate(ps):
|
|
||||||
srt += '%s\r\n%s --> %s\r\n%s\r\n\r\n' % (str(pos), p.get('begin'), p.get('end'),
|
|
||||||
p.text.strip() if p.text is not None else '')
|
|
||||||
subtitles[lang] = srt
|
|
||||||
return subtitles
|
|
||||||
|
|
||||||
def _download_media_selector(self, programme_id):
|
|
||||||
try:
|
|
||||||
media_selection = self._download_xml(
|
|
||||||
'http://open.live.bbc.co.uk/mediaselector/5/select/version/2.0/mediaset/pc/vpid/%s' % programme_id,
|
|
||||||
programme_id, 'Downloading media selection XML')
|
|
||||||
except ExtractorError as ee:
|
|
||||||
if isinstance(ee.cause, compat_HTTPError) and ee.cause.code == 403:
|
|
||||||
media_selection = xml.etree.ElementTree.fromstring(ee.cause.read().encode('utf-8'))
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
formats = []
|
|
||||||
subtitles = None
|
|
||||||
|
|
||||||
for media in self._extract_medias(media_selection):
|
|
||||||
kind = media.get('kind')
|
|
||||||
if kind == 'audio':
|
|
||||||
formats.extend(self._extract_audio(media, programme_id))
|
|
||||||
elif kind == 'video':
|
|
||||||
formats.extend(self._extract_video(media, programme_id))
|
|
||||||
elif kind == 'captions':
|
|
||||||
subtitles = self._extract_captions(media, programme_id)
|
|
||||||
|
|
||||||
return formats, subtitles
|
|
||||||
|
|
||||||
def _download_playlist(self, playlist_id):
|
|
||||||
try:
|
|
||||||
playlist = self._download_json(
|
|
||||||
'http://www.bbc.co.uk/programmes/%s/playlist.json' % playlist_id,
|
|
||||||
playlist_id, 'Downloading playlist JSON')
|
|
||||||
|
|
||||||
version = playlist.get('defaultAvailableVersion')
|
|
||||||
if version:
|
|
||||||
smp_config = version['smpConfig']
|
|
||||||
title = smp_config['title']
|
|
||||||
description = smp_config['summary']
|
|
||||||
for item in smp_config['items']:
|
|
||||||
kind = item['kind']
|
|
||||||
if kind != 'programme' and kind != 'radioProgramme':
|
|
||||||
continue
|
|
||||||
programme_id = item.get('vpid')
|
|
||||||
duration = int(item.get('duration'))
|
|
||||||
formats, subtitles = self._download_media_selector(programme_id)
|
|
||||||
return programme_id, title, description, duration, formats, subtitles
|
|
||||||
except ExtractorError as ee:
|
|
||||||
if not (isinstance(ee.cause, compat_HTTPError) and ee.cause.code == 404):
|
|
||||||
raise
|
|
||||||
|
|
||||||
# fallback to legacy playlist
|
|
||||||
playlist = self._download_xml(
|
|
||||||
'http://www.bbc.co.uk/iplayer/playlist/%s' % playlist_id,
|
|
||||||
playlist_id, 'Downloading legacy playlist XML')
|
|
||||||
|
|
||||||
no_items = playlist.find('./{http://bbc.co.uk/2008/emp/playlist}noItems')
|
|
||||||
if no_items is not None:
|
|
||||||
reason = no_items.get('reason')
|
|
||||||
if reason == 'preAvailability':
|
|
||||||
msg = 'Episode %s is not yet available' % playlist_id
|
|
||||||
elif reason == 'postAvailability':
|
|
||||||
msg = 'Episode %s is no longer available' % playlist_id
|
|
||||||
elif reason == 'noMedia':
|
|
||||||
msg = 'Episode %s is not currently available' % playlist_id
|
|
||||||
else:
|
|
||||||
msg = 'Episode %s is not available: %s' % (playlist_id, reason)
|
|
||||||
raise ExtractorError(msg, expected=True)
|
|
||||||
|
|
||||||
for item in self._extract_items(playlist):
|
|
||||||
kind = item.get('kind')
|
|
||||||
if kind != 'programme' and kind != 'radioProgramme':
|
|
||||||
continue
|
|
||||||
title = playlist.find('./{http://bbc.co.uk/2008/emp/playlist}title').text
|
|
||||||
description = playlist.find('./{http://bbc.co.uk/2008/emp/playlist}summary').text
|
|
||||||
programme_id = item.get('identifier')
|
|
||||||
duration = int(item.get('duration'))
|
|
||||||
formats, subtitles = self._download_media_selector(programme_id)
|
|
||||||
|
|
||||||
return programme_id, title, description, duration, formats, subtitles
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
group_id = self._match_id(url)
|
|
||||||
|
|
||||||
webpage = self._download_webpage(url, group_id, 'Downloading video page')
|
|
||||||
|
|
||||||
programme_id = self._search_regex(
|
|
||||||
r'"vpid"\s*:\s*"([\da-z]{8})"', webpage, 'vpid', fatal=False, default=None)
|
|
||||||
if programme_id:
|
|
||||||
player = self._download_json(
|
|
||||||
'http://www.bbc.co.uk/iplayer/episode/%s.json' % group_id,
|
|
||||||
group_id)['jsConf']['player']
|
|
||||||
title = player['title']
|
|
||||||
description = player['subtitle']
|
|
||||||
duration = player['duration']
|
|
||||||
formats, subtitles = self._download_media_selector(programme_id)
|
|
||||||
else:
|
|
||||||
programme_id, title, description, duration, formats, subtitles = self._download_playlist(group_id)
|
|
||||||
|
|
||||||
if self._downloader.params.get('listsubtitles', False):
|
|
||||||
self._list_available_subtitles(programme_id, subtitles)
|
|
||||||
return
|
|
||||||
|
|
||||||
self._sort_formats(formats)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': programme_id,
|
|
||||||
'title': title,
|
|
||||||
'description': description,
|
|
||||||
'duration': duration,
|
|
||||||
'formats': formats,
|
|
||||||
'subtitles': subtitles,
|
|
||||||
}
|
|
||||||
103
youtube_dl/extractor/beatportpro.py
Normal file
103
youtube_dl/extractor/beatportpro.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
class BeatportProIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://pro\.beatport\.com/track/(?P<display_id>[^/]+)/(?P<id>[0-9]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://pro.beatport.com/track/synesthesia-original-mix/5379371',
|
||||||
|
'md5': 'b3c34d8639a2f6a7f734382358478887',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '5379371',
|
||||||
|
'display_id': 'synesthesia-original-mix',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Froxic - Synesthesia (Original Mix)',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://pro.beatport.com/track/love-and-war-original-mix/3756896',
|
||||||
|
'md5': 'e44c3025dfa38c6577fbaeb43da43514',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '3756896',
|
||||||
|
'display_id': 'love-and-war-original-mix',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'title': 'Wolfgang Gartner - Love & War (Original Mix)',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://pro.beatport.com/track/birds-original-mix/4991738',
|
||||||
|
'md5': 'a1fd8e8046de3950fd039304c186c05f',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '4991738',
|
||||||
|
'display_id': 'birds-original-mix',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': "Tos, Middle Milk, Mumblin' Johnsson - Birds (Original Mix)",
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
mobj = re.match(self._VALID_URL, url)
|
||||||
|
track_id = mobj.group('id')
|
||||||
|
display_id = mobj.group('display_id')
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
|
||||||
|
playables = self._parse_json(
|
||||||
|
self._search_regex(
|
||||||
|
r'window\.Playables\s*=\s*({.+?});', webpage,
|
||||||
|
'playables info', flags=re.DOTALL),
|
||||||
|
track_id)
|
||||||
|
|
||||||
|
track = next(t for t in playables['tracks'] if t['id'] == int(track_id))
|
||||||
|
|
||||||
|
title = ', '.join((a['name'] for a in track['artists'])) + ' - ' + track['name']
|
||||||
|
if track['mix']:
|
||||||
|
title += ' (' + track['mix'] + ')'
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
for ext, info in track['preview'].items():
|
||||||
|
if not info['url']:
|
||||||
|
continue
|
||||||
|
fmt = {
|
||||||
|
'url': info['url'],
|
||||||
|
'ext': ext,
|
||||||
|
'format_id': ext,
|
||||||
|
'vcodec': 'none',
|
||||||
|
}
|
||||||
|
if ext == 'mp3':
|
||||||
|
fmt['preference'] = 0
|
||||||
|
fmt['acodec'] = 'mp3'
|
||||||
|
fmt['abr'] = 96
|
||||||
|
fmt['asr'] = 44100
|
||||||
|
elif ext == 'mp4':
|
||||||
|
fmt['preference'] = 1
|
||||||
|
fmt['acodec'] = 'aac'
|
||||||
|
fmt['abr'] = 96
|
||||||
|
fmt['asr'] = 44100
|
||||||
|
formats.append(fmt)
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
images = []
|
||||||
|
for name, info in track['images'].items():
|
||||||
|
image_url = info.get('url')
|
||||||
|
if name == 'dynamic' or not image_url:
|
||||||
|
continue
|
||||||
|
image = {
|
||||||
|
'id': name,
|
||||||
|
'url': image_url,
|
||||||
|
'height': int_or_none(info.get('height')),
|
||||||
|
'width': int_or_none(info.get('width')),
|
||||||
|
}
|
||||||
|
images.append(image)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': compat_str(track.get('id')) or track_id,
|
||||||
|
'display_id': track.get('slug') or display_id,
|
||||||
|
'title': title,
|
||||||
|
'formats': formats,
|
||||||
|
'thumbnails': images,
|
||||||
|
}
|
||||||
@@ -1,65 +1,105 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
from ..compat import (
|
||||||
|
compat_chr,
|
||||||
|
compat_ord,
|
||||||
|
compat_urllib_parse_unquote,
|
||||||
|
)
|
||||||
|
from ..utils import (
|
||||||
|
int_or_none,
|
||||||
|
parse_iso8601,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BeegIE(InfoExtractor):
|
class BeegIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?beeg\.com/(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:www\.)?beeg\.com/(?P<id>\d+)'
|
||||||
_TEST = {
|
_TEST = {
|
||||||
'url': 'http://beeg.com/5416503',
|
'url': 'http://beeg.com/5416503',
|
||||||
'md5': '1bff67111adb785c51d1b42959ec10e5',
|
'md5': '46c384def73b33dbc581262e5ee67cef',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '5416503',
|
'id': '5416503',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Sultry Striptease',
|
'title': 'Sultry Striptease',
|
||||||
'description': 'md5:6db3c6177972822aaba18652ff59c773',
|
'description': 'md5:d22219c09da287c14bed3d6c37ce4bc2',
|
||||||
'categories': list, # NSFW
|
'timestamp': 1391813355,
|
||||||
'thumbnail': 're:https?://.*\.jpg$',
|
'upload_date': '20140207',
|
||||||
|
'duration': 383,
|
||||||
|
'tags': list,
|
||||||
'age_limit': 18,
|
'age_limit': 18,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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('id')
|
|
||||||
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
video = self._download_json(
|
||||||
|
'https://api.beeg.com/api/v5/video/%s' % video_id, video_id)
|
||||||
|
|
||||||
quality_arr = self._search_regex(
|
def split(o, e):
|
||||||
r'(?s)var\s+qualityArr\s*=\s*{\s*(.+?)\s*}', webpage, 'quality formats')
|
def cut(s, x):
|
||||||
|
n.append(s[:x])
|
||||||
|
return s[x:]
|
||||||
|
n = []
|
||||||
|
r = len(o) % e
|
||||||
|
if r > 0:
|
||||||
|
o = cut(o, r)
|
||||||
|
while len(o) > e:
|
||||||
|
o = cut(o, e)
|
||||||
|
n.append(o)
|
||||||
|
return n
|
||||||
|
|
||||||
formats = [{
|
def decrypt_key(key):
|
||||||
'url': fmt[1],
|
# Reverse engineered from http://static.beeg.com/cpl/1105.js
|
||||||
'format_id': fmt[0],
|
a = '5ShMcIQlssOd7zChAIOlmeTZDaUxULbJRnywYaiB'
|
||||||
'height': int(fmt[0][:-1]),
|
e = compat_urllib_parse_unquote(key)
|
||||||
} for fmt in re.findall(r"'([^']+)'\s*:\s*'([^']+)'", quality_arr)]
|
o = ''.join([
|
||||||
|
compat_chr(compat_ord(e[n]) - compat_ord(a[n % len(a)]) % 21)
|
||||||
|
for n in range(len(e))])
|
||||||
|
return ''.join(split(o, 3)[::-1])
|
||||||
|
|
||||||
|
def decrypt_url(encrypted_url):
|
||||||
|
encrypted_url = self._proto_relative_url(
|
||||||
|
encrypted_url.replace('{DATA_MARKERS}', ''), 'https:')
|
||||||
|
key = self._search_regex(
|
||||||
|
r'/key=(.*?)%2Cend=', encrypted_url, 'key', default=None)
|
||||||
|
if not key:
|
||||||
|
return encrypted_url
|
||||||
|
return encrypted_url.replace(key, decrypt_key(key))
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
for format_id, video_url in video.items():
|
||||||
|
if not video_url:
|
||||||
|
continue
|
||||||
|
height = self._search_regex(
|
||||||
|
r'^(\d+)[pP]$', format_id, 'height', default=None)
|
||||||
|
if not height:
|
||||||
|
continue
|
||||||
|
formats.append({
|
||||||
|
'url': decrypt_url(video_url),
|
||||||
|
'format_id': format_id,
|
||||||
|
'height': int(height),
|
||||||
|
})
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
title = self._html_search_regex(
|
title = video['title']
|
||||||
r'<title>([^<]+)\s*-\s*beeg\.?</title>', webpage, 'title')
|
video_id = video.get('id') or video_id
|
||||||
|
display_id = video.get('code')
|
||||||
|
description = video.get('desc')
|
||||||
|
|
||||||
description = self._html_search_regex(
|
timestamp = parse_iso8601(video.get('date'), ' ')
|
||||||
r'<meta name="description" content="([^"]*)"',
|
duration = int_or_none(video.get('duration'))
|
||||||
webpage, 'description', fatal=False)
|
|
||||||
thumbnail = self._html_search_regex(
|
|
||||||
r'\'previewer.url\'\s*:\s*"([^"]*)"',
|
|
||||||
webpage, 'thumbnail', fatal=False)
|
|
||||||
|
|
||||||
categories_str = self._html_search_regex(
|
tags = [tag.strip() for tag in video['tags'].split(',')] if video.get('tags') else None
|
||||||
r'<meta name="keywords" content="([^"]+)"', webpage, 'categories', fatal=False)
|
|
||||||
categories = (
|
|
||||||
None if categories_str is None
|
|
||||||
else categories_str.split(','))
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
|
'display_id': display_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': description,
|
'description': description,
|
||||||
'thumbnail': thumbnail,
|
'timestamp': timestamp,
|
||||||
'categories': categories,
|
'duration': duration,
|
||||||
|
'tags': tags,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'age_limit': 18,
|
'age_limit': 18,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import compat_urllib_parse
|
from ..compat import compat_urllib_parse_unquote
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
xpath_text,
|
xpath_text,
|
||||||
xpath_with_ns,
|
xpath_with_ns,
|
||||||
@@ -16,11 +16,11 @@ class BetIE(InfoExtractor):
|
|||||||
{
|
{
|
||||||
'url': 'http://www.bet.com/news/politics/2014/12/08/in-bet-exclusive-obama-talks-race-and-racism.html',
|
'url': 'http://www.bet.com/news/politics/2014/12/08/in-bet-exclusive-obama-talks-race-and-racism.html',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '740ab250-bb94-4a8a-8787-fe0de7c74471',
|
'id': 'news/national/2014/a-conversation-with-president-obama',
|
||||||
'display_id': 'in-bet-exclusive-obama-talks-race-and-racism',
|
'display_id': 'in-bet-exclusive-obama-talks-race-and-racism',
|
||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
'title': 'BET News Presents: A Conversation With President Obama',
|
'title': 'A Conversation With President Obama',
|
||||||
'description': 'md5:5a88d8ae912c1b33e090290af7ec33c6',
|
'description': 'md5:699d0652a350cf3e491cd15cc745b5da',
|
||||||
'duration': 1534,
|
'duration': 1534,
|
||||||
'timestamp': 1418075340,
|
'timestamp': 1418075340,
|
||||||
'upload_date': '20141208',
|
'upload_date': '20141208',
|
||||||
@@ -35,7 +35,7 @@ class BetIE(InfoExtractor):
|
|||||||
{
|
{
|
||||||
'url': 'http://www.bet.com/video/news/national/2014/justice-for-ferguson-a-community-reacts.html',
|
'url': 'http://www.bet.com/video/news/national/2014/justice-for-ferguson-a-community-reacts.html',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'bcd1b1df-673a-42cf-8d01-b282db608f2d',
|
'id': 'news/national/2014/justice-for-ferguson-a-community-reacts',
|
||||||
'display_id': 'justice-for-ferguson-a-community-reacts',
|
'display_id': 'justice-for-ferguson-a-community-reacts',
|
||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
'title': 'Justice for Ferguson: A Community Reacts',
|
'title': 'Justice for Ferguson: A Community Reacts',
|
||||||
@@ -57,10 +57,13 @@ class BetIE(InfoExtractor):
|
|||||||
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)
|
||||||
|
|
||||||
media_url = compat_urllib_parse.unquote(self._search_regex(
|
media_url = compat_urllib_parse_unquote(self._search_regex(
|
||||||
[r'mediaURL\s*:\s*"([^"]+)"', r"var\s+mrssMediaUrl\s*=\s*'([^']+)'"],
|
[r'mediaURL\s*:\s*"([^"]+)"', r"var\s+mrssMediaUrl\s*=\s*'([^']+)'"],
|
||||||
webpage, 'media URL'))
|
webpage, 'media URL'))
|
||||||
|
|
||||||
|
video_id = self._search_regex(
|
||||||
|
r'/video/(.*)/_jcr_content/', media_url, 'video id')
|
||||||
|
|
||||||
mrss = self._download_xml(media_url, display_id)
|
mrss = self._download_xml(media_url, display_id)
|
||||||
|
|
||||||
item = mrss.find('./channel/item')
|
item = mrss.find('./channel/item')
|
||||||
@@ -75,8 +78,6 @@ class BetIE(InfoExtractor):
|
|||||||
description = xpath_text(
|
description = xpath_text(
|
||||||
item, './description', 'description', fatal=False)
|
item, './description', 'description', fatal=False)
|
||||||
|
|
||||||
video_id = xpath_text(item, './guid', 'video id', fatal=False)
|
|
||||||
|
|
||||||
timestamp = parse_iso8601(xpath_text(
|
timestamp = parse_iso8601(xpath_text(
|
||||||
item, xpath_with_ns('./dc:date', NS_MAP),
|
item, xpath_with_ns('./dc:date', NS_MAP),
|
||||||
'upload date', fatal=False))
|
'upload date', fatal=False))
|
||||||
|
|||||||
85
youtube_dl/extractor/bigflix.py
Normal file
85
youtube_dl/extractor/bigflix.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..compat import compat_urllib_parse_unquote
|
||||||
|
|
||||||
|
|
||||||
|
class BigflixIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?bigflix\.com/.+/(?P<id>[0-9]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://www.bigflix.com/Hindi-movies/Action-movies/Singham-Returns/16537',
|
||||||
|
'md5': 'ec76aa9b1129e2e5b301a474e54fab74',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '16537',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Singham Returns',
|
||||||
|
'description': 'md5:3d2ba5815f14911d5cc6a501ae0cf65d',
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
# 2 formats
|
||||||
|
'url': 'http://www.bigflix.com/Tamil-movies/Drama-movies/Madarasapatinam/16070',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '16070',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Madarasapatinam',
|
||||||
|
'description': 'md5:63b9b8ed79189c6f0418c26d9a3452ca',
|
||||||
|
'formats': 'mincount:2',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
# multiple formats
|
||||||
|
'url': 'http://www.bigflix.com/Malayalam-movies/Drama-movies/Indian-Rupee/15967',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
title = self._html_search_regex(
|
||||||
|
r'<div[^>]+class=["\']pagetitle["\'][^>]*>(.+?)</div>',
|
||||||
|
webpage, 'title')
|
||||||
|
|
||||||
|
def decode_url(quoted_b64_url):
|
||||||
|
return base64.b64decode(compat_urllib_parse_unquote(
|
||||||
|
quoted_b64_url).encode('ascii')).decode('utf-8')
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
for height, encoded_url in re.findall(
|
||||||
|
r'ContentURL_(\d{3,4})[pP][^=]+=([^&]+)', webpage):
|
||||||
|
video_url = decode_url(encoded_url)
|
||||||
|
f = {
|
||||||
|
'url': video_url,
|
||||||
|
'format_id': '%sp' % height,
|
||||||
|
'height': int(height),
|
||||||
|
}
|
||||||
|
if video_url.startswith('rtmp'):
|
||||||
|
f['ext'] = 'flv'
|
||||||
|
formats.append(f)
|
||||||
|
|
||||||
|
file_url = self._search_regex(
|
||||||
|
r'file=([^&]+)', webpage, 'video url', default=None)
|
||||||
|
if file_url:
|
||||||
|
video_url = decode_url(file_url)
|
||||||
|
if all(f['url'] != video_url for f in formats):
|
||||||
|
formats.append({
|
||||||
|
'url': decode_url(file_url),
|
||||||
|
})
|
||||||
|
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
description = self._html_search_meta('description', webpage)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'formats': formats
|
||||||
|
}
|
||||||
@@ -2,7 +2,10 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import int_or_none
|
from ..utils import (
|
||||||
|
int_or_none,
|
||||||
|
unescapeHTML,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BildIE(InfoExtractor):
|
class BildIE(InfoExtractor):
|
||||||
@@ -14,26 +17,24 @@ class BildIE(InfoExtractor):
|
|||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '38184146',
|
'id': '38184146',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'BILD hat sie getestet',
|
'title': 'Das können die neuen iPads',
|
||||||
'thumbnail': 'http://bilder.bild.de/fotos/stand-das-koennen-die-neuen-ipads-38184138/Bild/1.bild.jpg',
|
'description': 'md5:a4058c4fa2a804ab59c00d7244bbf62f',
|
||||||
|
'thumbnail': 're:^https?://.*\.jpg$',
|
||||||
'duration': 196,
|
'duration': 196,
|
||||||
'description': 'Mit dem iPad Air 2 und dem iPad Mini 3 hat Apple zwei neue Tablet-Modelle präsentiert. BILD-Reporter Sven Stein durfte die Geräte bereits testen. ',
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
xml_url = url.split(".bild.html")[0] + ",view=xml.bild.xml"
|
video_data = self._download_json(
|
||||||
doc = self._download_xml(xml_url, video_id)
|
url.split('.bild.html')[0] + ',view=json.bild.html', video_id)
|
||||||
|
|
||||||
duration = int_or_none(doc.attrib.get('duration'), scale=1000)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': doc.attrib['ueberschrift'],
|
'title': unescapeHTML(video_data['title']).strip(),
|
||||||
'description': doc.attrib.get('text'),
|
'description': unescapeHTML(video_data.get('description')),
|
||||||
'url': doc.attrib['src'],
|
'url': video_data['clipList'][0]['srces'][0]['src'],
|
||||||
'thumbnail': doc.attrib.get('img'),
|
'thumbnail': video_data.get('poster'),
|
||||||
'duration': duration,
|
'duration': int_or_none(video_data.get('durationSec')),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,93 +4,107 @@ from __future__ import unicode_literals
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
from ..compat import compat_str
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
int_or_none,
|
int_or_none,
|
||||||
unified_strdate,
|
unescapeHTML,
|
||||||
|
ExtractorError,
|
||||||
|
xpath_text,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BiliBiliIE(InfoExtractor):
|
class BiliBiliIE(InfoExtractor):
|
||||||
_VALID_URL = r'http://www\.bilibili\.(?:tv|com)/video/av(?P<id>[0-9]+)/'
|
_VALID_URL = r'http://www\.bilibili\.(?:tv|com)/video/av(?P<id>\d+)(?:/index_(?P<page_num>\d+).html)?'
|
||||||
|
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'http://www.bilibili.tv/video/av1074402/',
|
'url': 'http://www.bilibili.tv/video/av1074402/',
|
||||||
'md5': '2c301e4dab317596e837c3e7633e7d86',
|
'md5': '2c301e4dab317596e837c3e7633e7d86',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '1074402',
|
'id': '1554319',
|
||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
'title': '【金坷垃】金泡沫',
|
'title': '【金坷垃】金泡沫',
|
||||||
'duration': 308,
|
'duration': 308313,
|
||||||
'upload_date': '20140420',
|
'upload_date': '20140420',
|
||||||
'thumbnail': 're:^https?://.+\.jpg',
|
'thumbnail': 're:^https?://.+\.jpg',
|
||||||
|
'description': 'md5:ce18c2a2d2193f0df2917d270f2e5923',
|
||||||
|
'timestamp': 1397983878,
|
||||||
|
'uploader': '菊子桑',
|
||||||
},
|
},
|
||||||
}
|
}, {
|
||||||
|
'url': 'http://www.bilibili.com/video/av1041170/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1041170',
|
||||||
|
'title': '【BD1080P】刀语【诸神&异域】',
|
||||||
|
'description': '这是个神奇的故事~每个人不留弹幕不给走哦~切利哦!~',
|
||||||
|
'uploader': '枫叶逝去',
|
||||||
|
'timestamp': 1396501299,
|
||||||
|
},
|
||||||
|
'playlist_count': 9,
|
||||||
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
webpage = self._download_webpage(url, video_id)
|
video_id = mobj.group('id')
|
||||||
|
page_num = mobj.group('page_num') or '1'
|
||||||
|
|
||||||
video_code = self._search_regex(
|
view_data = self._download_json(
|
||||||
r'(?s)<div itemprop="video".*?>(.*?)</div>', webpage, 'video code')
|
'http://api.bilibili.com/view?type=json&appkey=8e9fc618fbd41e28&id=%s&page=%s' % (video_id, page_num),
|
||||||
|
video_id)
|
||||||
|
if 'error' in view_data:
|
||||||
|
raise ExtractorError('%s said: %s' % (self.IE_NAME, view_data['error']), expected=True)
|
||||||
|
|
||||||
title = self._html_search_meta(
|
cid = view_data['cid']
|
||||||
'media:title', video_code, 'title', fatal=True)
|
title = unescapeHTML(view_data['title'])
|
||||||
duration_str = self._html_search_meta(
|
|
||||||
'duration', video_code, 'duration')
|
|
||||||
if duration_str is None:
|
|
||||||
duration = None
|
|
||||||
else:
|
|
||||||
duration_mobj = re.match(
|
|
||||||
r'^T(?:(?P<hours>[0-9]+)H)?(?P<minutes>[0-9]+)M(?P<seconds>[0-9]+)S$',
|
|
||||||
duration_str)
|
|
||||||
duration = (
|
|
||||||
int_or_none(duration_mobj.group('hours'), default=0) * 3600 +
|
|
||||||
int(duration_mobj.group('minutes')) * 60 +
|
|
||||||
int(duration_mobj.group('seconds')))
|
|
||||||
upload_date = unified_strdate(self._html_search_meta(
|
|
||||||
'uploadDate', video_code, fatal=False))
|
|
||||||
thumbnail = self._html_search_meta(
|
|
||||||
'thumbnailUrl', video_code, 'thumbnail', fatal=False)
|
|
||||||
|
|
||||||
cid = self._search_regex(r'cid=(\d+)', webpage, 'cid')
|
doc = self._download_xml(
|
||||||
|
'http://interface.bilibili.com/v_cdn_play?appkey=8e9fc618fbd41e28&cid=%s' % cid,
|
||||||
lq_doc = self._download_xml(
|
cid,
|
||||||
'http://interface.bilibili.com/v_cdn_play?appkey=1&cid=%s' % cid,
|
'Downloading page %s/%s' % (page_num, view_data['pages'])
|
||||||
video_id,
|
|
||||||
note='Downloading LQ video info'
|
|
||||||
)
|
)
|
||||||
lq_durl = lq_doc.find('./durl')
|
|
||||||
formats = [{
|
|
||||||
'format_id': 'lq',
|
|
||||||
'quality': 1,
|
|
||||||
'url': lq_durl.find('./url').text,
|
|
||||||
'filesize': int_or_none(
|
|
||||||
lq_durl.find('./size'), get_attr='text'),
|
|
||||||
}]
|
|
||||||
|
|
||||||
hq_doc = self._download_xml(
|
if xpath_text(doc, './result') == 'error':
|
||||||
'http://interface.bilibili.com/playurl?appkey=1&cid=%s' % cid,
|
raise ExtractorError('%s said: %s' % (self.IE_NAME, xpath_text(doc, './message')), expected=True)
|
||||||
video_id,
|
|
||||||
note='Downloading HQ video info',
|
entries = []
|
||||||
fatal=False,
|
|
||||||
)
|
for durl in doc.findall('./durl'):
|
||||||
if hq_doc is not False:
|
size = xpath_text(durl, ['./filesize', './size'])
|
||||||
hq_durl = hq_doc.find('./durl')
|
formats = [{
|
||||||
formats.append({
|
'url': durl.find('./url').text,
|
||||||
'format_id': 'hq',
|
'filesize': int_or_none(size),
|
||||||
'quality': 2,
|
|
||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
'url': hq_durl.find('./url').text,
|
}]
|
||||||
'filesize': int_or_none(
|
backup_urls = durl.find('./backup_url')
|
||||||
hq_durl.find('./size'), get_attr='text'),
|
if backup_urls is not None:
|
||||||
|
for backup_url in backup_urls.findall('./url'):
|
||||||
|
formats.append({'url': backup_url.text})
|
||||||
|
formats.reverse()
|
||||||
|
|
||||||
|
entries.append({
|
||||||
|
'id': '%s_part%s' % (cid, xpath_text(durl, './order')),
|
||||||
|
'title': title,
|
||||||
|
'duration': int_or_none(xpath_text(durl, './length'), 1000),
|
||||||
|
'formats': formats,
|
||||||
})
|
})
|
||||||
|
|
||||||
self._sort_formats(formats)
|
info = {
|
||||||
return {
|
'id': compat_str(cid),
|
||||||
'id': video_id,
|
|
||||||
'title': title,
|
'title': title,
|
||||||
'formats': formats,
|
'description': view_data.get('description'),
|
||||||
'duration': duration,
|
'thumbnail': view_data.get('pic'),
|
||||||
'upload_date': upload_date,
|
'uploader': view_data.get('author'),
|
||||||
'thumbnail': thumbnail,
|
'timestamp': int_or_none(view_data.get('created')),
|
||||||
|
'view_count': int_or_none(view_data.get('play')),
|
||||||
|
'duration': int_or_none(xpath_text(doc, './timelength')),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(entries) == 1:
|
||||||
|
entries[0].update(info)
|
||||||
|
return entries[0]
|
||||||
|
else:
|
||||||
|
info.update({
|
||||||
|
'_type': 'multi_video',
|
||||||
|
'id': video_id,
|
||||||
|
'entries': entries,
|
||||||
|
})
|
||||||
|
return info
|
||||||
|
|||||||
106
youtube_dl/extractor/bleacherreport.py
Normal file
106
youtube_dl/extractor/bleacherreport.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from .amp import AMPIE
|
||||||
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
int_or_none,
|
||||||
|
parse_iso8601,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BleacherReportIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?bleacherreport\.com/articles/(?P<id>\d+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://bleacherreport.com/articles/2496438-fsu-stat-projections-is-jalen-ramsey-best-defensive-player-in-college-football',
|
||||||
|
'md5': 'a3ffc3dc73afdbc2010f02d98f990f20',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '2496438',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'FSU Stat Projections: Is Jalen Ramsey Best Defensive Player in College Football?',
|
||||||
|
'uploader_id': 3992341,
|
||||||
|
'description': 'CFB, ACC, Florida State',
|
||||||
|
'timestamp': 1434380212,
|
||||||
|
'upload_date': '20150615',
|
||||||
|
'uploader': 'Team Stream Now ',
|
||||||
|
},
|
||||||
|
'add_ie': ['Ooyala'],
|
||||||
|
}, {
|
||||||
|
'url': 'http://bleacherreport.com/articles/2586817-aussie-golfers-get-fright-of-their-lives-after-being-chased-by-angry-kangaroo',
|
||||||
|
'md5': 'af5f90dc9c7ba1c19d0a3eac806bbf50',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '2586817',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Aussie Golfers Get Fright of Their Lives After Being Chased by Angry Kangaroo',
|
||||||
|
'timestamp': 1446839961,
|
||||||
|
'uploader': 'Sean Fay',
|
||||||
|
'description': 'md5:825e94e0f3521df52fa83b2ed198fa20',
|
||||||
|
'uploader_id': 6466954,
|
||||||
|
'upload_date': '20151011',
|
||||||
|
},
|
||||||
|
'add_ie': ['Youtube'],
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
article_id = self._match_id(url)
|
||||||
|
|
||||||
|
article_data = self._download_json('http://api.bleacherreport.com/api/v1/articles/%s' % article_id, article_id)['article']
|
||||||
|
|
||||||
|
thumbnails = []
|
||||||
|
primary_photo = article_data.get('primaryPhoto')
|
||||||
|
if primary_photo:
|
||||||
|
thumbnails = [{
|
||||||
|
'url': primary_photo['url'],
|
||||||
|
'width': primary_photo.get('width'),
|
||||||
|
'height': primary_photo.get('height'),
|
||||||
|
}]
|
||||||
|
|
||||||
|
info = {
|
||||||
|
'_type': 'url_transparent',
|
||||||
|
'id': article_id,
|
||||||
|
'title': article_data['title'],
|
||||||
|
'uploader': article_data.get('author', {}).get('name'),
|
||||||
|
'uploader_id': article_data.get('authorId'),
|
||||||
|
'timestamp': parse_iso8601(article_data.get('createdAt')),
|
||||||
|
'thumbnails': thumbnails,
|
||||||
|
'comment_count': int_or_none(article_data.get('commentsCount')),
|
||||||
|
'view_count': int_or_none(article_data.get('hitCount')),
|
||||||
|
}
|
||||||
|
|
||||||
|
video = article_data.get('video')
|
||||||
|
if video:
|
||||||
|
video_type = video['type']
|
||||||
|
if video_type == 'cms.bleacherreport.com':
|
||||||
|
info['url'] = 'http://bleacherreport.com/video_embed?id=%s' % video['id']
|
||||||
|
elif video_type == 'ooyala.com':
|
||||||
|
info['url'] = 'ooyala:%s' % video['id']
|
||||||
|
elif video_type == 'youtube.com':
|
||||||
|
info['url'] = video['id']
|
||||||
|
elif video_type == 'vine.co':
|
||||||
|
info['url'] = 'https://vine.co/v/%s' % video['id']
|
||||||
|
else:
|
||||||
|
info['url'] = video_type + video['id']
|
||||||
|
return info
|
||||||
|
else:
|
||||||
|
raise ExtractorError('no video in the article', expected=True)
|
||||||
|
|
||||||
|
|
||||||
|
class BleacherReportCMSIE(AMPIE):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?bleacherreport\.com/video_embed\?id=(?P<id>[0-9a-f-]{36})'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://bleacherreport.com/video_embed?id=8fd44c2f-3dc5-4821-9118-2c825a98c0e1',
|
||||||
|
'md5': '8c2c12e3af7805152675446c905d159b',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '8fd44c2f-3dc5-4821-9118-2c825a98c0e1',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': 'Cena vs. Rollins Would Expose the Heavyweight Division',
|
||||||
|
'description': 'md5:984afb4ade2f9c0db35f3267ed88b36e',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
info = self._extract_feed_info('http://cms.bleacherreport.com/media/items/%s/akamai.json' % video_id)
|
||||||
|
info['id'] = video_id
|
||||||
|
return info
|
||||||
@@ -1,40 +1,35 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import re
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import remove_start
|
from ..utils import (
|
||||||
|
remove_start,
|
||||||
|
int_or_none,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BlinkxIE(InfoExtractor):
|
class BlinkxIE(InfoExtractor):
|
||||||
_VALID_URL = r'^(?:https?://(?:www\.)blinkx\.com/#?ce/|blinkx:)(?P<id>[^?]+)'
|
_VALID_URL = r'(?:https?://(?:www\.)blinkx\.com/#?ce/|blinkx:)(?P<id>[^?]+)'
|
||||||
IE_NAME = 'blinkx'
|
IE_NAME = 'blinkx'
|
||||||
|
|
||||||
_TEST = {
|
_TEST = {
|
||||||
'url': 'http://www.blinkx.com/ce/8aQUy7GVFYgFzpKhT0oqsilwOGFRVXk3R1ZGWWdGenBLaFQwb3FzaWx3OGFRVXk3R1ZGWWdGenB',
|
'url': 'http://www.blinkx.com/ce/Da0Gw3xc5ucpNduzLuDDlv4WC9PuI4fDi1-t6Y3LyfdY2SZS5Urbvn-UPJvrvbo8LTKTc67Wu2rPKSQDJyZeeORCR8bYkhs8lI7eqddznH2ofh5WEEdjYXnoRtj7ByQwt7atMErmXIeYKPsSDuMAAqJDlQZ-3Ff4HJVeH_s3Gh8oQ',
|
||||||
'md5': '2e9a07364af40163a908edbf10bb2492',
|
'md5': '337cf7a344663ec79bf93a526a2e06c7',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '8aQUy7GV',
|
'id': 'Da0Gw3xc',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Police Car Rolls Away',
|
'title': 'No Daily Show for John Oliver; HBO Show Renewed - IGN News',
|
||||||
'uploader': 'stupidvideos.com',
|
'uploader': 'IGN News',
|
||||||
'upload_date': '20131215',
|
'upload_date': '20150217',
|
||||||
'timestamp': 1387068000,
|
'timestamp': 1424215740,
|
||||||
'description': 'A police car gently rolls away from a fight. Maybe it felt weird being around a confrontation and just had to get out of there!',
|
'description': 'HBO has renewed Last Week Tonight With John Oliver for two more seasons.',
|
||||||
'duration': 14.886,
|
'duration': 47.743333,
|
||||||
'thumbnails': [{
|
|
||||||
'width': 100,
|
|
||||||
'height': 76,
|
|
||||||
'resolution': '100x76',
|
|
||||||
'url': 'http://cdn.blinkx.com/stream/b/41/StupidVideos/20131215/1873969261/1873969261_tn_0.jpg',
|
|
||||||
}],
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def _real_extract(self, rl):
|
def _real_extract(self, url):
|
||||||
m = re.match(self._VALID_URL, rl)
|
video_id = self._match_id(url)
|
||||||
video_id = m.group('id')
|
|
||||||
display_id = video_id[:8]
|
display_id = video_id[:8]
|
||||||
|
|
||||||
api_url = ('https://apib4.blinkx.com/api.php?action=play_video&' +
|
api_url = ('https://apib4.blinkx.com/api.php?action=play_video&' +
|
||||||
@@ -60,18 +55,20 @@ class BlinkxIE(InfoExtractor):
|
|||||||
elif m['type'] in ('flv', 'mp4'):
|
elif m['type'] in ('flv', 'mp4'):
|
||||||
vcodec = remove_start(m['vcodec'], 'ff')
|
vcodec = remove_start(m['vcodec'], 'ff')
|
||||||
acodec = remove_start(m['acodec'], 'ff')
|
acodec = remove_start(m['acodec'], 'ff')
|
||||||
tbr = (int(m['vbr']) + int(m['abr'])) // 1000
|
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'])
|
format_id = '%s-%sk-%s' % (vcodec, tbr, m['w'])
|
||||||
formats.append({
|
formats.append({
|
||||||
'format_id': format_id,
|
'format_id': format_id,
|
||||||
'url': m['link'],
|
'url': m['link'],
|
||||||
'vcodec': vcodec,
|
'vcodec': vcodec,
|
||||||
'acodec': acodec,
|
'acodec': acodec,
|
||||||
'abr': int(m['abr']) // 1000,
|
'abr': abr,
|
||||||
'vbr': int(m['vbr']) // 1000,
|
'vbr': vbr,
|
||||||
'tbr': tbr,
|
'tbr': tbr,
|
||||||
'width': int(m['w']),
|
'width': int_or_none(m.get('w')),
|
||||||
'height': int(m['h']),
|
'height': int_or_none(m.get('h')),
|
||||||
})
|
})
|
||||||
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|||||||
@@ -1,266 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
|
||||||
from .subtitles import SubtitlesInfoExtractor
|
|
||||||
|
|
||||||
from ..compat import (
|
|
||||||
compat_str,
|
|
||||||
compat_urllib_request,
|
|
||||||
compat_urlparse,
|
|
||||||
)
|
|
||||||
from ..utils import (
|
|
||||||
clean_html,
|
|
||||||
int_or_none,
|
|
||||||
parse_iso8601,
|
|
||||||
unescapeHTML,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BlipTVIE(SubtitlesInfoExtractor):
|
|
||||||
_VALID_URL = r'https?://(?:\w+\.)?blip\.tv/(?:(?:.+-|rss/flash/)(?P<id>\d+)|((?:play/|api\.swf#)(?P<lookup_id>[\da-zA-Z+_]+)))'
|
|
||||||
|
|
||||||
_TESTS = [
|
|
||||||
{
|
|
||||||
'url': 'http://blip.tv/cbr/cbr-exclusive-gotham-city-imposters-bats-vs-jokerz-short-3-5796352',
|
|
||||||
'md5': 'c6934ad0b6acf2bd920720ec888eb812',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '5779306',
|
|
||||||
'ext': 'mov',
|
|
||||||
'title': 'CBR EXCLUSIVE: "Gotham City Imposters" Bats VS Jokerz Short 3',
|
|
||||||
'description': 'md5:9bc31f227219cde65e47eeec8d2dc596',
|
|
||||||
'timestamp': 1323138843,
|
|
||||||
'upload_date': '20111206',
|
|
||||||
'uploader': 'cbr',
|
|
||||||
'uploader_id': '679425',
|
|
||||||
'duration': 81,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
# https://github.com/rg3/youtube-dl/pull/2274
|
|
||||||
'note': 'Video with subtitles',
|
|
||||||
'url': 'http://blip.tv/play/h6Uag5OEVgI.html',
|
|
||||||
'md5': '309f9d25b820b086ca163ffac8031806',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '6586561',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Red vs. Blue Season 11 Episode 1',
|
|
||||||
'description': 'One-Zero-One',
|
|
||||||
'timestamp': 1371261608,
|
|
||||||
'upload_date': '20130615',
|
|
||||||
'uploader': 'redvsblue',
|
|
||||||
'uploader_id': '792887',
|
|
||||||
'duration': 279,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
# https://bugzilla.redhat.com/show_bug.cgi?id=967465
|
|
||||||
'url': 'http://a.blip.tv/api.swf#h6Uag5KbVwI',
|
|
||||||
'md5': '314e87b1ebe7a48fcbfdd51b791ce5a6',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '6573122',
|
|
||||||
'ext': 'mov',
|
|
||||||
'upload_date': '20130520',
|
|
||||||
'description': 'Two hapless space marines argue over what to do when they realize they have an astronomically huge problem on their hands.',
|
|
||||||
'title': 'Red vs. Blue Season 11 Trailer',
|
|
||||||
'timestamp': 1369029609,
|
|
||||||
'uploader': 'redvsblue',
|
|
||||||
'uploader_id': '792887',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'url': 'http://blip.tv/play/gbk766dkj4Yn',
|
|
||||||
'md5': 'fe0a33f022d49399a241e84a8ea8b8e3',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '1749452',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'upload_date': '20090208',
|
|
||||||
'description': 'Witness the first appearance of the Nostalgia Critic character, as Doug reviews the movie Transformers.',
|
|
||||||
'title': 'Nostalgia Critic: Transformers',
|
|
||||||
'timestamp': 1234068723,
|
|
||||||
'uploader': 'NostalgiaCritic',
|
|
||||||
'uploader_id': '246467',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
# https://github.com/rg3/youtube-dl/pull/4404
|
|
||||||
'note': 'Audio only',
|
|
||||||
'url': 'http://blip.tv/hilarios-productions/weekly-manga-recap-kingdom-7119982',
|
|
||||||
'md5': '76c0a56f24e769ceaab21fbb6416a351',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '7103299',
|
|
||||||
'ext': 'flv',
|
|
||||||
'title': 'Weekly Manga Recap: Kingdom',
|
|
||||||
'description': 'And then Shin breaks the enemy line, and he's all like HWAH! And then he slices a guy and it's all like FWASHING! And... it's really hard to describe the best parts of this series without breaking down into sound effects, okay?',
|
|
||||||
'timestamp': 1417660321,
|
|
||||||
'upload_date': '20141204',
|
|
||||||
'uploader': 'The Rollo T',
|
|
||||||
'uploader_id': '407429',
|
|
||||||
'duration': 7251,
|
|
||||||
'vcodec': 'none',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
mobj = re.match(self._VALID_URL, url)
|
|
||||||
lookup_id = mobj.group('lookup_id')
|
|
||||||
|
|
||||||
# See https://github.com/rg3/youtube-dl/issues/857 and
|
|
||||||
# https://github.com/rg3/youtube-dl/issues/4197
|
|
||||||
if lookup_id:
|
|
||||||
urlh = self._request_webpage(
|
|
||||||
'http://blip.tv/play/%s' % lookup_id, lookup_id, 'Resolving lookup id')
|
|
||||||
url = compat_urlparse.urlparse(urlh.geturl())
|
|
||||||
qs = compat_urlparse.parse_qs(url.query)
|
|
||||||
mobj = re.match(self._VALID_URL, qs['file'][0])
|
|
||||||
|
|
||||||
video_id = mobj.group('id')
|
|
||||||
|
|
||||||
rss = self._download_xml('http://blip.tv/rss/flash/%s' % video_id, video_id, 'Downloading video RSS')
|
|
||||||
|
|
||||||
def blip(s):
|
|
||||||
return '{http://blip.tv/dtd/blip/1.0}%s' % s
|
|
||||||
|
|
||||||
def media(s):
|
|
||||||
return '{http://search.yahoo.com/mrss/}%s' % s
|
|
||||||
|
|
||||||
def itunes(s):
|
|
||||||
return '{http://www.itunes.com/dtds/podcast-1.0.dtd}%s' % s
|
|
||||||
|
|
||||||
item = rss.find('channel/item')
|
|
||||||
|
|
||||||
video_id = item.find(blip('item_id')).text
|
|
||||||
title = item.find('./title').text
|
|
||||||
description = clean_html(compat_str(item.find(blip('puredescription')).text))
|
|
||||||
timestamp = parse_iso8601(item.find(blip('datestamp')).text)
|
|
||||||
uploader = item.find(blip('user')).text
|
|
||||||
uploader_id = item.find(blip('userid')).text
|
|
||||||
duration = int(item.find(blip('runtime')).text)
|
|
||||||
media_thumbnail = item.find(media('thumbnail'))
|
|
||||||
thumbnail = media_thumbnail.get('url') if media_thumbnail is not None else item.find(itunes('image')).text
|
|
||||||
categories = [category.text for category in item.findall('category')]
|
|
||||||
|
|
||||||
formats = []
|
|
||||||
subtitles = {}
|
|
||||||
|
|
||||||
media_group = item.find(media('group'))
|
|
||||||
for media_content in media_group.findall(media('content')):
|
|
||||||
url = media_content.get('url')
|
|
||||||
role = media_content.get(blip('role'))
|
|
||||||
msg = self._download_webpage(
|
|
||||||
url + '?showplayer=20140425131715&referrer=http://blip.tv&mask=7&skin=flashvars&view=url',
|
|
||||||
video_id, 'Resolving URL for %s' % role)
|
|
||||||
real_url = compat_urlparse.parse_qs(msg.strip())['message'][0]
|
|
||||||
|
|
||||||
media_type = media_content.get('type')
|
|
||||||
if media_type == 'text/srt' or url.endswith('.srt'):
|
|
||||||
LANGS = {
|
|
||||||
'english': 'en',
|
|
||||||
}
|
|
||||||
lang = role.rpartition('-')[-1].strip().lower()
|
|
||||||
langcode = LANGS.get(lang, lang)
|
|
||||||
subtitles[langcode] = url
|
|
||||||
elif media_type.startswith('video/'):
|
|
||||||
formats.append({
|
|
||||||
'url': real_url,
|
|
||||||
'format_id': role,
|
|
||||||
'format_note': media_type,
|
|
||||||
'vcodec': media_content.get(blip('vcodec')) or 'none',
|
|
||||||
'acodec': media_content.get(blip('acodec')),
|
|
||||||
'filesize': media_content.get('filesize'),
|
|
||||||
'width': int_or_none(media_content.get('width')),
|
|
||||||
'height': int_or_none(media_content.get('height')),
|
|
||||||
})
|
|
||||||
self._sort_formats(formats)
|
|
||||||
|
|
||||||
# subtitles
|
|
||||||
video_subtitles = self.extract_subtitles(video_id, subtitles)
|
|
||||||
if self._downloader.params.get('listsubtitles', False):
|
|
||||||
self._list_available_subtitles(video_id, subtitles)
|
|
||||||
return
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': video_id,
|
|
||||||
'title': title,
|
|
||||||
'description': description,
|
|
||||||
'timestamp': timestamp,
|
|
||||||
'uploader': uploader,
|
|
||||||
'uploader_id': uploader_id,
|
|
||||||
'duration': duration,
|
|
||||||
'thumbnail': thumbnail,
|
|
||||||
'categories': categories,
|
|
||||||
'formats': formats,
|
|
||||||
'subtitles': video_subtitles,
|
|
||||||
}
|
|
||||||
|
|
||||||
def _download_subtitle_url(self, sub_lang, url):
|
|
||||||
# For some weird reason, blip.tv serves a video instead of subtitles
|
|
||||||
# when we request with a common UA
|
|
||||||
req = compat_urllib_request.Request(url)
|
|
||||||
req.add_header('User-Agent', 'youtube-dl')
|
|
||||||
return self._download_webpage(req, None, note=False)
|
|
||||||
|
|
||||||
|
|
||||||
class BlipTVUserIE(InfoExtractor):
|
|
||||||
_VALID_URL = r'(?:(?:https?://(?:\w+\.)?blip\.tv/)|bliptvuser:)(?!api\.swf)([^/]+)/*$'
|
|
||||||
_PAGE_SIZE = 12
|
|
||||||
IE_NAME = 'blip.tv:user'
|
|
||||||
_TEST = {
|
|
||||||
'url': 'http://blip.tv/actone',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'actone',
|
|
||||||
'title': 'Act One: The Series',
|
|
||||||
},
|
|
||||||
'playlist_count': 5,
|
|
||||||
}
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
mobj = re.match(self._VALID_URL, url)
|
|
||||||
username = mobj.group(1)
|
|
||||||
|
|
||||||
page_base = 'http://m.blip.tv/pr/show_get_full_episode_list?users_id=%s&lite=0&esi=1'
|
|
||||||
|
|
||||||
page = self._download_webpage(url, username, 'Downloading user page')
|
|
||||||
mobj = re.search(r'data-users-id="([^"]+)"', page)
|
|
||||||
page_base = page_base % mobj.group(1)
|
|
||||||
title = self._og_search_title(page)
|
|
||||||
|
|
||||||
# Download video ids using BlipTV Ajax calls. Result size per
|
|
||||||
# query is limited (currently to 12 videos) so we need to query
|
|
||||||
# page by page until there are no video ids - it means we got
|
|
||||||
# all of them.
|
|
||||||
|
|
||||||
video_ids = []
|
|
||||||
pagenum = 1
|
|
||||||
|
|
||||||
while True:
|
|
||||||
url = page_base + "&page=" + str(pagenum)
|
|
||||||
page = self._download_webpage(
|
|
||||||
url, username, 'Downloading video ids from page %d' % pagenum)
|
|
||||||
|
|
||||||
# Extract video identifiers
|
|
||||||
ids_in_page = []
|
|
||||||
|
|
||||||
for mobj in re.finditer(r'href="/([^"]+)"', page):
|
|
||||||
if mobj.group(1) not in ids_in_page:
|
|
||||||
ids_in_page.append(unescapeHTML(mobj.group(1)))
|
|
||||||
|
|
||||||
video_ids.extend(ids_in_page)
|
|
||||||
|
|
||||||
# A little optimization - if current page is not
|
|
||||||
# "full", ie. does not contain PAGE_SIZE video ids then
|
|
||||||
# we can assume that this page is the last one - there
|
|
||||||
# are no more ids on further pages - no need to query
|
|
||||||
# again.
|
|
||||||
|
|
||||||
if len(ids_in_page) < self._PAGE_SIZE:
|
|
||||||
break
|
|
||||||
|
|
||||||
pagenum += 1
|
|
||||||
|
|
||||||
urls = ['http://blip.tv/%s' % video_id for video_id in video_ids]
|
|
||||||
url_entries = [self.url_result(vurl, 'BlipTV') for vurl in urls]
|
|
||||||
return self.playlist_result(
|
|
||||||
url_entries, playlist_title=title, playlist_id=username)
|
|
||||||
@@ -6,32 +6,52 @@ from .common import InfoExtractor
|
|||||||
|
|
||||||
|
|
||||||
class BloombergIE(InfoExtractor):
|
class BloombergIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://www\.bloomberg\.com/video/(?P<name>.+?)\.html'
|
_VALID_URL = r'https?://(?:www\.)?bloomberg\.com/(?:[^/]+/)*(?P<id>[^/?#]+)'
|
||||||
|
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'http://www.bloomberg.com/video/shah-s-presentation-on-foreign-exchange-strategies-qurhIVlJSB6hzkVi229d8g.html',
|
'url': 'http://www.bloomberg.com/news/videos/b/aaeae121-5949-481e-a1ce-4562db6f5df2',
|
||||||
# The md5 checksum changes
|
# The md5 checksum changes
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'qurhIVlJSB6hzkVi229d8g',
|
'id': 'qurhIVlJSB6hzkVi229d8g',
|
||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
'title': 'Shah\'s Presentation on Foreign-Exchange Strategies',
|
'title': 'Shah\'s Presentation on Foreign-Exchange Strategies',
|
||||||
'description': 'md5:0681e0d30dcdfc6abf34594961d8ea88',
|
'description': 'md5:a8ba0302912d03d246979735c17d2761',
|
||||||
},
|
},
|
||||||
}
|
}, {
|
||||||
|
'url': 'http://www.bloomberg.com/news/articles/2015-11-12/five-strange-things-that-have-been-happening-in-financial-markets',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.bloomberg.com/politics/videos/2015-11-25/karl-rove-on-jeb-bush-s-struggles-stopping-trump',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
name = self._match_id(url)
|
||||||
name = mobj.group('name')
|
|
||||||
webpage = self._download_webpage(url, name)
|
webpage = self._download_webpage(url, name)
|
||||||
f4m_url = self._search_regex(
|
video_id = self._search_regex(
|
||||||
r'<source src="(https?://[^"]+\.f4m.*?)"', webpage,
|
r'["\']bmmrId["\']\s*:\s*(["\'])(?P<url>.+?)\1',
|
||||||
'f4m url')
|
webpage, 'id', group='url')
|
||||||
title = re.sub(': Video$', '', self._og_search_title(webpage))
|
title = re.sub(': Video$', '', self._og_search_title(webpage))
|
||||||
|
|
||||||
|
embed_info = self._download_json(
|
||||||
|
'http://www.bloomberg.com/api/embed?id=%s' % video_id, video_id)
|
||||||
|
formats = []
|
||||||
|
for stream in embed_info['streams']:
|
||||||
|
stream_url = stream.get('url')
|
||||||
|
if not stream_url:
|
||||||
|
continue
|
||||||
|
if stream['muxing_format'] == 'TS':
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
stream_url, video_id, 'mp4', m3u8_id='hls', fatal=False))
|
||||||
|
else:
|
||||||
|
formats.extend(self._extract_f4m_formats(
|
||||||
|
stream_url, video_id, f4m_id='hds', fatal=False))
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': name.split('-')[-1],
|
'id': video_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'formats': self._extract_f4m_formats(f4m_url, name),
|
'formats': formats,
|
||||||
'description': self._og_search_description(webpage),
|
'description': self._og_search_description(webpage),
|
||||||
'thumbnail': self._og_search_thumbnail(webpage),
|
'thumbnail': self._og_search_thumbnail(webpage),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
js_to_json,
|
||||||
|
determine_ext,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BpbIE(InfoExtractor):
|
class BpbIE(InfoExtractor):
|
||||||
@@ -10,7 +16,8 @@ class BpbIE(InfoExtractor):
|
|||||||
|
|
||||||
_TEST = {
|
_TEST = {
|
||||||
'url': 'http://www.bpb.de/mediathek/297/joachim-gauck-zu-1989-und-die-erinnerung-an-die-ddr',
|
'url': 'http://www.bpb.de/mediathek/297/joachim-gauck-zu-1989-und-die-erinnerung-an-die-ddr',
|
||||||
'md5': '0792086e8e2bfbac9cdf27835d5f2093',
|
# md5 fails in Python 2.6 due to buggy server response and wrong handling of urllib2
|
||||||
|
'md5': 'c4f84c8a8044ca9ff68bb8441d300b3f',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '297',
|
'id': '297',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
@@ -25,13 +32,26 @@ class BpbIE(InfoExtractor):
|
|||||||
|
|
||||||
title = self._html_search_regex(
|
title = self._html_search_regex(
|
||||||
r'<h2 class="white">(.*?)</h2>', webpage, 'title')
|
r'<h2 class="white">(.*?)</h2>', webpage, 'title')
|
||||||
video_url = self._html_search_regex(
|
video_info_dicts = re.findall(
|
||||||
r'(http://film\.bpb\.de/player/dokument_[0-9]+\.mp4)',
|
r"({\s*src:\s*'http://film\.bpb\.de/[^}]+})", webpage)
|
||||||
webpage, 'video URL')
|
|
||||||
|
formats = []
|
||||||
|
for video_info in video_info_dicts:
|
||||||
|
video_info = self._parse_json(video_info, video_id, transform_source=js_to_json)
|
||||||
|
quality = video_info['quality']
|
||||||
|
video_url = video_info['src']
|
||||||
|
formats.append({
|
||||||
|
'url': video_url,
|
||||||
|
'preference': 10 if quality == 'high' else 0,
|
||||||
|
'format_note': quality,
|
||||||
|
'format_id': '%s-%s' % (quality, determine_ext(video_url)),
|
||||||
|
})
|
||||||
|
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'url': video_url,
|
'formats': formats,
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': self._og_search_description(webpage),
|
'description': self._og_search_description(webpage),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,56 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_duration,
|
parse_duration,
|
||||||
|
xpath_element,
|
||||||
|
xpath_text,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BRIE(InfoExtractor):
|
class BRIE(InfoExtractor):
|
||||||
IE_DESC = 'Bayerischer Rundfunk Mediathek'
|
IE_DESC = 'Bayerischer Rundfunk Mediathek'
|
||||||
_VALID_URL = r'https?://(?:www\.)?br\.de/(?:[a-z0-9\-_]+/)+(?P<id>[a-z0-9\-_]+)\.html'
|
_VALID_URL = r'(?P<base_url>https?://(?:www\.)?br(?:-klassik)?\.de)/(?:[a-z0-9\-_]+/)+(?P<id>[a-z0-9\-_]+)\.html'
|
||||||
_BASE_URL = 'http://www.br.de'
|
|
||||||
|
|
||||||
_TESTS = [
|
_TESTS = [
|
||||||
{
|
{
|
||||||
'url': 'http://www.br.de/mediathek/video/sendungen/heimatsound/heimatsound-festival-2014-trailer-100.html',
|
'url': 'http://www.br.de/mediathek/video/sendungen/abendschau/betriebliche-altersvorsorge-104.html',
|
||||||
'md5': '93556dd2bcb2948d9259f8670c516d59',
|
'md5': '83a0477cf0b8451027eb566d88b51106',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '25e279aa-1ffd-40fd-9955-5325bd48a53a',
|
'id': '48f656ef-287e-486f-be86-459122db22cc',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Wenn das Traditions-Theater wackelt',
|
'title': 'Die böse Überraschung',
|
||||||
'description': 'Heimatsound-Festival 2014: Wenn das Traditions-Theater wackelt',
|
'description': 'md5:ce9ac81b466ce775b8018f6801b48ac9',
|
||||||
'duration': 34,
|
'duration': 180,
|
||||||
'uploader': 'BR',
|
'uploader': 'Reinhard Weber',
|
||||||
'upload_date': '20140802',
|
'upload_date': '20150422',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'http://www.br.de/nachrichten/schaeuble-haushaltsentwurf-bundestag-100.html',
|
'url': 'http://www.br.de/nachrichten/oberbayern/inhalt/muenchner-polizeipraesident-schreiber-gestorben-100.html',
|
||||||
'md5': '3db0df1a9a9cd9fa0c70e6ea8aa8e820',
|
'md5': 'af3a3a4aa43ff0ce6a89504c67f427ef',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'c6aae3de-2cf9-43f2-957f-f17fef9afaab',
|
'id': 'a4b83e34-123d-4b81-9f4e-c0d3121a4e05',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': 'Manfred Schreiber ist tot',
|
||||||
|
'description': 'md5:b454d867f2a9fc524ebe88c3f5092d97',
|
||||||
|
'duration': 26,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'https://www.br-klassik.de/audio/peeping-tom-premierenkritik-dance-festival-muenchen-100.html',
|
||||||
|
'md5': '8b5b27c0b090f3b35eac4ab3f7a73d3d',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '74c603c9-26d3-48bb-b85b-079aeed66e0b',
|
||||||
'ext': 'aac',
|
'ext': 'aac',
|
||||||
'title': '"Keine neuen Schulden im nächsten Jahr"',
|
'title': 'Kurzweilig und sehr bewegend',
|
||||||
'description': 'Haushaltsentwurf: "Keine neuen Schulden im nächsten Jahr"',
|
'description': 'md5:0351996e3283d64adeb38ede91fac54e',
|
||||||
'duration': 64,
|
'duration': 296,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -46,7 +60,7 @@ class BRIE(InfoExtractor):
|
|||||||
'id': '6ba73750-d405-45d3-861d-1ce8c524e059',
|
'id': '6ba73750-d405-45d3-861d-1ce8c524e059',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Umweltbewusster Häuslebauer',
|
'title': 'Umweltbewusster Häuslebauer',
|
||||||
'description': 'Uwe Erdelt: Umweltbewusster Häuslebauer',
|
'description': 'md5:d52dae9792d00226348c1dbb13c9bae2',
|
||||||
'duration': 116,
|
'duration': 116,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -57,7 +71,7 @@ class BRIE(InfoExtractor):
|
|||||||
'id': 'd982c9ce-8648-4753-b358-98abb8aec43d',
|
'id': 'd982c9ce-8648-4753-b358-98abb8aec43d',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Folge 1 - Metaphysik',
|
'title': 'Folge 1 - Metaphysik',
|
||||||
'description': 'Kant für Anfänger: Folge 1 - Metaphysik',
|
'description': 'md5:bb659990e9e59905c3d41e369db1fbe3',
|
||||||
'duration': 893,
|
'duration': 893,
|
||||||
'uploader': 'Eva Maria Steimle',
|
'uploader': 'Eva Maria Steimle',
|
||||||
'upload_date': '20140117',
|
'upload_date': '20140117',
|
||||||
@@ -66,28 +80,31 @@ class BRIE(InfoExtractor):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id = self._match_id(url)
|
base_url, display_id = re.search(self._VALID_URL, url).groups()
|
||||||
page = self._download_webpage(url, display_id)
|
page = self._download_webpage(url, display_id)
|
||||||
xml_url = self._search_regex(
|
xml_url = self._search_regex(
|
||||||
r"return BRavFramework\.register\(BRavFramework\('avPlayer_(?:[a-f0-9-]{36})'\)\.setup\({dataURL:'(/(?:[a-z0-9\-]+/)+[a-z0-9/~_.-]+)'}\)\);", page, 'XMLURL')
|
r"return BRavFramework\.register\(BRavFramework\('avPlayer_(?:[a-f0-9-]{36})'\)\.setup\({dataURL:'(/(?:[a-z0-9\-]+/)+[a-z0-9/~_.-]+)'}\)\);", page, 'XMLURL')
|
||||||
xml = self._download_xml(self._BASE_URL + xml_url, None)
|
xml = self._download_xml(base_url + xml_url, display_id)
|
||||||
|
|
||||||
medias = []
|
medias = []
|
||||||
|
|
||||||
for xml_media in xml.findall('video') + xml.findall('audio'):
|
for xml_media in xml.findall('video') + xml.findall('audio'):
|
||||||
|
media_id = xml_media.get('externalId')
|
||||||
media = {
|
media = {
|
||||||
'id': xml_media.get('externalId'),
|
'id': media_id,
|
||||||
'title': xml_media.find('title').text,
|
'title': xpath_text(xml_media, 'title', 'title', True),
|
||||||
'duration': parse_duration(xml_media.find('duration').text),
|
'duration': parse_duration(xpath_text(xml_media, 'duration')),
|
||||||
'formats': self._extract_formats(xml_media.find('assets')),
|
'formats': self._extract_formats(xpath_element(
|
||||||
'thumbnails': self._extract_thumbnails(xml_media.find('teaserImage/variants')),
|
xml_media, 'assets'), media_id),
|
||||||
'description': ' '.join(xml_media.find('shareTitle').text.splitlines()),
|
'thumbnails': self._extract_thumbnails(xpath_element(
|
||||||
'webpage_url': xml_media.find('permalink').text
|
xml_media, 'teaserImage/variants'), base_url),
|
||||||
|
'description': xpath_text(xml_media, 'desc'),
|
||||||
|
'webpage_url': xpath_text(xml_media, 'permalink'),
|
||||||
|
'uploader': xpath_text(xml_media, 'author'),
|
||||||
}
|
}
|
||||||
if xml_media.find('author').text:
|
broadcast_date = xpath_text(xml_media, 'broadcastDate')
|
||||||
media['uploader'] = xml_media.find('author').text
|
if broadcast_date:
|
||||||
if xml_media.find('broadcastDate').text:
|
media['upload_date'] = ''.join(reversed(broadcast_date.split('.')))
|
||||||
media['upload_date'] = ''.join(reversed(xml_media.find('broadcastDate').text.split('.')))
|
|
||||||
medias.append(media)
|
medias.append(media)
|
||||||
|
|
||||||
if len(medias) > 1:
|
if len(medias) > 1:
|
||||||
@@ -98,35 +115,54 @@ class BRIE(InfoExtractor):
|
|||||||
raise ExtractorError('No media entries found')
|
raise ExtractorError('No media entries found')
|
||||||
return medias[0]
|
return medias[0]
|
||||||
|
|
||||||
def _extract_formats(self, assets):
|
def _extract_formats(self, assets, media_id):
|
||||||
|
formats = []
|
||||||
def text_or_none(asset, tag):
|
for asset in assets.findall('asset'):
|
||||||
elem = asset.find(tag)
|
format_url = xpath_text(asset, ['downloadUrl', 'url'])
|
||||||
return None if elem is None else elem.text
|
asset_type = asset.get('type')
|
||||||
|
if asset_type == 'HDS':
|
||||||
formats = [{
|
formats.extend(self._extract_f4m_formats(
|
||||||
'url': text_or_none(asset, 'downloadUrl'),
|
format_url + '?hdcore=3.2.0', media_id, f4m_id='hds', fatal=False))
|
||||||
'ext': text_or_none(asset, 'mediaType'),
|
elif asset_type == 'HLS':
|
||||||
'format_id': asset.get('type'),
|
formats.extend(self._extract_m3u8_formats(
|
||||||
'width': int_or_none(text_or_none(asset, 'frameWidth')),
|
format_url, media_id, 'mp4', 'm3u8_native', m3u8_id='hds', fatal=False))
|
||||||
'height': int_or_none(text_or_none(asset, 'frameHeight')),
|
else:
|
||||||
'tbr': int_or_none(text_or_none(asset, 'bitrateVideo')),
|
format_info = {
|
||||||
'abr': int_or_none(text_or_none(asset, 'bitrateAudio')),
|
'ext': xpath_text(asset, 'mediaType'),
|
||||||
'vcodec': text_or_none(asset, 'codecVideo'),
|
'width': int_or_none(xpath_text(asset, 'frameWidth')),
|
||||||
'acodec': text_or_none(asset, 'codecAudio'),
|
'height': int_or_none(xpath_text(asset, 'frameHeight')),
|
||||||
'container': text_or_none(asset, 'mediaType'),
|
'tbr': int_or_none(xpath_text(asset, 'bitrateVideo')),
|
||||||
'filesize': int_or_none(text_or_none(asset, 'size')),
|
'abr': int_or_none(xpath_text(asset, 'bitrateAudio')),
|
||||||
} for asset in assets.findall('asset')
|
'vcodec': xpath_text(asset, 'codecVideo'),
|
||||||
if asset.find('downloadUrl') is not None]
|
'acodec': xpath_text(asset, 'codecAudio'),
|
||||||
|
'container': xpath_text(asset, 'mediaType'),
|
||||||
|
'filesize': int_or_none(xpath_text(asset, 'size')),
|
||||||
|
}
|
||||||
|
format_url = self._proto_relative_url(format_url)
|
||||||
|
if format_url:
|
||||||
|
http_format_info = format_info.copy()
|
||||||
|
http_format_info.update({
|
||||||
|
'url': format_url,
|
||||||
|
'format_id': 'http-%s' % asset_type,
|
||||||
|
})
|
||||||
|
formats.append(http_format_info)
|
||||||
|
server_prefix = xpath_text(asset, 'serverPrefix')
|
||||||
|
if server_prefix:
|
||||||
|
rtmp_format_info = format_info.copy()
|
||||||
|
rtmp_format_info.update({
|
||||||
|
'url': server_prefix,
|
||||||
|
'play_path': xpath_text(asset, 'fileName'),
|
||||||
|
'format_id': 'rtmp-%s' % asset_type,
|
||||||
|
})
|
||||||
|
formats.append(rtmp_format_info)
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
return formats
|
return formats
|
||||||
|
|
||||||
def _extract_thumbnails(self, variants):
|
def _extract_thumbnails(self, variants, base_url):
|
||||||
thumbnails = [{
|
thumbnails = [{
|
||||||
'url': self._BASE_URL + variant.find('url').text,
|
'url': base_url + xpath_text(variant, 'url'),
|
||||||
'width': int_or_none(variant.find('width').text),
|
'width': int_or_none(xpath_text(variant, 'width')),
|
||||||
'height': int_or_none(variant.find('height').text),
|
'height': int_or_none(xpath_text(variant, 'height')),
|
||||||
} for variant in variants.findall('variant')]
|
} for variant in variants.findall('variant') if xpath_text(variant, 'url')]
|
||||||
thumbnails.sort(key=lambda x: x['width'] * x['height'], reverse=True)
|
thumbnails.sort(key=lambda x: x['width'] * x['height'], reverse=True)
|
||||||
return thumbnails
|
return thumbnails
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class BreakIE(InfoExtractor):
|
|||||||
'id': '2468056',
|
'id': '2468056',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'When Girls Act Like D-Bags',
|
'title': 'When Girls Act Like D-Bags',
|
||||||
|
'age_limit': 13,
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.break.com/video/ugc/baby-flex-2773063',
|
'url': 'http://www.break.com/video/ugc/baby-flex-2773063',
|
||||||
@@ -41,7 +42,7 @@ class BreakIE(InfoExtractor):
|
|||||||
'tbr': media['bitRate'],
|
'tbr': media['bitRate'],
|
||||||
'width': media['width'],
|
'width': media['width'],
|
||||||
'height': media['height'],
|
'height': media['height'],
|
||||||
} for media in info['media']]
|
} for media in info['media'] if media.get('mediaPurpose') == 'play']
|
||||||
|
|
||||||
if not formats:
|
if not formats:
|
||||||
formats.append({
|
formats.append({
|
||||||
|
|||||||
@@ -3,28 +3,34 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
import xml.etree.ElementTree
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
|
compat_etree_fromstring,
|
||||||
compat_parse_qs,
|
compat_parse_qs,
|
||||||
compat_str,
|
compat_str,
|
||||||
compat_urllib_parse,
|
compat_urllib_parse,
|
||||||
compat_urllib_parse_urlparse,
|
compat_urllib_parse_urlparse,
|
||||||
compat_urllib_request,
|
|
||||||
compat_urlparse,
|
compat_urlparse,
|
||||||
|
compat_xml_parse_error,
|
||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
determine_ext,
|
determine_ext,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
find_xpath_attr,
|
find_xpath_attr,
|
||||||
fix_xml_ampersands,
|
fix_xml_ampersands,
|
||||||
|
float_or_none,
|
||||||
|
js_to_json,
|
||||||
|
int_or_none,
|
||||||
|
parse_iso8601,
|
||||||
|
sanitized_Request,
|
||||||
unescapeHTML,
|
unescapeHTML,
|
||||||
unsmuggle_url,
|
unsmuggle_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BrightcoveIE(InfoExtractor):
|
class BrightcoveLegacyIE(InfoExtractor):
|
||||||
|
IE_NAME = 'brightcove:legacy'
|
||||||
_VALID_URL = r'(?:https?://.*brightcove\.com/(services|viewer).*?\?|brightcove:)(?P<query>.*)'
|
_VALID_URL = r'(?:https?://.*brightcove\.com/(services|viewer).*?\?|brightcove:)(?P<query>.*)'
|
||||||
_FEDERATED_URL_TEMPLATE = 'http://c.brightcove.com/services/viewer/htmlFederated?%s'
|
_FEDERATED_URL_TEMPLATE = 'http://c.brightcove.com/services/viewer/htmlFederated?%s'
|
||||||
|
|
||||||
@@ -117,7 +123,10 @@ class BrightcoveIE(InfoExtractor):
|
|||||||
object_str = re.sub(r'(<object[^>]*)(xmlns=".*?")', r'\1', object_str)
|
object_str = re.sub(r'(<object[^>]*)(xmlns=".*?")', r'\1', object_str)
|
||||||
object_str = fix_xml_ampersands(object_str)
|
object_str = fix_xml_ampersands(object_str)
|
||||||
|
|
||||||
object_doc = xml.etree.ElementTree.fromstring(object_str.encode('utf-8'))
|
try:
|
||||||
|
object_doc = compat_etree_fromstring(object_str.encode('utf-8'))
|
||||||
|
except compat_xml_parse_error:
|
||||||
|
return
|
||||||
|
|
||||||
fv_el = find_xpath_attr(object_doc, './param', 'name', 'flashVars')
|
fv_el = find_xpath_attr(object_doc, './param', 'name', 'flashVars')
|
||||||
if fv_el is not None:
|
if fv_el is not None:
|
||||||
@@ -153,6 +162,28 @@ class BrightcoveIE(InfoExtractor):
|
|||||||
linkBase = find_param('linkBaseURL')
|
linkBase = find_param('linkBaseURL')
|
||||||
if linkBase is not None:
|
if linkBase is not None:
|
||||||
params['linkBaseURL'] = linkBase
|
params['linkBaseURL'] = linkBase
|
||||||
|
return cls._make_brightcove_url(params)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _build_brighcove_url_from_js(cls, object_js):
|
||||||
|
# The layout of JS is as follows:
|
||||||
|
# customBC.createVideo = function (width, height, playerID, playerKey, videoPlayer, VideoRandomID) {
|
||||||
|
# // build Brightcove <object /> XML
|
||||||
|
# }
|
||||||
|
m = re.search(
|
||||||
|
r'''(?x)customBC.\createVideo\(
|
||||||
|
.*? # skipping width and height
|
||||||
|
["\'](?P<playerID>\d+)["\']\s*,\s* # playerID
|
||||||
|
["\'](?P<playerKey>AQ[^"\']{48})[^"\']*["\']\s*,\s* # playerKey begins with AQ and is 50 characters
|
||||||
|
# in length, however it's appended to itself
|
||||||
|
# in places, so truncate
|
||||||
|
["\'](?P<videoID>\d+)["\'] # @videoPlayer
|
||||||
|
''', object_js)
|
||||||
|
if m:
|
||||||
|
return cls._make_brightcove_url(m.groupdict())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _make_brightcove_url(cls, params):
|
||||||
data = compat_urllib_parse.urlencode(params)
|
data = compat_urllib_parse.urlencode(params)
|
||||||
return cls._FEDERATED_URL_TEMPLATE % data
|
return cls._FEDERATED_URL_TEMPLATE % data
|
||||||
|
|
||||||
@@ -169,7 +200,7 @@ class BrightcoveIE(InfoExtractor):
|
|||||||
"""Return a list of all Brightcove URLs from the webpage """
|
"""Return a list of all Brightcove URLs from the webpage """
|
||||||
|
|
||||||
url_m = re.search(
|
url_m = re.search(
|
||||||
r'<meta\s+property="og:video"\s+content="(https?://(?:secure|c)\.brightcove.com/[^"]+)"',
|
r'<meta\s+property=[\'"]og:video[\'"]\s+content=[\'"](https?://(?:secure|c)\.brightcove.com/[^\'"]+)[\'"]',
|
||||||
webpage)
|
webpage)
|
||||||
if url_m:
|
if url_m:
|
||||||
url = unescapeHTML(url_m.group(1))
|
url = unescapeHTML(url_m.group(1))
|
||||||
@@ -183,9 +214,14 @@ class BrightcoveIE(InfoExtractor):
|
|||||||
(?:
|
(?:
|
||||||
[^>]+?class=[\'"][^>]*?BrightcoveExperience.*?[\'"] |
|
[^>]+?class=[\'"][^>]*?BrightcoveExperience.*?[\'"] |
|
||||||
[^>]*?>\s*<param\s+name="movie"\s+value="https?://[^/]*brightcove\.com/
|
[^>]*?>\s*<param\s+name="movie"\s+value="https?://[^/]*brightcove\.com/
|
||||||
).+?</object>''',
|
).+?>\s*</object>''',
|
||||||
webpage)
|
webpage)
|
||||||
return [cls._build_brighcove_url(m) for m in matches]
|
if matches:
|
||||||
|
return list(filter(None, [cls._build_brighcove_url(m) for m in matches]))
|
||||||
|
|
||||||
|
return list(filter(None, [
|
||||||
|
cls._build_brighcove_url_from_js(custom_bc)
|
||||||
|
for custom_bc in re.findall(r'(customBC\.createVideo\(.+?\);)', webpage)]))
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
url, smuggled_data = unsmuggle_url(url, {})
|
url, smuggled_data = unsmuggle_url(url, {})
|
||||||
@@ -214,7 +250,7 @@ class BrightcoveIE(InfoExtractor):
|
|||||||
|
|
||||||
def _get_video_info(self, video_id, query_str, query, referer=None):
|
def _get_video_info(self, video_id, query_str, query, referer=None):
|
||||||
request_url = self._FEDERATED_URL_TEMPLATE % query_str
|
request_url = self._FEDERATED_URL_TEMPLATE % query_str
|
||||||
req = compat_urllib_request.Request(request_url)
|
req = sanitized_Request(request_url)
|
||||||
linkBase = query.get('linkBaseURL')
|
linkBase = query.get('linkBaseURL')
|
||||||
if linkBase is not None:
|
if linkBase is not None:
|
||||||
referer = linkBase[0]
|
referer = linkBase[0]
|
||||||
@@ -315,3 +351,181 @@ class BrightcoveIE(InfoExtractor):
|
|||||||
if 'url' not in info and not info.get('formats'):
|
if 'url' not in info and not info.get('formats'):
|
||||||
raise ExtractorError('Unable to extract video url for %s' % info['id'])
|
raise ExtractorError('Unable to extract video url for %s' % info['id'])
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
class BrightcoveNewIE(InfoExtractor):
|
||||||
|
IE_NAME = 'brightcove:new'
|
||||||
|
_VALID_URL = r'https?://players\.brightcove\.net/(?P<account_id>\d+)/(?P<player_id>[^/]+)_(?P<embed>[^/]+)/index\.html\?.*videoId=(?P<video_id>(?:ref:)?\d+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://players.brightcove.net/929656772001/e41d32dc-ec74-459e-a845-6c69f7b724ea_default/index.html?videoId=4463358922001',
|
||||||
|
'md5': 'c8100925723840d4b0d243f7025703be',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '4463358922001',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Meet the man behind Popcorn Time',
|
||||||
|
'description': 'md5:eac376a4fe366edc70279bfb681aea16',
|
||||||
|
'duration': 165.768,
|
||||||
|
'timestamp': 1441391203,
|
||||||
|
'upload_date': '20150904',
|
||||||
|
'uploader_id': '929656772001',
|
||||||
|
'formats': 'mincount:22',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# with rtmp streams
|
||||||
|
'url': 'http://players.brightcove.net/4036320279001/5d112ed9-283f-485f-a7f9-33f42e8bc042_default/index.html?videoId=4279049078001',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '4279049078001',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Titansgrave: Chapter 0',
|
||||||
|
'description': 'Titansgrave: Chapter 0',
|
||||||
|
'duration': 1242.058,
|
||||||
|
'timestamp': 1433556729,
|
||||||
|
'upload_date': '20150606',
|
||||||
|
'uploader_id': '4036320279001',
|
||||||
|
'formats': 'mincount:41',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
# ref: prefixed video id
|
||||||
|
'url': 'http://players.brightcove.net/3910869709001/21519b5c-4b3b-4363-accb-bdc8f358f823_default/index.html?videoId=ref:7069442',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_url(webpage):
|
||||||
|
urls = BrightcoveNewIE._extract_urls(webpage)
|
||||||
|
return urls[0] if urls else None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_urls(webpage):
|
||||||
|
# Reference:
|
||||||
|
# 1. http://docs.brightcove.com/en/video-cloud/brightcove-player/guides/publish-video.html#setvideoiniframe
|
||||||
|
# 2. http://docs.brightcove.com/en/video-cloud/brightcove-player/guides/publish-video.html#setvideousingjavascript
|
||||||
|
# 3. http://docs.brightcove.com/en/video-cloud/brightcove-player/guides/embed-in-page.html
|
||||||
|
# 4. https://support.brightcove.com/en/video-cloud/docs/dynamically-assigning-videos-player
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
|
||||||
|
# Look for iframe embeds [1]
|
||||||
|
for _, url in re.findall(
|
||||||
|
r'<iframe[^>]+src=(["\'])((?:https?:)//players\.brightcove\.net/\d+/[^/]+/index\.html.+?)\1', webpage):
|
||||||
|
entries.append(url)
|
||||||
|
|
||||||
|
# Look for embed_in_page embeds [2]
|
||||||
|
for video_id, account_id, player_id, embed in re.findall(
|
||||||
|
# According to examples from [3] it's unclear whether video id
|
||||||
|
# may be optional and what to do when it is
|
||||||
|
# According to [4] data-video-id may be prefixed with ref:
|
||||||
|
r'''(?sx)
|
||||||
|
<video[^>]+
|
||||||
|
data-video-id=["\']((?:ref:)?\d+)["\'][^>]*>.*?
|
||||||
|
</video>.*?
|
||||||
|
<script[^>]+
|
||||||
|
src=["\'](?:https?:)?//players\.brightcove\.net/
|
||||||
|
(\d+)/([\da-f-]+)_([^/]+)/index\.min\.js
|
||||||
|
''', webpage):
|
||||||
|
entries.append(
|
||||||
|
'http://players.brightcove.net/%s/%s_%s/index.html?videoId=%s'
|
||||||
|
% (account_id, player_id, embed, video_id))
|
||||||
|
|
||||||
|
return entries
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
account_id, player_id, embed, video_id = re.match(self._VALID_URL, url).groups()
|
||||||
|
|
||||||
|
webpage = self._download_webpage(
|
||||||
|
'http://players.brightcove.net/%s/%s_%s/index.min.js'
|
||||||
|
% (account_id, player_id, embed), video_id)
|
||||||
|
|
||||||
|
policy_key = None
|
||||||
|
|
||||||
|
catalog = self._search_regex(
|
||||||
|
r'catalog\(({.+?})\);', webpage, 'catalog', default=None)
|
||||||
|
if catalog:
|
||||||
|
catalog = self._parse_json(
|
||||||
|
js_to_json(catalog), video_id, fatal=False)
|
||||||
|
if catalog:
|
||||||
|
policy_key = catalog.get('policyKey')
|
||||||
|
|
||||||
|
if not policy_key:
|
||||||
|
policy_key = self._search_regex(
|
||||||
|
r'policyKey\s*:\s*(["\'])(?P<pk>.+?)\1',
|
||||||
|
webpage, 'policy key', group='pk')
|
||||||
|
|
||||||
|
req = sanitized_Request(
|
||||||
|
'https://edge.api.brightcove.com/playback/v1/accounts/%s/videos/%s'
|
||||||
|
% (account_id, video_id),
|
||||||
|
headers={'Accept': 'application/json;pk=%s' % policy_key})
|
||||||
|
json_data = self._download_json(req, video_id)
|
||||||
|
|
||||||
|
title = json_data['name']
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
for source in json_data.get('sources', []):
|
||||||
|
source_type = source.get('type')
|
||||||
|
src = source.get('src')
|
||||||
|
if source_type == 'application/x-mpegURL':
|
||||||
|
if not src:
|
||||||
|
continue
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
src, video_id, 'mp4', entry_protocol='m3u8_native',
|
||||||
|
m3u8_id='hls', fatal=False))
|
||||||
|
else:
|
||||||
|
streaming_src = source.get('streaming_src')
|
||||||
|
stream_name, app_name = source.get('stream_name'), source.get('app_name')
|
||||||
|
if not src and not streaming_src and (not stream_name or not app_name):
|
||||||
|
continue
|
||||||
|
tbr = float_or_none(source.get('avg_bitrate'), 1000)
|
||||||
|
height = int_or_none(source.get('height'))
|
||||||
|
f = {
|
||||||
|
'tbr': tbr,
|
||||||
|
'width': int_or_none(source.get('width')),
|
||||||
|
'height': height,
|
||||||
|
'filesize': int_or_none(source.get('size')),
|
||||||
|
'container': source.get('container'),
|
||||||
|
'vcodec': source.get('codec'),
|
||||||
|
'ext': source.get('container').lower(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def build_format_id(kind):
|
||||||
|
format_id = kind
|
||||||
|
if tbr:
|
||||||
|
format_id += '-%dk' % int(tbr)
|
||||||
|
if height:
|
||||||
|
format_id += '-%dp' % height
|
||||||
|
return format_id
|
||||||
|
|
||||||
|
if src or streaming_src:
|
||||||
|
f.update({
|
||||||
|
'url': src or streaming_src,
|
||||||
|
'format_id': build_format_id('http' if src else 'http-streaming'),
|
||||||
|
'preference': 2 if src else 1,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
f.update({
|
||||||
|
'url': app_name,
|
||||||
|
'play_path': stream_name,
|
||||||
|
'format_id': build_format_id('rtmp'),
|
||||||
|
})
|
||||||
|
formats.append(f)
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
description = json_data.get('description')
|
||||||
|
thumbnail = json_data.get('thumbnail')
|
||||||
|
timestamp = parse_iso8601(json_data.get('published_at'))
|
||||||
|
duration = float_or_none(json_data.get('duration'), 1000)
|
||||||
|
tags = json_data.get('tags', [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
|
'duration': duration,
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'uploader_id': account_id,
|
||||||
|
'formats': formats,
|
||||||
|
'tags': tags,
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ class BYUtvIE(InfoExtractor):
|
|||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'studio-c-season-5-episode-5',
|
'id': 'studio-c-season-5-episode-5',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'description': 'md5:5438d33774b6bdc662f9485a340401cc',
|
'description': 'md5:e07269172baff037f8e8bf9956bc9747',
|
||||||
'title': 'Season 5 Episode 5',
|
'title': 'Season 5 Episode 5',
|
||||||
'thumbnail': 're:^https?://.*promo.*'
|
'thumbnail': 're:^https?://.*\.jpg$',
|
||||||
|
'duration': 1486.486,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
# coding: utf-8
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
|
||||||
|
|
||||||
|
|
||||||
class Canal13clIE(InfoExtractor):
|
|
||||||
_VALID_URL = r'^http://(?:www\.)?13\.cl/(?:[^/?#]+/)*(?P<id>[^/?#]+)'
|
|
||||||
_TEST = {
|
|
||||||
'url': 'http://www.13.cl/t13/nacional/el-circulo-de-hierro-de-michelle-bachelet-en-su-regreso-a-la-moneda',
|
|
||||||
'md5': '4cb1fa38adcad8fea88487a078831755',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '1403022125',
|
|
||||||
'display_id': 'el-circulo-de-hierro-de-michelle-bachelet-en-su-regreso-a-la-moneda',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'El "círculo de hierro" de Michelle Bachelet en su regreso a La Moneda',
|
|
||||||
'description': '(Foto: Agencia Uno) En nueve días más, Michelle Bachelet va a asumir por segunda vez como presidenta de la República. Entre aquellos que la acompañarán hay caras que se repiten y otras que se consolidan en su entorno de colaboradores más cercanos.',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
mobj = re.match(self._VALID_URL, url)
|
|
||||||
display_id = mobj.group('id')
|
|
||||||
|
|
||||||
webpage = self._download_webpage(url, display_id)
|
|
||||||
|
|
||||||
title = self._html_search_meta(
|
|
||||||
'twitter:title', webpage, 'title', fatal=True)
|
|
||||||
description = self._html_search_meta(
|
|
||||||
'twitter:description', webpage, 'description')
|
|
||||||
url = self._html_search_regex(
|
|
||||||
r'articuloVideo = \"(.*?)\"', webpage, 'url')
|
|
||||||
real_id = self._search_regex(
|
|
||||||
r'[^0-9]([0-9]{7,})[^0-9]', url, 'id', default=display_id)
|
|
||||||
thumbnail = self._html_search_regex(
|
|
||||||
r'articuloImagen = \"(.*?)\"', webpage, 'thumbnail')
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': real_id,
|
|
||||||
'display_id': display_id,
|
|
||||||
'url': url,
|
|
||||||
'title': title,
|
|
||||||
'description': description,
|
|
||||||
'ext': 'mp4',
|
|
||||||
'thumbnail': thumbnail,
|
|
||||||
}
|
|
||||||
@@ -4,38 +4,65 @@ from __future__ import unicode_literals
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
from ..utils import parse_duration
|
||||||
|
|
||||||
|
|
||||||
class Canalc2IE(InfoExtractor):
|
class Canalc2IE(InfoExtractor):
|
||||||
IE_NAME = 'canalc2.tv'
|
IE_NAME = 'canalc2.tv'
|
||||||
_VALID_URL = r'http://.*?\.canalc2\.tv/video\.asp\?.*?idVideo=(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:(?:www\.)?canalc2\.tv/video/|archives-canalc2\.u-strasbg\.fr/video\.asp\?.*\bidVideo=)(?P<id>\d+)'
|
||||||
|
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'http://www.canalc2.tv/video.asp?idVideo=12163&voir=oui',
|
'url': 'http://www.canalc2.tv/video/12163',
|
||||||
'md5': '060158428b650f896c542dfbb3d6487f',
|
'md5': '060158428b650f896c542dfbb3d6487f',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '12163',
|
'id': '12163',
|
||||||
'ext': 'mp4',
|
'ext': 'flv',
|
||||||
'title': 'Terrasses du Numérique'
|
'title': 'Terrasses du Numérique',
|
||||||
|
'duration': 122,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True, # Requires rtmpdump
|
||||||
}
|
}
|
||||||
}
|
}, {
|
||||||
|
'url': 'http://archives-canalc2.u-strasbg.fr/video.asp?idVideo=11427&voir=oui',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = re.match(self._VALID_URL, url).group('id')
|
video_id = self._match_id(url)
|
||||||
# We need to set the voir field for getting the file name
|
|
||||||
url = 'http://www.canalc2.tv/video.asp?idVideo=%s&voir=oui' % video_id
|
webpage = self._download_webpage(
|
||||||
webpage = self._download_webpage(url, video_id)
|
'http://www.canalc2.tv/video/%s' % video_id, video_id)
|
||||||
file_name = self._search_regex(
|
|
||||||
r"so\.addVariable\('file','(.*?)'\);",
|
formats = []
|
||||||
webpage, 'file name')
|
for _, video_url in re.findall(r'file\s*=\s*(["\'])(.+?)\1', webpage):
|
||||||
video_url = 'http://vod-flash.u-strasbg.fr:8080/' + file_name
|
if video_url.startswith('rtmp://'):
|
||||||
|
rtmp = re.search(
|
||||||
|
r'^(?P<url>rtmp://[^/]+/(?P<app>.+/))(?P<play_path>mp4:.+)$', video_url)
|
||||||
|
formats.append({
|
||||||
|
'url': rtmp.group('url'),
|
||||||
|
'format_id': 'rtmp',
|
||||||
|
'ext': 'flv',
|
||||||
|
'app': rtmp.group('app'),
|
||||||
|
'play_path': rtmp.group('play_path'),
|
||||||
|
'page_url': url,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
formats.append({
|
||||||
|
'url': video_url,
|
||||||
|
'format_id': 'http',
|
||||||
|
})
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
title = self._html_search_regex(
|
title = self._html_search_regex(
|
||||||
r'class="evenement8">(.*?)</a>', webpage, 'title')
|
r'(?s)class="[^"]*col_description[^"]*">.*?<h3>(.*?)</h3>', webpage, 'title')
|
||||||
|
duration = parse_duration(self._search_regex(
|
||||||
|
r'id=["\']video_duree["\'][^>]*>([^<]+)',
|
||||||
|
webpage, 'duration', fatal=False))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'ext': 'mp4',
|
|
||||||
'url': video_url,
|
|
||||||
'title': title,
|
'title': title,
|
||||||
|
'duration': duration,
|
||||||
|
'formats': formats,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,14 @@ from ..utils import (
|
|||||||
unified_strdate,
|
unified_strdate,
|
||||||
url_basename,
|
url_basename,
|
||||||
qualities,
|
qualities,
|
||||||
|
int_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CanalplusIE(InfoExtractor):
|
class CanalplusIE(InfoExtractor):
|
||||||
IE_DESC = 'canalplus.fr, piwiplus.fr and d8.tv'
|
IE_DESC = 'canalplus.fr, piwiplus.fr and d8.tv'
|
||||||
_VALID_URL = r'https?://(?:www\.(?P<site>canalplus\.fr|piwiplus\.fr|d8\.tv|itele\.fr)/.*?/(?P<path>.*)|player\.canalplus\.fr/#/(?P<id>[0-9]+))'
|
_VALID_URL = r'https?://(?:www\.(?P<site>canalplus\.fr|piwiplus\.fr|d8\.tv|itele\.fr)/.*?/(?P<path>.*)|player\.canalplus\.fr/#/(?P<id>[0-9]+))'
|
||||||
_VIDEO_INFO_TEMPLATE = 'http://service.canal-plus.com/video/rest/getVideosLiees/%s/%s'
|
_VIDEO_INFO_TEMPLATE = 'http://service.canal-plus.com/video/rest/getVideosLiees/%s/%s?format=json'
|
||||||
_SITE_ID_MAP = {
|
_SITE_ID_MAP = {
|
||||||
'canalplus.fr': 'cplus',
|
'canalplus.fr': 'cplus',
|
||||||
'piwiplus.fr': 'teletoon',
|
'piwiplus.fr': 'teletoon',
|
||||||
@@ -25,14 +26,14 @@ class CanalplusIE(InfoExtractor):
|
|||||||
}
|
}
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.canalplus.fr/c-infos-documentaires/pid1830-c-zapping.html?vid=922470',
|
'url': 'http://www.canalplus.fr/c-emissions/pid1830-c-zapping.html?vid=1263092',
|
||||||
'md5': '3db39fb48b9685438ecf33a1078023e4',
|
'md5': '12164a6f14ff6df8bd628e8ba9b10b78',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '922470',
|
'id': '1263092',
|
||||||
'ext': 'flv',
|
'ext': 'mp4',
|
||||||
'title': 'Zapping - 26/08/13',
|
'title': 'Le Zapping - 13/05/15',
|
||||||
'description': 'Le meilleur de toutes les chaînes, tous les jours.\nEmission du 26 août 2013',
|
'description': 'md5:09738c0d06be4b5d06a0940edb0da73f',
|
||||||
'upload_date': '20130826',
|
'upload_date': '20150513',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.piwiplus.fr/videos-piwi/pid1405-le-labyrinthe-boing-super-ranger.html?vid=1108190',
|
'url': 'http://www.piwiplus.fr/videos-piwi/pid1405-le-labyrinthe-boing-super-ranger.html?vid=1108190',
|
||||||
@@ -56,10 +57,10 @@ class CanalplusIE(InfoExtractor):
|
|||||||
'skip': 'videos get deleted after a while',
|
'skip': 'videos get deleted after a while',
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.itele.fr/france/video/aubervilliers-un-lycee-en-colere-111559',
|
'url': 'http://www.itele.fr/france/video/aubervilliers-un-lycee-en-colere-111559',
|
||||||
'md5': '65aa83ad62fe107ce29e564bb8712580',
|
'md5': '38b8f7934def74f0d6f3ba6c036a5f82',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '1213714',
|
'id': '1213714',
|
||||||
'ext': 'flv',
|
'ext': 'mp4',
|
||||||
'title': 'Aubervilliers : un lycée en colère - Le 11/02/2015 à 06h45',
|
'title': 'Aubervilliers : un lycée en colère - Le 11/02/2015 à 06h45',
|
||||||
'description': 'md5:8216206ec53426ea6321321f3b3c16db',
|
'description': 'md5:8216206ec53426ea6321321f3b3c16db',
|
||||||
'upload_date': '20150211',
|
'upload_date': '20150211',
|
||||||
@@ -78,18 +79,20 @@ class CanalplusIE(InfoExtractor):
|
|||||||
if video_id is None:
|
if video_id is None:
|
||||||
webpage = self._download_webpage(url, display_id)
|
webpage = self._download_webpage(url, display_id)
|
||||||
video_id = self._search_regex(
|
video_id = self._search_regex(
|
||||||
r'<canal:player[^>]+?videoId="(\d+)"', webpage, 'video id')
|
[r'<canal:player[^>]+?videoId=(["\'])(?P<id>\d+)', r'id=["\']canal_video_player(?P<id>\d+)'],
|
||||||
|
webpage, 'video id', group='id')
|
||||||
|
|
||||||
info_url = self._VIDEO_INFO_TEMPLATE % (site_id, video_id)
|
info_url = self._VIDEO_INFO_TEMPLATE % (site_id, video_id)
|
||||||
doc = self._download_xml(info_url, video_id, 'Downloading video XML')
|
video_data = self._download_json(info_url, video_id, 'Downloading video JSON')
|
||||||
|
|
||||||
video_info = [video for video in doc if video.find('ID').text == video_id][0]
|
if isinstance(video_data, list):
|
||||||
media = video_info.find('MEDIA')
|
video_data = [video for video in video_data if video.get('ID') == video_id][0]
|
||||||
infos = video_info.find('INFOS')
|
media = video_data['MEDIA']
|
||||||
|
infos = video_data['INFOS']
|
||||||
|
|
||||||
preference = qualities(['MOBILE', 'BAS_DEBIT', 'HAUT_DEBIT', 'HD', 'HLS', 'HDS'])
|
preference = qualities(['MOBILE', 'BAS_DEBIT', 'HAUT_DEBIT', 'HD'])
|
||||||
|
|
||||||
fmt_url = next(iter(media.find('VIDEOS'))).text
|
fmt_url = next(iter(media.get('VIDEOS')))
|
||||||
if '/geo' in fmt_url.lower():
|
if '/geo' in fmt_url.lower():
|
||||||
response = self._request_webpage(
|
response = self._request_webpage(
|
||||||
HEADRequest(fmt_url), video_id,
|
HEADRequest(fmt_url), video_id,
|
||||||
@@ -100,39 +103,42 @@ class CanalplusIE(InfoExtractor):
|
|||||||
expected=True)
|
expected=True)
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
for fmt in media.find('VIDEOS'):
|
for format_id, format_url in media['VIDEOS'].items():
|
||||||
format_url = fmt.text
|
|
||||||
if not format_url:
|
if not format_url:
|
||||||
continue
|
continue
|
||||||
format_id = fmt.tag
|
|
||||||
if format_id == 'HLS':
|
if format_id == 'HLS':
|
||||||
hls_formats = self._extract_m3u8_formats(format_url, video_id, 'flv')
|
formats.extend(self._extract_m3u8_formats(
|
||||||
for fmt in hls_formats:
|
format_url, video_id, 'mp4', 'm3u8_native', m3u8_id=format_id, fatal=False))
|
||||||
fmt['preference'] = preference(format_id)
|
|
||||||
formats.extend(hls_formats)
|
|
||||||
elif format_id == 'HDS':
|
elif format_id == 'HDS':
|
||||||
hds_formats = self._extract_f4m_formats(format_url + '?hdcore=2.11.3', video_id)
|
formats.extend(self._extract_f4m_formats(
|
||||||
for fmt in hds_formats:
|
format_url + '?hdcore=2.11.3', video_id, f4m_id=format_id, fatal=False))
|
||||||
fmt['preference'] = preference(format_id)
|
|
||||||
formats.extend(hds_formats)
|
|
||||||
else:
|
else:
|
||||||
formats.append({
|
formats.append({
|
||||||
'url': format_url,
|
# the secret extracted ya function in http://player.canalplus.fr/common/js/canalPlayer.js
|
||||||
|
'url': format_url + '?secret=pqzerjlsmdkjfoiuerhsdlfknaes',
|
||||||
'format_id': format_id,
|
'format_id': format_id,
|
||||||
'preference': preference(format_id),
|
'preference': preference(format_id),
|
||||||
})
|
})
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
thumbnails = [{
|
||||||
|
'id': image_id,
|
||||||
|
'url': image_url,
|
||||||
|
} for image_id, image_url in media.get('images', {}).items()]
|
||||||
|
|
||||||
|
titrage = infos['TITRAGE']
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'display_id': display_id,
|
'display_id': display_id,
|
||||||
'title': '%s - %s' % (infos.find('TITRAGE/TITRE').text,
|
'title': '%s - %s' % (titrage['TITRE'],
|
||||||
infos.find('TITRAGE/SOUS_TITRE').text),
|
titrage['SOUS_TITRE']),
|
||||||
'upload_date': unified_strdate(infos.find('PUBLICATION/DATE').text),
|
'upload_date': unified_strdate(infos.get('PUBLICATION', {}).get('DATE')),
|
||||||
'thumbnail': media.find('IMAGES/GRAND').text,
|
'thumbnails': thumbnails,
|
||||||
'description': infos.find('DESCRIPTION').text,
|
'description': infos.get('DESCRIPTION'),
|
||||||
'view_count': int(infos.find('NB_VUES').text),
|
'duration': int_or_none(infos.get('DURATION')),
|
||||||
'like_count': int(infos.find('NB_LIKES').text),
|
'view_count': int_or_none(infos.get('NB_VUES')),
|
||||||
'comment_count': int(infos.find('NB_COMMENTS').text),
|
'like_count': int_or_none(infos.get('NB_LIKES')),
|
||||||
|
'comment_count': int_or_none(infos.get('NB_COMMENTS')),
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user