Compare commits
1880 Commits
2015.03.28
...
2015.09.09
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d85187eb74 | ||
|
|
cc1ac11017 | ||
|
|
73f536439e | ||
|
|
b17e7d9a9b | ||
|
|
2f29b758e0 | ||
|
|
482aa3fecc | ||
|
|
d9c19db340 | ||
|
|
6c4d243de5 | ||
|
|
d1561ef777 | ||
|
|
1072336249 | ||
|
|
75bb5c7028 | ||
|
|
47004d9579 | ||
|
|
12810c9cd3 | ||
|
|
7a459170fa | ||
|
|
3cf0df568a | ||
|
|
b88ebd472e | ||
|
|
3ecc527209 | ||
|
|
b1b7d1ffba | ||
|
|
4003bd82b0 | ||
|
|
8801255d7d | ||
|
|
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 | ||
|
|
8df5ae15d1 | ||
|
|
12439dd5ec | ||
|
|
3513d41436 | ||
|
|
cab792abe5 | ||
|
|
8870358b1b | ||
|
|
ee087c79ad | ||
|
|
51f579b635 | ||
|
|
4abf617b9c | ||
|
|
3026164b16 | ||
|
|
9dd73ef4a4 | ||
|
|
75c72a1e67 | ||
|
|
08354db47b | ||
|
|
027eb5a6b0 | ||
|
|
f71264490c | ||
|
|
6270239a6d | ||
|
|
1195a38f46 | ||
|
|
66e289bab4 | ||
|
|
52c6f26cab | ||
|
|
dc534b674f | ||
|
|
f30c2e8e98 | ||
|
|
c482b3c69a | ||
|
|
266b0ad676 | ||
|
|
87f70ab39d | ||
|
|
8e636da499 | ||
|
|
22889ab175 | ||
|
|
5d2354f177 | ||
|
|
a41fb80ce1 | ||
|
|
2e2575e213 | ||
|
|
26c61e0809 | ||
|
|
e7a8c3032d | ||
|
|
725d1c58aa | ||
|
|
bd6742137f | ||
|
|
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 | ||
|
|
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 | ||
|
|
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 | ||
|
|
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 | ||
|
|
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 | ||
|
|
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 | ||
|
|
2315fb5e5f | ||
|
|
8896b614a9 | ||
|
|
a7fce980ad | ||
|
|
db40364b87 | ||
|
|
17941321ab | ||
|
|
5d1f0e607b | ||
|
|
c41a2ec4af | ||
|
|
575dad3c98 |
@@ -5,9 +5,7 @@ python:
|
|||||||
- "3.2"
|
- "3.2"
|
||||||
- "3.3"
|
- "3.3"
|
||||||
- "3.4"
|
- "3.4"
|
||||||
before_install:
|
sudo: false
|
||||||
- sudo apt-get update -qq
|
|
||||||
- sudo apt-get install -yqq rtmpdump
|
|
||||||
script: nosetests test --verbose
|
script: nosetests test --verbose
|
||||||
notifications:
|
notifications:
|
||||||
email:
|
email:
|
||||||
|
|||||||
25
AUTHORS
25
AUTHORS
@@ -118,3 +118,28 @@ Devin J. Pohly
|
|||||||
Eduardo Ferro Aldama
|
Eduardo Ferro Aldama
|
||||||
Jeff Buchbinder
|
Jeff Buchbinder
|
||||||
Amish Bhadeshia
|
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ý
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ If you want to add support for a new site, you can follow this quick list (assum
|
|||||||
```
|
```
|
||||||
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 be 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:
|
||||||
|
|
||||||
|
|||||||
298
README.md
298
README.md
@@ -5,6 +5,7 @@ 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)
|
||||||
@@ -16,12 +17,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 +34,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 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,21 +46,22 @@ 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 sure that you have sufficient permissions (run with sudo if needed)
|
-U, --update Update this program to latest version. Make sure that you have sufficient permissions (run with sudo if needed)
|
||||||
-i, --ignore-errors continue on download errors, for example to skip unavailable videos in a playlist
|
-i, --ignore-errors Continue on download errors, for example to skip unavailable videos in a playlist
|
||||||
--abort-on-error Abort downloading of further videos (in the playlist or the command line) if an error occurs
|
--abort-on-error Abort downloading of further videos (in the playlist or the command line) if an error 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 they would handle
|
--list-extractors List all supported extractors
|
||||||
--extractor-descriptions Output descriptions of all supported extractors
|
--extractor-descriptions Output descriptions of all supported extractors
|
||||||
--default-search PREFIX Use this prefix for unqualified URLs. For example "gvsearch2:" downloads two videos from google videos for youtube-dl "large apple".
|
--force-generic-extractor Force extraction to use the generic extractor
|
||||||
|
--default-search PREFIX Use this prefix for unqualified URLs. For example "gvsearch2:" downloads two videos from google videos for youtube-dl "large apple".
|
||||||
Use the value "auto" to let youtube-dl guess ("auto_warning" to emit a warning when guessing). "error" just throws an error. The
|
Use the value "auto" to let youtube-dl guess ("auto_warning" to emit a warning when guessing). "error" just throws an error. The
|
||||||
default value "fixup_error" repairs broken URLs, but emits an error if this is not possible instead of searching.
|
default value "fixup_error" repairs broken URLs, but emits an error if this is not possible instead of searching.
|
||||||
--ignore-config Do not read configuration files. When given in the global configuration file /etc/youtube-dl.conf: Do not read the user configuration
|
--ignore-config Do not read configuration files. When given in the global configuration file /etc/youtube-dl.conf: Do not read the user configuration
|
||||||
in ~/.config/youtube-dl/config (%APPDATA%/youtube-dl/config.txt on Windows)
|
in ~/.config/youtube-dl/config (%APPDATA%/youtube-dl/config.txt on Windows)
|
||||||
--flat-playlist Do not extract the videos of a playlist, only list them.
|
--flat-playlist Do not extract the videos of a playlist, 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 an empty string (--proxy "") for direct connection
|
--proxy URL Use the specified HTTP/HTTPS proxy. Pass in an empty string (--proxy "") for direct connection
|
||||||
@@ -71,70 +73,70 @@ which means you can modify it, redistribute it or use it however you like.
|
|||||||
not present) is used for the actual downloading. (experimental)
|
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 indices of the videos in the playlist seperated by commas like: "--playlist-items 1,2,5,8"
|
--playlist-items ITEM_SPEC Playlist video items to download. Specify indices of the videos in the playlist separated by commas like: "--playlist-items 1,2,5,8"
|
||||||
if you want to download videos indexed 1, 2, 5, 8 in the playlist. You can specify range: "--playlist-items 1-3,7,10-13", it will
|
if you want to download videos indexed 1, 2, 5, 8 in the playlist. You can specify range: "--playlist-items 1-3,7,10-13", it will
|
||||||
download the videos at index 1, 2, 3, 7, 10, 11, 12 and 13.
|
download the videos at index 1, 2, 3, 7, 10, 11, 12 and 13.
|
||||||
--match-title REGEX download only matching titles (regex or caseless sub-string)
|
--match-title REGEX Download only matching titles (regex or caseless sub-string)
|
||||||
--reject-title REGEX skip download for matching titles (regex or caseless sub-string)
|
--reject-title REGEX Skip download for matching titles (regex or caseless sub-string)
|
||||||
--max-downloads NUMBER Abort after downloading NUMBER files
|
--max-downloads NUMBER Abort after downloading NUMBER files
|
||||||
--min-filesize SIZE Do not download any videos smaller than SIZE (e.g. 50k or 44.6m)
|
--min-filesize SIZE Do not download any videos smaller than SIZE (e.g. 50k or 44.6m)
|
||||||
--max-filesize SIZE Do not download any videos larger than SIZE (e.g. 50k or 44.6m)
|
--max-filesize SIZE Do not download any videos larger than SIZE (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 this date (i.e. inclusive)
|
--datebefore DATE Download only videos uploaded on or before this date (i.e. inclusive)
|
||||||
--dateafter DATE download only videos uploaded on or after this date (i.e. inclusive)
|
--dateafter DATE Download only videos uploaded on or after this date (i.e. inclusive)
|
||||||
--min-views COUNT Do not download any videos with less than COUNT views
|
--min-views COUNT Do not download any videos with less than COUNT views
|
||||||
--max-views COUNT Do not download any videos with more than COUNT views
|
--max-views COUNT Do not download any videos with more than COUNT views
|
||||||
--match-filter FILTER (Experimental) Generic video filter. Specify any key (see help for -o for a list of available keys) to match if the key is present,
|
--match-filter FILTER Generic video filter (experimental). Specify any key (see help for -o for a list of available keys) to match if the key is present,
|
||||||
!key to check if the key is not present,key > NUMBER (like "comment_count > 12", also works with >=, <, <=, !=, =) to compare against
|
!key to check if the key is not present,key > NUMBER (like "comment_count > 12", also works with >=, <, <=, !=, =) to compare against
|
||||||
a number, and & to require multiple matches. Values which are not known are excluded unless you put a question mark (?) after the
|
a number, and & to require multiple matches. Values which are not known are excluded unless you put a question mark (?) after the
|
||||||
operator.For example, to only match videos that have been liked more than 100 times and disliked less than 50 times (or the dislike
|
operator.For example, to only match videos that have been liked more than 100 times and disliked less than 50 times (or the dislike
|
||||||
functionality is not available at the given service), but who also have a description, use --match-filter "like_count > 100 &
|
functionality is not available at the given service), but who also have a description, use --match-filter "like_count > 100 &
|
||||||
dislike_count <? 50 & description" .
|
dislike_count <? 50 & description" .
|
||||||
--no-playlist If the URL refers to a video and a playlist, download only the video.
|
--no-playlist Download only the video, if the URL refers to a video and a playlist.
|
||||||
--yes-playlist If the URL refers to a video and a playlist, download the playlist.
|
--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-limit YEARS Download only videos suitable for the given age
|
||||||
--download-archive FILE Download only videos not listed in the archive file. Record the IDs of all downloaded videos in it.
|
--download-archive FILE Download only videos not listed in the archive file. Record the IDs of all downloaded videos in it.
|
||||||
--include-ads Download advertisements as well (experimental)
|
--include-ads Download advertisements as well (experimental)
|
||||||
|
|
||||||
## Download Options:
|
## Download Options:
|
||||||
-r, --rate-limit LIMIT maximum download rate in bytes per second (e.g. 50K or 4.2M)
|
-r, --rate-limit LIMIT Maximum download rate in bytes per second (e.g. 50K or 4.2M)
|
||||||
-R, --retries RETRIES number of retries (default is 10), or "infinite".
|
-R, --retries RETRIES Number of retries (default is 10), or "infinite".
|
||||||
--buffer-size SIZE size of download buffer (e.g. 1024 or 16K) (default is 1024)
|
--buffer-size SIZE Size of download buffer (e.g. 1024 or 16K) (default is 1024)
|
||||||
--no-resize-buffer do not automatically adjust the buffer size. By default, the buffer size is automatically resized from an initial value of SIZE.
|
--no-resize-buffer Do not automatically adjust the buffer size. By default, the buffer size is automatically resized from an initial value 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 ytdl.filesize with expected filesize
|
--xattr-set-filesize Set file xattribute ytdl.filesize with expected filesize (experimental)
|
||||||
--hls-prefer-native (experimental) Use the native HLS downloader instead of ffmpeg.
|
--hls-prefer-native Use the native HLS downloader instead of ffmpeg (experimental)
|
||||||
--external-downloader COMMAND Use the specified external downloader. Currently supports aria2c,curl,wget
|
--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.
|
--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 stdin)
|
-a, --batch-file FILE File containing URLs to download ('-' for 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 get the title, %(uploader)s for the uploader name, %(uploader_id)s for the uploader
|
-o, --output TEMPLATE Output filename template. Use %(title)s to get the title, %(uploader)s for the uploader name, %(uploader_id)s for the uploader
|
||||||
nickname if different, %(autonumber)s to get an automatically incremented number, %(ext)s for the filename extension, %(format)s for
|
nickname if different, %(autonumber)s to get an automatically incremented number, %(ext)s for the filename extension, %(format)s for
|
||||||
the format description (like "22 - 1280x720" or "HD"), %(format_id)s for the unique id of the format (like Youtube's itags: "137"),
|
the format description (like "22 - 1280x720" or "HD"), %(format_id)s for the unique id of the format (like YouTube's itags: "137"),
|
||||||
%(upload_date)s for the upload date (YYYYMMDD), %(extractor)s for the provider (youtube, metacafe, etc), %(id)s for the video id,
|
%(upload_date)s for the upload date (YYYYMMDD), %(extractor)s for the provider (youtube, metacafe, etc), %(id)s for the video id,
|
||||||
%(playlist_title)s, %(playlist_id)s, or %(playlist)s (=title if present, ID otherwise) for the playlist the video is in,
|
%(playlist_title)s, %(playlist_id)s, or %(playlist)s (=title if present, ID otherwise) for the playlist the video is in,
|
||||||
%(playlist_index)s for the position in the playlist. %(height)s and %(width)s for the width and height of the video format.
|
%(playlist_index)s for the position in the playlist. %(height)s and %(width)s for the width and height of the video format.
|
||||||
%(resolution)s for a textual description of the resolution of the video format. %% for a literal percent. Use - to output to stdout.
|
%(resolution)s for a textual description of the resolution of the video format. %% for a literal percent. Use - to output to stdout.
|
||||||
Can also be used to download to a different directory, for example with -o '/my/downloads/%(uploader)s/%(title)s-%(id)s.%(ext)s' .
|
Can also be used to download to a different directory, for example with -o '/my/downloads/%(uploader)s/%(title)s-%(id)s.%(ext)s' .
|
||||||
--autonumber-size NUMBER Specifies the number of digits in %(autonumber)s when it is present in output filename template or --auto-number option is given
|
--autonumber-size NUMBER Specify the number of digits in %(autonumber)s when it is present in output filename template or --auto-number option is given
|
||||||
--restrict-filenames Restrict filenames to only ASCII characters, and avoid "&" and spaces in filenames
|
--restrict-filenames Restrict filenames to only ASCII characters, and avoid "&" and spaces in filenames
|
||||||
-A, --auto-number [deprecated; use -o "%(autonumber)s-%(title)s.%(ext)s" ] number downloaded files starting from 00000
|
-A, --auto-number [deprecated; use -o "%(autonumber)s-%(title)s.%(ext)s" ] Number downloaded files starting from 00000
|
||||||
-t, --title [deprecated] use title in file name (default)
|
-t, --title [deprecated] Use title in file name (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. By default, youtube-dl will resume downloads if possible.
|
-c, --continue Force resume of partially downloaded files. By default, youtube-dl will resume downloads if possible.
|
||||||
--no-continue do not resume partially downloaded files (restart from beginning)
|
--no-continue Do not resume partially downloaded files (restart from beginning)
|
||||||
--no-part do not use .part files - write directly into output file
|
--no-part Do not use .part files - write directly into output file
|
||||||
--no-mtime do not use the Last-modified header to set the file modification time
|
--no-mtime Do not use the Last-modified header to set the file modification time
|
||||||
--write-description write video description to a .description file
|
--write-description Write video description to a .description 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 file
|
--write-annotations Write video annotations to a .annotations.xml file
|
||||||
--load-info FILE json file containing the video information (created with the "--write-json" option)
|
--load-info FILE JSON file containing the video information (created with the "--write-info-json" option)
|
||||||
--cookies FILE file to read cookies from and dump cookie jar in
|
--cookies FILE File to read cookies from and dump cookie jar in
|
||||||
--cache-dir DIR Location in the filesystem where youtube-dl can store some downloaded information permanently. By default $XDG_CACHE_HOME/youtube-dl
|
--cache-dir DIR Location in the filesystem where youtube-dl can store some downloaded information permanently. By default $XDG_CACHE_HOME/youtube-dl
|
||||||
or ~/.cache/youtube-dl . At the moment, only YouTube player files (for videos with obfuscated signatures) are cached, but that may
|
or ~/.cache/youtube-dl . At the moment, only YouTube player files (for videos with obfuscated signatures) are cached, but that may
|
||||||
change.
|
change.
|
||||||
@@ -142,97 +144,88 @@ 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 formats
|
--list-thumbnails Simulate and list all available thumbnail 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 anything to disk
|
-s, --simulate Do not download the video and do not write anything to disk
|
||||||
--skip-download do not download the video
|
--skip-download Do not download the video
|
||||||
-g, --get-url simulate, quiet but print URL
|
-g, --get-url Simulate, quiet but print URL
|
||||||
-e, --get-title simulate, quiet but print title
|
-e, --get-title Simulate, quiet but print title
|
||||||
--get-id simulate, quiet but print id
|
--get-id Simulate, quiet but print id
|
||||||
--get-thumbnail simulate, quiet but print thumbnail URL
|
--get-thumbnail Simulate, quiet but print thumbnail URL
|
||||||
--get-description simulate, quiet but print video description
|
--get-description Simulate, quiet but print video description
|
||||||
--get-duration simulate, quiet but print video length
|
--get-duration Simulate, quiet but print video length
|
||||||
--get-filename simulate, quiet but print output filename
|
--get-filename Simulate, quiet but print output filename
|
||||||
--get-format simulate, quiet but print output format
|
--get-format Simulate, quiet but print output format
|
||||||
-j, --dump-json simulate, quiet but print JSON information. See --output for a description of available keys.
|
-j, --dump-json Simulate, quiet but print JSON information. See --output for a description of available keys.
|
||||||
-J, --dump-single-json simulate, quiet but print JSON information for each command-line argument. If the URL refers to a playlist, dump the whole playlist
|
-J, --dump-single-json Simulate, quiet but print JSON information for each command-line argument. If the URL refers to a playlist, dump the whole playlist
|
||||||
information in a single line.
|
information in a single line.
|
||||||
--print-json Be quiet and print the video information as JSON (video is still being downloaded).
|
--print-json Be quiet and print the video information as JSON (video is still being downloaded).
|
||||||
--newline output progress bar as new lines
|
--newline Output progress bar as new lines
|
||||||
--no-progress do not print progress bar
|
--no-progress Do not print progress bar
|
||||||
--console-title display progress in console titlebar
|
--console-title Display progress in console titlebar
|
||||||
-v, --verbose print various debugging information
|
-v, --verbose Print various debugging information
|
||||||
--dump-pages print downloaded pages to debug problems (very verbose)
|
--dump-pages Print downloaded pages encoded using base64 to debug problems (very verbose)
|
||||||
--write-pages Write downloaded intermediary pages to files in the current directory to debug problems
|
--write-pages Write downloaded intermediary pages to files in the current directory to debug problems
|
||||||
--print-traffic Display sent and read HTTP traffic
|
--print-traffic Display sent and read HTTP traffic
|
||||||
-C, --call-home Contact the youtube-dl server for debugging.
|
-C, --call-home Contact the youtube-dl server for debugging
|
||||||
--no-call-home Do NOT contact the youtube-dl server for debugging.
|
--no-call-home Do NOT contact the youtube-dl server for debugging
|
||||||
|
|
||||||
## Workarounds:
|
## Workarounds:
|
||||||
--encoding ENCODING Force the specified encoding (experimental)
|
--encoding ENCODING Force the specified encoding (experimental)
|
||||||
--no-check-certificate Suppress HTTPS certificate validation.
|
--no-check-certificate Suppress HTTPS certificate validation
|
||||||
--prefer-insecure Use an unencrypted connection to retrieve information about the video. (Currently supported only for YouTube)
|
--prefer-insecure Use an unencrypted connection to retrieve information about the video. (Currently supported only for YouTube)
|
||||||
--user-agent UA specify a custom user agent
|
--user-agent UA Specify a custom user agent
|
||||||
--referer URL specify a custom referer, use if the video access is restricted to one domain
|
--referer URL Specify a custom referer, use if the video access is restricted to one domain
|
||||||
--add-header FIELD:VALUE specify a custom HTTP header and its value, separated by a colon ':'. You can use this option multiple times
|
--add-header FIELD:VALUE Specify a custom HTTP header and its value, separated by a colon ':'. You can use this option multiple times
|
||||||
--bidi-workaround Work around terminals that lack bidirectional text support. Requires bidiv or fribidi executable in PATH
|
--bidi-workaround Work around terminals that lack bidirectional text support. Requires bidiv or fribidi executable in PATH
|
||||||
--sleep-interval SECONDS Number of seconds to sleep before each download.
|
--sleep-interval SECONDS Number of seconds to sleep before each download.
|
||||||
|
|
||||||
## Video Format Options:
|
## Video Format Options:
|
||||||
-f, --format FORMAT video format code, specify the order of preference using slashes, as in -f 22/17/18 . Instead of format codes, you can select by
|
-f, --format FORMAT Video format code, see the "FORMAT SELECTION" for all the info
|
||||||
extension for the extensions aac, m4a, mp3, mp4, ogg, wav, webm. You can also use the special names "best", "bestvideo", "bestaudio",
|
--all-formats Download all available video formats
|
||||||
"worst". You can filter the video results by putting a condition in brackets, as in -f "best[height=720]" (or -f "[filesize>10M]").
|
--prefer-free-formats Prefer free video formats unless a specific one is requested
|
||||||
This works for filesize, height, width, tbr, abr, vbr, asr, and fps and the comparisons <, <=, >, >=, =, != and for ext, acodec,
|
-F, --list-formats List all available formats
|
||||||
vcodec, container, and protocol and the comparisons =, != . Formats for which the value is not known are excluded unless you put a
|
--youtube-skip-dash-manifest Do not download the DASH manifests and related data on YouTube videos
|
||||||
question mark (?) after the operator. You can combine format filters, so -f "[height <=? 720][tbr>500]" selects up to 720p videos
|
--merge-output-format FORMAT If a merge is required (e.g. bestvideo+bestaudio), output to given container format. One of mkv, mp4, ogg, webm, flv. Ignored if no
|
||||||
(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
|
|
||||||
--max-quality FORMAT highest quality format to download
|
|
||||||
-F, --list-formats list all available formats
|
|
||||||
--youtube-skip-dash-manifest Do not download the DASH manifest on YouTube videos
|
|
||||||
--merge-output-format FORMAT If a merge is required (e.g. bestvideo+bestaudio), output to given container format. One of mkv, mp4, ogg, webm, flv.Ignored if no
|
|
||||||
merge is required
|
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 only)
|
--write-auto-sub Write automatic subtitle file (YouTube only)
|
||||||
--all-subs downloads all the available subtitles of the video
|
--all-subs Download all the available subtitles of the video
|
||||||
--list-subs lists all available subtitles for the video
|
--list-subs List all available subtitles for the video
|
||||||
--sub-format FORMAT subtitle format, accepts formats preference, for example: "ass/srt/best"
|
--sub-format FORMAT Subtitle format, accepts formats preference, for example: "srt" or "ass/srt/best"
|
||||||
--sub-lang LANGS languages of the subtitles to download (optional) separated by commas, use IETF language tags like 'en,pt'
|
--sub-lang LANGS Languages of the subtitles to download (optional) separated by commas, use IETF language tags like 'en,pt'
|
||||||
|
|
||||||
## 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 out, youtube-dl will ask interactively.
|
-p, --password PASSWORD Account password. If this option is left 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 (requires ffmpeg or avconv and ffprobe or avprobe)
|
-x, --extract-audio Convert video files to audio-only files (requires ffmpeg or avconv and ffprobe or avprobe)
|
||||||
--audio-format FORMAT "best", "aac", "vorbis", "mp3", "m4a", "opus", or "wav"; "best" by default
|
--audio-format FORMAT Specify audio format: "best", "aac", "vorbis", "mp3", "m4a", "opus", or "wav"; "best" by default
|
||||||
--audio-quality QUALITY ffmpeg/avconv audio quality specification, insert a value between 0 (better) and 9 (worse) for VBR or a specific bitrate like 128K
|
--audio-quality QUALITY Specify ffmpeg/avconv audio quality, insert a value between 0 (better) and 9 (worse) for VBR or a specific bitrate like 128K (default
|
||||||
(default 5)
|
5)
|
||||||
--recode-video FORMAT Encode the video to another format if necessary (currently supported: mp4|flv|ogg|webm|mkv)
|
--recode-video FORMAT Encode the video to another format if necessary (currently supported: mp4|flv|ogg|webm|mkv|avi)
|
||||||
-k, --keep-video keeps the video file on disk after the post-processing; the video is erased by default
|
--postprocessor-args ARGS Give these arguments to the postprocessor
|
||||||
--no-post-overwrites do not overwrite post-processed files; the post-processed files are overwritten by default
|
-k, --keep-video Keep the video file on disk after the post-processing; the video is erased by default
|
||||||
--embed-subs embed subtitles in the video (only for mp4 videos)
|
--no-post-overwrites Do not overwrite post-processed files; the post-processed files are overwritten by default
|
||||||
--embed-thumbnail embed thumbnail in the audio as cover art
|
--embed-subs Embed subtitles in the video (only for mkv and mp4 videos)
|
||||||
--add-metadata write metadata to the video file
|
--embed-thumbnail Embed thumbnail in the audio as cover art
|
||||||
--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
|
--add-metadata Write metadata to the video file
|
||||||
parameters replace existing values. Additional templates: %(album), %(artist). Example: --metadata-from-title "%(artist)s -
|
--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"
|
%(title)s" matches a title like "Coldplay - Paradise"
|
||||||
--xattrs write metadata to the video file's xattrs (using dublin core and xdg standards)
|
--xattrs Write metadata to the video file's xattrs (using dublin core and xdg standards)
|
||||||
--fixup POLICY Automatically correct known faults of the file. One of never (do nothing), warn (only emit a warning), detect_or_warn(the default;
|
--fixup POLICY Automatically correct known faults of the file. One of never (do nothing), warn (only emit a warning), detect_or_warn (the default;
|
||||||
fix file if we can, warn otherwise)
|
fix file if we can, warn otherwise)
|
||||||
--prefer-avconv Prefer avconv over ffmpeg for running the postprocessors (default)
|
--prefer-avconv Prefer avconv over ffmpeg for running the postprocessors (default)
|
||||||
--prefer-ffmpeg Prefer ffmpeg over avconv for running the postprocessors
|
--prefer-ffmpeg Prefer ffmpeg over avconv for running the postprocessors
|
||||||
@@ -243,7 +236,34 @@ which means you can modify it, redistribute it or use it however you like.
|
|||||||
|
|
||||||
# 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, system wide configuration file is located at `/etc/youtube-dl.conf` and 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 configration file youtube-dl will always extract the audio, not copy the mtime and use proxy:
|
||||||
|
```
|
||||||
|
--extract-audio
|
||||||
|
--no-mtime
|
||||||
|
--proxy 127.0.0.1:3128
|
||||||
|
```
|
||||||
|
|
||||||
|
You can use `--ignore-config` if you want to disable 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 shell command history. You can achieve this using [`.netrc` file](http://stackoverflow.com/tags/.netrc/info) on per extractor basis. For that you will need to create `.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 `.netrc` file you should pass `--netrc` to youtube-dl or to place it in [configuration file](#configuration).
|
||||||
|
|
||||||
|
On Windows you may also need to setup `%HOME%` environment variable manually.
|
||||||
|
|
||||||
# OUTPUT TEMPLATE
|
# OUTPUT TEMPLATE
|
||||||
|
|
||||||
@@ -259,6 +279,7 @@ The `-o` option allows users to indicate a template for the output file names. T
|
|||||||
- `autonumber`: The sequence will be replaced by a five-digit number that will be increased with each download, starting at zero.
|
- `autonumber`: The sequence will be replaced by a five-digit number that will be increased with each download, starting at zero.
|
||||||
- `playlist`: The name or the id of the playlist that contains the video.
|
- `playlist`: The name or the id of the playlist that contains the video.
|
||||||
- `playlist_index`: The index of the video in the playlist, a five-digit number.
|
- `playlist_index`: The index of the video in the playlist, a five-digit number.
|
||||||
|
- `format_id`: The sequence will be replaced by the format code specified by `--format`.
|
||||||
|
|
||||||
The current default template is `%(title)s-%(id)s.%(ext)s`.
|
The current default template is `%(title)s-%(id)s.%(ext)s`.
|
||||||
|
|
||||||
@@ -271,6 +292,17 @@ $ youtube-dl --get-filename -o "%(title)s.%(ext)s" BaW_jenozKc --restrict-filena
|
|||||||
youtube-dl_test_video_.mp4 # A simple file name
|
youtube-dl_test_video_.mp4 # A simple file name
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# FORMAT SELECTION
|
||||||
|
|
||||||
|
By default youtube-dl tries to download the best quality, but sometimes you may want to download other format.
|
||||||
|
The simplest case is requesting a specific format, for example `-f 22`. You can get the list of available formats using `--list-formats`, you can also use a file extension (currently it supports aac, m4a, mp3, mp4, ogg, wav, webm) or the special names `best`, `bestvideo`, `bestaudio` and `worst`.
|
||||||
|
|
||||||
|
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, as in `-f 22/17/18`. You can also 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. Use commas to download multiple 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`. 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 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 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.
|
||||||
|
|
||||||
# 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:
|
||||||
@@ -321,9 +353,9 @@ 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?
|
||||||
|
|
||||||
@@ -355,13 +387,29 @@ YouTube has switched to a new video info format in July 2011 which is not suppor
|
|||||||
|
|
||||||
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 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.
|
||||||
|
|
||||||
### HTTP Error 429: Too Many Requests or 402: Payment Required
|
### HTTP Error 429: Too Many Requests or 402: Payment Required
|
||||||
|
|
||||||
These two error codes indicate that the service is blocking your IP address because of overuse. Contact the service and ask them to unblock your IP address, or - if you have acquired a whitelisted IP address already - use the [`--proxy` or `--network-address` options](#network-options) to select another IP address.
|
These two error codes indicate that the service is blocking your IP address because of overuse. Contact the service and ask them to unblock your IP address, or - if you have acquired a whitelisted IP address already - use the [`--proxy` or `--source-address` options](#network-options) to select another IP address.
|
||||||
|
|
||||||
### SyntaxError: Non-ASCII character ###
|
### SyntaxError: Non-ASCII character ###
|
||||||
|
|
||||||
@@ -399,6 +447,12 @@ Either prepend `http://www.youtube.com/watch?v=` or separate the ID from the opt
|
|||||||
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 cookies file must be in Mozilla/Netscape format and the first line of 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 cookies file and convert newlines if necessary to correspond 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 particular extractor does not implement it explicitly.
|
||||||
|
|
||||||
### 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.
|
||||||
@@ -498,7 +552,7 @@ If you want to add support for a new site, you can follow this quick list (assum
|
|||||||
```
|
```
|
||||||
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 be 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:
|
||||||
|
|
||||||
@@ -526,7 +580,7 @@ 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#L117-L265). 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:
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,14 @@
|
|||||||
- **56.com**
|
- **56.com**
|
||||||
- **5min**
|
- **5min**
|
||||||
- **8tracks**
|
- **8tracks**
|
||||||
|
- **91porn**
|
||||||
- **9gag**
|
- **9gag**
|
||||||
- **abc.net.au**
|
- **abc.net.au**
|
||||||
- **Abc7News**
|
- **Abc7News**
|
||||||
- **AcademicEarth:Course**
|
- **AcademicEarth:Course**
|
||||||
- **AddAnime**
|
- **AddAnime**
|
||||||
- **AdobeTV**
|
- **AdobeTV**
|
||||||
|
- **AdobeTVVideo**
|
||||||
- **AdultSwim**
|
- **AdultSwim**
|
||||||
- **Aftenposten**
|
- **Aftenposten**
|
||||||
- **Aftonbladet**
|
- **Aftonbladet**
|
||||||
@@ -26,8 +28,8 @@
|
|||||||
- **anitube.se**
|
- **anitube.se**
|
||||||
- **AnySex**
|
- **AnySex**
|
||||||
- **Aparat**
|
- **Aparat**
|
||||||
- **AppleDailyAnimationNews**
|
- **AppleConnect**
|
||||||
- **AppleDailyRealtimeNews**
|
- **AppleDaily**: 臺灣蘋果日報
|
||||||
- **AppleTrailers**
|
- **AppleTrailers**
|
||||||
- **archive.org**: archive.org videos
|
- **archive.org**: archive.org videos
|
||||||
- **ARD**
|
- **ARD**
|
||||||
@@ -44,10 +46,12 @@
|
|||||||
- **audiomack**
|
- **audiomack**
|
||||||
- **audiomack:album**
|
- **audiomack:album**
|
||||||
- **Azubu**
|
- **Azubu**
|
||||||
|
- **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
|
||||||
- **BeatportPro**
|
- **BeatportPro**
|
||||||
- **Beeg**
|
- **Beeg**
|
||||||
@@ -63,6 +67,8 @@
|
|||||||
- **BR**: Bayerischer Rundfunk Mediathek
|
- **BR**: Bayerischer Rundfunk Mediathek
|
||||||
- **Break**
|
- **Break**
|
||||||
- **Brightcove**
|
- **Brightcove**
|
||||||
|
- **bt:article**: Bergens Tidende Articles
|
||||||
|
- **bt:vestlendingen**: Bergens Tidende - Vestlendingen
|
||||||
- **BuzzFeed**
|
- **BuzzFeed**
|
||||||
- **BYUtv**
|
- **BYUtv**
|
||||||
- **Camdemy**
|
- **Camdemy**
|
||||||
@@ -80,7 +86,7 @@
|
|||||||
- **chirbit:profile**
|
- **chirbit:profile**
|
||||||
- **Cinchcast**
|
- **Cinchcast**
|
||||||
- **Cinemassacre**
|
- **Cinemassacre**
|
||||||
- **clipfish**
|
- **Clipfish**
|
||||||
- **cliphunter**
|
- **cliphunter**
|
||||||
- **Clipsyndicate**
|
- **Clipsyndicate**
|
||||||
- **Cloudy**
|
- **Cloudy**
|
||||||
@@ -98,28 +104,35 @@
|
|||||||
- **CondeNast**: Condé Nast media group: Condé Nast, GQ, Glamour, Vanity Fair, Vogue, W Magazine, WIRED
|
- **CondeNast**: Condé Nast media group: Condé Nast, GQ, Glamour, 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**
|
||||||
- **dailymotion**
|
- **dailymotion**
|
||||||
- **dailymotion:playlist**
|
- **dailymotion:playlist**
|
||||||
- **dailymotion:user**
|
- **dailymotion:user**
|
||||||
|
- **DailymotionCloud**
|
||||||
- **daum.net**
|
- **daum.net**
|
||||||
- **DBTV**
|
- **DBTV**
|
||||||
|
- **DCN**
|
||||||
- **DctpTv**
|
- **DctpTv**
|
||||||
- **DeezerPlaylist**
|
- **DeezerPlaylist**
|
||||||
- **defense.gouv.fr**
|
- **defense.gouv.fr**
|
||||||
|
- **DHM**: Filmarchiv - Deutsches Historisches Museum
|
||||||
- **Discovery**
|
- **Discovery**
|
||||||
- **divxstage**: DivxStage
|
- **divxstage**: DivxStage
|
||||||
- **Dotsub**
|
- **Dotsub**
|
||||||
- **DouyuTV**
|
- **DouyuTV**: 斗鱼
|
||||||
|
- **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**
|
- **EaglePlatform**
|
||||||
- **EbaumsWorld**
|
- **EbaumsWorld**
|
||||||
@@ -136,6 +149,8 @@
|
|||||||
- **Eporner**
|
- **Eporner**
|
||||||
- **EroProfile**
|
- **EroProfile**
|
||||||
- **Escapist**
|
- **Escapist**
|
||||||
|
- **ESPN** (Currently broken)
|
||||||
|
- **EsriVideo**
|
||||||
- **EveryonesMixtape**
|
- **EveryonesMixtape**
|
||||||
- **exfm**: ex.fm
|
- **exfm**: ex.fm
|
||||||
- **ExpoTV**
|
- **ExpoTV**
|
||||||
@@ -145,13 +160,14 @@
|
|||||||
- **fc2**
|
- **fc2**
|
||||||
- **fernsehkritik.tv**
|
- **fernsehkritik.tv**
|
||||||
- **fernsehkritik.tv:postecke**
|
- **fernsehkritik.tv:postecke**
|
||||||
- **Firedrive**
|
|
||||||
- **Firstpost**
|
- **Firstpost**
|
||||||
|
- **FiveTV**
|
||||||
- **Flickr**
|
- **Flickr**
|
||||||
- **Folketinget**: Folketinget (ft.dk; Danish parliament)
|
- **Folketinget**: Folketinget (ft.dk; Danish parliament)
|
||||||
- **FootyRoom**
|
- **FootyRoom**
|
||||||
- **Foxgay**
|
- **Foxgay**
|
||||||
- **FoxNews**
|
- **FoxNews**: Fox News and Fox Business Video
|
||||||
|
- **FoxSports**
|
||||||
- **france2.fr:generation-quoi**
|
- **france2.fr:generation-quoi**
|
||||||
- **FranceCulture**
|
- **FranceCulture**
|
||||||
- **FranceInter**
|
- **FranceInter**
|
||||||
@@ -164,12 +180,14 @@
|
|||||||
- **Gamekings**
|
- **Gamekings**
|
||||||
- **GameOne**
|
- **GameOne**
|
||||||
- **gameone:playlist**
|
- **gameone:playlist**
|
||||||
|
- **Gamersyde**
|
||||||
- **GameSpot**
|
- **GameSpot**
|
||||||
- **GameStar**
|
- **GameStar**
|
||||||
- **Gametrailers**
|
- **Gametrailers**
|
||||||
- **Gazeta**
|
- **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)
|
||||||
@@ -177,9 +195,8 @@
|
|||||||
- **GodTube**
|
- **GodTube**
|
||||||
- **GoldenMoustache**
|
- **GoldenMoustache**
|
||||||
- **Golem**
|
- **Golem**
|
||||||
- **GorillaVid**: GorillaVid.in, daclips.in, movpod.in and fastvideo.in
|
- **GorillaVid**: GorillaVid.in, daclips.in, movpod.in, fastvideo.in, realvid.net and filehoot.com
|
||||||
- **Goshgay**
|
- **Goshgay**
|
||||||
- **Grooveshark**
|
|
||||||
- **Groupon**
|
- **Groupon**
|
||||||
- **Hark**
|
- **Hark**
|
||||||
- **HearThisAt**
|
- **HearThisAt**
|
||||||
@@ -203,12 +220,17 @@
|
|||||||
- **imdb**: Internet Movie Database trailers
|
- **imdb**: Internet Movie Database trailers
|
||||||
- **imdb:list**: Internet Movie Database lists
|
- **imdb:list**: Internet Movie Database lists
|
||||||
- **Imgur**
|
- **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
|
||||||
- **Izlesene**
|
- **Izlesene**
|
||||||
@@ -221,6 +243,7 @@
|
|||||||
- **KanalPlay**: Kanal 5/9/11 Play
|
- **KanalPlay**: Kanal 5/9/11 Play
|
||||||
- **Kankan**
|
- **Kankan**
|
||||||
- **Karaoketv**
|
- **Karaoketv**
|
||||||
|
- **KarriereVideos**
|
||||||
- **keek**
|
- **keek**
|
||||||
- **KeezMovies**
|
- **KeezMovies**
|
||||||
- **KhanAcademy**
|
- **KhanAcademy**
|
||||||
@@ -228,12 +251,20 @@
|
|||||||
- **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**
|
||||||
- **Letv**
|
- **Lecture2Go**
|
||||||
|
- **Letv**: 乐视网
|
||||||
- **LetvPlaylist**
|
- **LetvPlaylist**
|
||||||
- **LetvTv**
|
- **LetvTv**
|
||||||
- **Libsyn**
|
- **Libsyn**
|
||||||
|
- **life:embed**
|
||||||
- **lifenews**: LIFE | NEWS
|
- **lifenews**: LIFE | NEWS
|
||||||
- **LiveLeak**
|
- **LiveLeak**
|
||||||
- **livestream**
|
- **livestream**
|
||||||
@@ -248,11 +279,13 @@
|
|||||||
- **Malemotion**
|
- **Malemotion**
|
||||||
- **MDR**
|
- **MDR**
|
||||||
- **media.ccc.de**
|
- **media.ccc.de**
|
||||||
|
- **MegaVideoz**
|
||||||
- **metacafe**
|
- **metacafe**
|
||||||
- **Metacritic**
|
- **Metacritic**
|
||||||
- **Mgoon**
|
- **Mgoon**
|
||||||
- **Minhateca**
|
- **Minhateca**
|
||||||
- **MinistryGrid**
|
- **MinistryGrid**
|
||||||
|
- **miomio.tv**
|
||||||
- **mitele.es**
|
- **mitele.es**
|
||||||
- **mixcloud**
|
- **mixcloud**
|
||||||
- **MLB**
|
- **MLB**
|
||||||
@@ -265,36 +298,50 @@
|
|||||||
- **Motherless**
|
- **Motherless**
|
||||||
- **Motorsport**: motorsport.com
|
- **Motorsport**: motorsport.com
|
||||||
- **MovieClips**
|
- **MovieClips**
|
||||||
|
- **MovieFap**
|
||||||
- **Moviezine**
|
- **Moviezine**
|
||||||
- **movshare**: MovShare
|
- **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-JOY**
|
||||||
- **n-tv.de**
|
- **n-tv.de**
|
||||||
- **NationalGeographic**
|
- **NationalGeographic**
|
||||||
- **Naver**
|
- **Naver**
|
||||||
- **NBA**
|
- **NBA**
|
||||||
- **NBC**
|
- **NBC**
|
||||||
- **NBCNews**
|
- **NBCNews**
|
||||||
|
- **NBCSports**
|
||||||
|
- **NBCSportsVPlayer**
|
||||||
- **ndr**: NDR.de - Mediathek
|
- **ndr**: NDR.de - Mediathek
|
||||||
- **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**: 蘋果日報 - 動新聞
|
||||||
- **nfb**: National Film Board of Canada
|
- **nfb**: National Film Board of Canada
|
||||||
- **nfl.com**
|
- **nfl.com**
|
||||||
- **nhl.com**
|
- **nhl.com**
|
||||||
@@ -305,24 +352,29 @@
|
|||||||
- **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**
|
||||||
|
- **NowTV**
|
||||||
- **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**
|
||||||
- **NRK**
|
- **NRK**
|
||||||
- **NRKPlaylist**
|
- **NRKPlaylist**
|
||||||
- **NRKTV**
|
- **NRKTV**: NRK TV and NRK Radio
|
||||||
- **ntv.ru**
|
- **ntv.ru**
|
||||||
- **Nuvid**
|
- **Nuvid**
|
||||||
- **NYTimes**
|
- **NYTimes**
|
||||||
|
- **NYTimesArticle**
|
||||||
- **ocw.mit.edu**
|
- **ocw.mit.edu**
|
||||||
- **Odnoklassniki**
|
- **Odnoklassniki**
|
||||||
- **OktoberfestTV**
|
- **OktoberfestTV**
|
||||||
- **on.aol.com**
|
- **on.aol.com**
|
||||||
|
- **OnionStudios**
|
||||||
- **Ooyala**
|
- **Ooyala**
|
||||||
|
- **OoyalaExternal**
|
||||||
- **OpenFilm**
|
- **OpenFilm**
|
||||||
- **orf:fm4**: radio FM4
|
- **orf:fm4**: radio FM4
|
||||||
- **orf:iptv**: iptv.ORF.at
|
- **orf:iptv**: iptv.ORF.at
|
||||||
@@ -331,14 +383,20 @@
|
|||||||
- **parliamentlive.tv**: UK parliament videos
|
- **parliamentlive.tv**: UK parliament videos
|
||||||
- **Patreon**
|
- **Patreon**
|
||||||
- **PBS**
|
- **PBS**
|
||||||
|
- **Periscope**: Periscope
|
||||||
|
- **PhilharmonieDeParis**: Philharmonie de Paris
|
||||||
- **Phoenix**
|
- **Phoenix**
|
||||||
- **Photobucket**
|
- **Photobucket**
|
||||||
|
- **Pinkbike**
|
||||||
- **Pladform**
|
- **Pladform**
|
||||||
- **PlanetaPlay**
|
- **PlanetaPlay**
|
||||||
- **play.fm**
|
- **play.fm**
|
||||||
- **played.to**
|
- **played.to**
|
||||||
|
- **Playtvak**: Playtvak.cz, iDNES.cz and Lidovky.cz
|
||||||
- **Playvid**
|
- **Playvid**
|
||||||
- **Playwire**
|
- **Playwire**
|
||||||
|
- **pluralsight**
|
||||||
|
- **pluralsight:course**
|
||||||
- **plus.google**: Google Plus
|
- **plus.google**: Google Plus
|
||||||
- **pluzz.francetv.fr**
|
- **pluzz.francetv.fr**
|
||||||
- **podomatic**
|
- **podomatic**
|
||||||
@@ -346,19 +404,28 @@
|
|||||||
- **PornHub**
|
- **PornHub**
|
||||||
- **PornHubPlaylist**
|
- **PornHubPlaylist**
|
||||||
- **Pornotube**
|
- **Pornotube**
|
||||||
|
- **PornoVoisines**
|
||||||
- **PornoXO**
|
- **PornoXO**
|
||||||
- **PrimeShareTV**
|
- **PrimeShareTV**
|
||||||
- **PromptFile**
|
- **PromptFile**
|
||||||
- **prosiebensat1**: ProSiebenSat.1 Digital
|
- **prosiebensat1**: ProSiebenSat.1 Digital
|
||||||
- **Puls4**
|
- **Puls4**
|
||||||
- **Pyvideo**
|
- **Pyvideo**
|
||||||
|
- **qqmusic**: QQ音乐
|
||||||
|
- **qqmusic:album**: QQ音乐 - 专辑
|
||||||
|
- **qqmusic:playlist**: QQ音乐 - 歌单
|
||||||
|
- **qqmusic:singer**: QQ音乐 - 歌手
|
||||||
|
- **qqmusic:toplist**: QQ音乐 - 排行榜
|
||||||
|
- **Quickscope**: Quick Scope
|
||||||
- **QuickVid**
|
- **QuickVid**
|
||||||
- **R7**
|
- **R7**
|
||||||
- **radio.de**
|
- **radio.de**
|
||||||
- **radiobremen**
|
- **radiobremen**
|
||||||
- **radiofrance**
|
- **radiofrance**
|
||||||
|
- **RadioJavan**
|
||||||
- **Rai**
|
- **Rai**
|
||||||
- **RBMARadio**
|
- **RBMARadio**
|
||||||
|
- **RDS**: RDS.ca
|
||||||
- **RedTube**
|
- **RedTube**
|
||||||
- **Restudy**
|
- **Restudy**
|
||||||
- **ReverbNation**
|
- **ReverbNation**
|
||||||
@@ -369,12 +436,12 @@
|
|||||||
- **Rte**
|
- **Rte**
|
||||||
- **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:infantil**: RTVE infantil
|
||||||
- **rtve.es:live**: RTVE.es live streams
|
- **rtve.es:live**: RTVE.es live streams
|
||||||
|
- **RTVNH**
|
||||||
- **RUHD**
|
- **RUHD**
|
||||||
- **rutube**: Rutube videos
|
- **rutube**: Rutube videos
|
||||||
- **rutube:channel**: Rutube channels
|
- **rutube:channel**: Rutube channels
|
||||||
@@ -382,6 +449,7 @@
|
|||||||
- **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**: safaribooksonline.com online video
|
||||||
- **safari:course**: safaribooksonline.com online courses
|
- **safari:course**: safaribooksonline.com online courses
|
||||||
- **Sandia**: Sandia National Laboratories
|
- **Sandia**: Sandia National Laboratories
|
||||||
@@ -393,10 +461,12 @@
|
|||||||
- **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**
|
||||||
- **Slideshare**
|
- **Slideshare**
|
||||||
@@ -405,9 +475,12 @@
|
|||||||
- **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**
|
||||||
|
- **soompi**
|
||||||
|
- **soompi:show**
|
||||||
- **soundcloud**
|
- **soundcloud**
|
||||||
- **soundcloud:playlist**
|
- **soundcloud:playlist**
|
||||||
- **soundcloud:set**
|
- **soundcloud:set**
|
||||||
@@ -415,8 +488,12 @@
|
|||||||
- **soundgasm**
|
- **soundgasm**
|
||||||
- **soundgasm:profile**
|
- **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
|
||||||
@@ -424,7 +501,10 @@
|
|||||||
- **Spike**
|
- **Spike**
|
||||||
- **Sport5**
|
- **Sport5**
|
||||||
- **SportBox**
|
- **SportBox**
|
||||||
|
- **SportBoxEmbed**
|
||||||
- **SportDeutschland**
|
- **SportDeutschland**
|
||||||
|
- **Sportschau**
|
||||||
|
- **Srf**
|
||||||
- **SRMediathek**: Saarländischer Rundfunk
|
- **SRMediathek**: Saarländischer Rundfunk
|
||||||
- **SSA**
|
- **SSA**
|
||||||
- **stanfordoc**: Stanford Open ClassRoom
|
- **stanfordoc**: Stanford Open ClassRoom
|
||||||
@@ -433,6 +513,7 @@
|
|||||||
- **StreamCZ**
|
- **StreamCZ**
|
||||||
- **StreetVoice**
|
- **StreetVoice**
|
||||||
- **SunPorno**
|
- **SunPorno**
|
||||||
|
- **SVT**
|
||||||
- **SVTPlay**: SVT Play and Öppet arkiv
|
- **SVTPlay**: SVT Play and Öppet arkiv
|
||||||
- **SWRMediathek**
|
- **SWRMediathek**
|
||||||
- **Syfy**
|
- **Syfy**
|
||||||
@@ -447,10 +528,10 @@
|
|||||||
- **TeamFour**
|
- **TeamFour**
|
||||||
- **TechTalks**
|
- **TechTalks**
|
||||||
- **techtv.mit.edu**
|
- **techtv.mit.edu**
|
||||||
- **TED**
|
- **ted**
|
||||||
- **tegenlicht.vpro.nl**
|
|
||||||
- **TeleBruxelles**
|
- **TeleBruxelles**
|
||||||
- **telecinco.es**
|
- **telecinco.es**
|
||||||
|
- **Telegraaf**
|
||||||
- **TeleMB**
|
- **TeleMB**
|
||||||
- **TeleTask**
|
- **TeleTask**
|
||||||
- **TenPlay**
|
- **TenPlay**
|
||||||
@@ -458,7 +539,9 @@
|
|||||||
- **TF1**
|
- **TF1**
|
||||||
- **TheOnion**
|
- **TheOnion**
|
||||||
- **ThePlatform**
|
- **ThePlatform**
|
||||||
|
- **ThePlatformFeed**
|
||||||
- **TheSixtyOne**
|
- **TheSixtyOne**
|
||||||
|
- **ThisAmericanLife**
|
||||||
- **ThisAV**
|
- **ThisAV**
|
||||||
- **THVideo**
|
- **THVideo**
|
||||||
- **THVideoPlaylist**
|
- **THVideoPlaylist**
|
||||||
@@ -466,6 +549,7 @@
|
|||||||
- **tlc.com**
|
- **tlc.com**
|
||||||
- **tlc.de**
|
- **tlc.de**
|
||||||
- **TMZ**
|
- **TMZ**
|
||||||
|
- **TMZArticle**
|
||||||
- **TNAFlix**
|
- **TNAFlix**
|
||||||
- **tou.tv**
|
- **tou.tv**
|
||||||
- **Toypics**: Toypics user profile
|
- **Toypics**: Toypics user profile
|
||||||
@@ -474,13 +558,18 @@
|
|||||||
- **Trilulilu**
|
- **Trilulilu**
|
||||||
- **TruTube**
|
- **TruTube**
|
||||||
- **Tube8**
|
- **Tube8**
|
||||||
|
- **TubiTv**
|
||||||
- **Tudou**
|
- **Tudou**
|
||||||
- **Tumblr**
|
- **Tumblr**
|
||||||
- **TuneIn**
|
- **TuneIn**
|
||||||
- **Turbo**
|
- **Turbo**
|
||||||
- **Tutv**
|
- **Tutv**
|
||||||
- **tv.dfb.de**
|
- **tv.dfb.de**
|
||||||
|
- **TV2**
|
||||||
|
- **TV2Article**
|
||||||
- **TV4**: tv4.se and tv4play.se
|
- **TV4**: tv4.se and tv4play.se
|
||||||
|
- **TVC**
|
||||||
|
- **TVCArticle**
|
||||||
- **tvigle**: Интернет-телевидение Tvigle.ru
|
- **tvigle**: Интернет-телевидение Tvigle.ru
|
||||||
- **tvp.pl**
|
- **tvp.pl**
|
||||||
- **tvp.pl:Series**
|
- **tvp.pl:Series**
|
||||||
@@ -493,27 +582,29 @@
|
|||||||
- **twitch:stream**
|
- **twitch:stream**
|
||||||
- **twitch:video**
|
- **twitch:video**
|
||||||
- **twitch:vod**
|
- **twitch:vod**
|
||||||
|
- **TwitterCard**
|
||||||
- **Ubu**
|
- **Ubu**
|
||||||
- **udemy**
|
- **udemy**
|
||||||
- **udemy:course**
|
- **udemy:course**
|
||||||
|
- **UDNEmbed**: 聯合影音
|
||||||
- **Ultimedia**
|
- **Ultimedia**
|
||||||
- **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**
|
- **Vessel**
|
||||||
- **Vesti**: Вести.Ru
|
- **Vesti**: Вести.Ru
|
||||||
- **Vevo**
|
- **Vevo**
|
||||||
- **VGTV**
|
- **VGTV**: VGTV and BTTV
|
||||||
- **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**
|
- **videolectures.net**
|
||||||
@@ -527,6 +618,7 @@
|
|||||||
- **vier:videos**
|
- **vier:videos**
|
||||||
- **Viewster**
|
- **Viewster**
|
||||||
- **viki**
|
- **viki**
|
||||||
|
- **viki:channel**
|
||||||
- **vimeo**
|
- **vimeo**
|
||||||
- **vimeo:album**
|
- **vimeo:album**
|
||||||
- **vimeo:channel**
|
- **vimeo:channel**
|
||||||
@@ -535,13 +627,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**
|
||||||
@@ -554,31 +649,36 @@
|
|||||||
- **wdr:mobile**
|
- **wdr:mobile**
|
||||||
- **WDRMaus**: Sendung mit der Maus
|
- **WDRMaus**: Sendung mit der Maus
|
||||||
- **WebOfStories**
|
- **WebOfStories**
|
||||||
|
- **WebOfStoriesPlaylist**
|
||||||
- **Weibo**
|
- **Weibo**
|
||||||
- **Wimp**
|
- **Wimp**
|
||||||
- **Wistia**
|
- **Wistia**
|
||||||
|
- **WNL**
|
||||||
- **WorldStarHipHop**
|
- **WorldStarHipHop**
|
||||||
- **wrzuta.pl**
|
- **wrzuta.pl**
|
||||||
- **WSJ**: Wall Street Journal
|
- **WSJ**: Wall Street Journal
|
||||||
- **XBef**
|
- **XBef**
|
||||||
- **XboxClips**
|
- **XboxClips**
|
||||||
- **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:album**: Яндекс.Музыка - Альбом
|
||||||
- **yandexmusic:playlist**: Яндекс.Музыка - Плейлист
|
- **yandexmusic:playlist**: Яндекс.Музыка - Плейлист
|
||||||
- **yandexmusic:track**: Яндекс.Музыка - Трек
|
- **yandexmusic:track**: Яндекс.Музыка - Трек
|
||||||
- **YesJapan**
|
- **YesJapan**
|
||||||
|
- **yinyuetai:video**: 音悦Tai
|
||||||
- **Ynet**
|
- **Ynet**
|
||||||
- **YouJizz**
|
- **YouJizz**
|
||||||
- **Youku**
|
- **youku**: 优酷
|
||||||
- **YouPorn**
|
- **YouPorn**
|
||||||
- **YourUpload**
|
- **YourUpload**
|
||||||
- **youtube**: YouTube.com
|
- **youtube**: YouTube.com
|
||||||
|
|||||||
@@ -133,8 +133,8 @@ def expect_info_dict(self, got_dict, expected_dict):
|
|||||||
elif isinstance(expected, compat_str) and expected.startswith('mincount:'):
|
elif isinstance(expected, compat_str) and expected.startswith('mincount:'):
|
||||||
got = got_dict.get(info_field)
|
got = got_dict.get(info_field)
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
isinstance(got, list),
|
isinstance(got, (list, dict)),
|
||||||
'Expected field %s to be a list, but it is of type %s' % (
|
'Expected field %s to be a list or a dict, but it is of type %s' % (
|
||||||
info_field, type(got).__name__))
|
info_field, type(got).__name__))
|
||||||
expected_num = int(expected.partition(':')[2])
|
expected_num = int(expected.partition(':')[2])
|
||||||
assertGreaterEqual(
|
assertGreaterEqual(
|
||||||
@@ -150,7 +150,7 @@ def expect_info_dict(self, got_dict, expected_dict):
|
|||||||
'invalid value for field %s, expected %r, got %r' % (info_field, expected, got))
|
'invalid value for field %s, expected %r, got %r' % (info_field, expected, got))
|
||||||
|
|
||||||
# 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 +160,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,
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ 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
|
||||||
from youtube_dl.extractor import YoutubeIE
|
from youtube_dl.extractor import YoutubeIE
|
||||||
from youtube_dl.postprocessor.common import PostProcessor
|
from youtube_dl.postprocessor.common import PostProcessor
|
||||||
from youtube_dl.utils import match_filter_func
|
from youtube_dl.utils import ExtractorError, match_filter_func
|
||||||
|
|
||||||
TEST_URL = 'http://localhost/sample.mp4'
|
TEST_URL = 'http://localhost/sample.mp4'
|
||||||
|
|
||||||
@@ -101,42 +102,10 @@ 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': TEST_URL},
|
{'format_id': '35', 'ext': 'mp4', 'preference': 1, 'url': TEST_URL},
|
||||||
|
{'format_id': 'example-with-dashes', 'ext': 'webm', 'preference': 1, 'url': TEST_URL},
|
||||||
{'format_id': '45', 'ext': 'webm', 'preference': 2, 'url': TEST_URL},
|
{'format_id': '45', 'ext': 'webm', 'preference': 2, 'url': TEST_URL},
|
||||||
{'format_id': '47', 'ext': 'webm', 'preference': 3, 'url': TEST_URL},
|
{'format_id': '47', 'ext': 'webm', 'preference': 3, 'url': TEST_URL},
|
||||||
{'format_id': '2', 'ext': 'flv', 'preference': 4, 'url': TEST_URL},
|
{'format_id': '2', 'ext': 'flv', 'preference': 4, 'url': TEST_URL},
|
||||||
@@ -168,6 +137,11 @@ 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': TEST_URL},
|
{'format_id': 'audio-low', 'ext': 'webm', 'preference': 1, 'vcodec': 'none', 'url': TEST_URL},
|
||||||
@@ -261,29 +235,89 @@ 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
|
info['format_id'] = f_id
|
||||||
f1['url'] = 'url:' + f1id
|
info['url'] = 'url:' + f_id
|
||||||
f2 = YoutubeIE._formats[f2id].copy()
|
return info
|
||||||
f2['format_id'] = f2id
|
formats_order = [format_info(f_id) for f_id in order]
|
||||||
f2['url'] = 'url:' + f2id
|
|
||||||
|
|
||||||
|
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 = [
|
||||||
@@ -340,6 +374,18 @@ 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):
|
class TestYoutubeDL(unittest.TestCase):
|
||||||
def test_subtitles(self):
|
def test_subtitles(self):
|
||||||
@@ -443,27 +489,36 @@ class TestYoutubeDL(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):
|
def test_match_filter(self):
|
||||||
class FilterYDL(YDL):
|
class FilterYDL(YDL):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -531,6 +586,51 @@ class TestYoutubeDL(unittest.TestCase):
|
|||||||
res = get_videos(f)
|
res = get_videos(f)
|
||||||
self.assertEqual(res, ['1'])
|
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, [])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class TestAES(unittest.TestCase):
|
|||||||
encrypted = base64.b64encode(
|
encrypted = base64.b64encode(
|
||||||
intlist_to_bytes(self.iv[:8]) +
|
intlist_to_bytes(self.iv[:8]) +
|
||||||
b'\x17\x15\x93\xab\x8d\x80V\xcdV\xe0\t\xcdo\xc2\xa5\xd8ksM\r\xe27N\xae'
|
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))
|
decrypted = (aes_decrypt_text(encrypted, password, 16))
|
||||||
self.assertEqual(decrypted, self.secret_msg)
|
self.assertEqual(decrypted, self.secret_msg)
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ class TestAES(unittest.TestCase):
|
|||||||
encrypted = base64.b64encode(
|
encrypted = base64.b64encode(
|
||||||
intlist_to_bytes(self.iv[:8]) +
|
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'
|
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))
|
decrypted = (aes_decrypt_text(encrypted, password, 32))
|
||||||
self.assertEqual(decrypted, self.secret_msg)
|
self.assertEqual(decrypted, self.secret_msg)
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ from youtube_dl.utils import get_filesystem_encoding
|
|||||||
from youtube_dl.compat import (
|
from youtube_dl.compat import (
|
||||||
compat_getenv,
|
compat_getenv,
|
||||||
compat_expanduser,
|
compat_expanduser,
|
||||||
|
compat_shlex_split,
|
||||||
|
compat_urllib_parse_unquote,
|
||||||
|
compat_urllib_parse_unquote_plus,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -42,5 +45,31 @@ 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'])
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -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', {}))
|
||||||
|
|
||||||
|
|||||||
@@ -8,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__)))
|
||||||
|
|
||||||
@@ -31,7 +34,7 @@ class TestExecution(unittest.TestCase):
|
|||||||
|
|
||||||
def test_cmdline_umlauts(self):
|
def test_cmdline_umlauts(self):
|
||||||
p = subprocess.Popen(
|
p = subprocess.Popen(
|
||||||
[sys.executable, 'youtube_dl/__main__.py', 'ä', '--version'],
|
[sys.executable, 'youtube_dl/__main__.py', encodeArgument('ä'), '--version'],
|
||||||
cwd=rootDir, stdout=_DEV_NULL, stderr=subprocess.PIPE)
|
cwd=rootDir, stdout=_DEV_NULL, stderr=subprocess.PIPE)
|
||||||
_, stderr = p.communicate()
|
_, stderr = p.communicate()
|
||||||
self.assertFalse(stderr)
|
self.assertFalse(stderr)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from youtube_dl.extractor import (
|
|||||||
RaiIE,
|
RaiIE,
|
||||||
VikiIE,
|
VikiIE,
|
||||||
ThePlatformIE,
|
ThePlatformIE,
|
||||||
|
ThePlatformFeedIE,
|
||||||
RTVEALaCartaIE,
|
RTVEALaCartaIE,
|
||||||
FunnyOrDieIE,
|
FunnyOrDieIE,
|
||||||
)
|
)
|
||||||
@@ -266,7 +267,7 @@ class TestNRKSubtitles(BaseTestSubtitles):
|
|||||||
self.DL.params['allsubtitles'] = True
|
self.DL.params['allsubtitles'] = True
|
||||||
subtitles = self.getSubtitles()
|
subtitles = self.getSubtitles()
|
||||||
self.assertEqual(set(subtitles.keys()), set(['no']))
|
self.assertEqual(set(subtitles.keys()), set(['no']))
|
||||||
self.assertEqual(md5(subtitles['no']), '1d221e6458c95c5494dcd38e6a1f129a')
|
self.assertEqual(md5(subtitles['no']), '544fa917d3197fcbee64634559221cc2')
|
||||||
|
|
||||||
|
|
||||||
class TestRaiSubtitles(BaseTestSubtitles):
|
class TestRaiSubtitles(BaseTestSubtitles):
|
||||||
@@ -307,6 +308,18 @@ class TestThePlatformSubtitles(BaseTestSubtitles):
|
|||||||
self.assertEqual(md5(subtitles['en']), '97e7670cbae3c4d26ae8bcc7fdd78d4b')
|
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):
|
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/'
|
url = 'http://www.rtve.es/alacarta/videos/los-misterios-de-laura/misterios-laura-capitulo-32-misterio-del-numero-17-2-parte/2428621/'
|
||||||
IE = RTVEALaCartaIE
|
IE = RTVEALaCartaIE
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ from youtube_dl.utils import (
|
|||||||
read_batch_urls,
|
read_batch_urls,
|
||||||
sanitize_filename,
|
sanitize_filename,
|
||||||
sanitize_path,
|
sanitize_path,
|
||||||
sanitize_url_path_consecutive_slashes,
|
prepend_extension,
|
||||||
|
replace_extension,
|
||||||
shell_quote,
|
shell_quote,
|
||||||
smuggle_url,
|
smuggle_url,
|
||||||
str_to_int,
|
str_to_int,
|
||||||
@@ -51,13 +52,21 @@ 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_text,
|
||||||
|
xpath_attr,
|
||||||
render_table,
|
render_table,
|
||||||
match_str,
|
match_str,
|
||||||
|
parse_dfxp_time_expr,
|
||||||
|
dfxp2srt,
|
||||||
|
cli_option,
|
||||||
|
cli_valueless_option,
|
||||||
|
cli_bool_option,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -171,25 +180,21 @@ class TestUtil(unittest.TestCase):
|
|||||||
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_sanitize_url_path_consecutive_slashes(self):
|
def test_prepend_extension(self):
|
||||||
self.assertEqual(
|
self.assertEqual(prepend_extension('abc.ext', 'temp'), 'abc.temp.ext')
|
||||||
sanitize_url_path_consecutive_slashes('http://hostname/foo//bar/filename.html'),
|
self.assertEqual(prepend_extension('abc.ext', 'temp', 'ext'), 'abc.temp.ext')
|
||||||
'http://hostname/foo/bar/filename.html')
|
self.assertEqual(prepend_extension('abc.unexpected_ext', 'temp', 'ext'), 'abc.unexpected_ext.temp')
|
||||||
self.assertEqual(
|
self.assertEqual(prepend_extension('abc', 'temp'), 'abc.temp')
|
||||||
sanitize_url_path_consecutive_slashes('http://hostname//foo/bar/filename.html'),
|
self.assertEqual(prepend_extension('.abc', 'temp'), '.abc.temp')
|
||||||
'http://hostname/foo/bar/filename.html')
|
self.assertEqual(prepend_extension('.abc.ext', 'temp'), '.abc.temp.ext')
|
||||||
self.assertEqual(
|
|
||||||
sanitize_url_path_consecutive_slashes('http://hostname//'),
|
def test_replace_extension(self):
|
||||||
'http://hostname/')
|
self.assertEqual(replace_extension('abc.ext', 'temp'), 'abc.temp')
|
||||||
self.assertEqual(
|
self.assertEqual(replace_extension('abc.ext', 'temp', 'ext'), 'abc.temp')
|
||||||
sanitize_url_path_consecutive_slashes('http://hostname/foo/bar/filename.html'),
|
self.assertEqual(replace_extension('abc.unexpected_ext', 'temp', 'ext'), 'abc.unexpected_ext.temp')
|
||||||
'http://hostname/foo/bar/filename.html')
|
self.assertEqual(replace_extension('abc', 'temp'), 'abc.temp')
|
||||||
self.assertEqual(
|
self.assertEqual(replace_extension('.abc', 'temp'), '.abc.temp')
|
||||||
sanitize_url_path_consecutive_slashes('http://hostname/'),
|
self.assertEqual(replace_extension('.abc.ext', 'temp'), '.abc.temp')
|
||||||
'http://hostname/')
|
|
||||||
self.assertEqual(
|
|
||||||
sanitize_url_path_consecutive_slashes('http://hostname/abc//'),
|
|
||||||
'http://hostname/abc/')
|
|
||||||
|
|
||||||
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])
|
||||||
@@ -227,6 +232,7 @@ 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')
|
||||||
|
|
||||||
def test_find_xpath_attr(self):
|
def test_find_xpath_attr(self):
|
||||||
testxml = '''<root>
|
testxml = '''<root>
|
||||||
@@ -234,12 +240,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 = xml.etree.ElementTree.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/">
|
||||||
@@ -254,6 +269,16 @@ class TestUtil(unittest.TestCase):
|
|||||||
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/bar', default='default'), 'default')
|
||||||
|
self.assertTrue(xpath_element(doc, 'div/bar') is None)
|
||||||
|
self.assertRaises(ExtractorError, xpath_element, doc, 'div/bar', fatal=True)
|
||||||
|
|
||||||
def test_xpath_text(self):
|
def test_xpath_text(self):
|
||||||
testxml = '''<root>
|
testxml = '''<root>
|
||||||
<div>
|
<div>
|
||||||
@@ -262,9 +287,25 @@ class TestUtil(unittest.TestCase):
|
|||||||
</root>'''
|
</root>'''
|
||||||
doc = xml.etree.ElementTree.fromstring(testxml)
|
doc = xml.etree.ElementTree.fromstring(testxml)
|
||||||
self.assertEqual(xpath_text(doc, 'div/p'), 'Foo')
|
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.assertTrue(xpath_text(doc, 'div/bar') is None)
|
||||||
self.assertRaises(ExtractorError, xpath_text, doc, 'div/bar', fatal=True)
|
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 = xml.etree.ElementTree.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'
|
||||||
@@ -323,6 +364,7 @@ class TestUtil(unittest.TestCase):
|
|||||||
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('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(
|
||||||
@@ -397,6 +439,10 @@ class TestUtil(unittest.TestCase):
|
|||||||
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')
|
||||||
@@ -470,6 +516,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"')
|
||||||
@@ -574,6 +626,102 @@ 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), 0.0)
|
||||||
|
self.assertEqual(parse_dfxp_time_expr(''), 0.0)
|
||||||
|
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)
|
||||||
|
|
||||||
|
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>
|
||||||
|
</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']
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
2
tox.ini
2
tox.ini
@@ -4,6 +4,8 @@ envlist = py26,py27,py33,py34
|
|||||||
deps =
|
deps =
|
||||||
nose
|
nose
|
||||||
coverage
|
coverage
|
||||||
|
# We need a valid $HOME for test_compat_expanduser
|
||||||
|
passenv = HOME
|
||||||
defaultargs = test --exclude test_download.py --exclude test_age_restriction.py
|
defaultargs = test --exclude test_download.py --exclude test_age_restriction.py
|
||||||
--exclude test_subtitles.py --exclude test_write_annotations.py
|
--exclude test_subtitles.py --exclude test_write_annotations.py
|
||||||
--exclude test_youtube_lists.py
|
--exclude test_youtube_lists.py
|
||||||
|
|||||||
@@ -21,24 +21,24 @@ import subprocess
|
|||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import tokenize
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
import ctypes
|
import ctypes
|
||||||
|
|
||||||
from .compat import (
|
from .compat import (
|
||||||
compat_basestring,
|
|
||||||
compat_cookiejar,
|
compat_cookiejar,
|
||||||
compat_expanduser,
|
compat_expanduser,
|
||||||
compat_get_terminal_size,
|
compat_get_terminal_size,
|
||||||
compat_http_client,
|
compat_http_client,
|
||||||
compat_kwargs,
|
compat_kwargs,
|
||||||
compat_str,
|
compat_str,
|
||||||
|
compat_tokenize_tokenize,
|
||||||
compat_urllib_error,
|
compat_urllib_error,
|
||||||
compat_urllib_request,
|
compat_urllib_request,
|
||||||
)
|
)
|
||||||
from .utils import (
|
from .utils import (
|
||||||
escape_url,
|
|
||||||
ContentTooShortError,
|
ContentTooShortError,
|
||||||
date_from_str,
|
date_from_str,
|
||||||
DateRange,
|
DateRange,
|
||||||
@@ -64,14 +64,15 @@ from .utils import (
|
|||||||
sanitize_path,
|
sanitize_path,
|
||||||
std_headers,
|
std_headers,
|
||||||
subtitles_filename,
|
subtitles_filename,
|
||||||
takewhile_inclusive,
|
|
||||||
UnavailableVideoError,
|
UnavailableVideoError,
|
||||||
url_basename,
|
url_basename,
|
||||||
version_tuple,
|
version_tuple,
|
||||||
write_json_file,
|
write_json_file,
|
||||||
write_string,
|
write_string,
|
||||||
|
YoutubeDLCookieProcessor,
|
||||||
YoutubeDLHandler,
|
YoutubeDLHandler,
|
||||||
prepend_extension,
|
prepend_extension,
|
||||||
|
replace_extension,
|
||||||
args_to_str,
|
args_to_str,
|
||||||
age_restricted,
|
age_restricted,
|
||||||
)
|
)
|
||||||
@@ -118,7 +119,7 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
username: Username for authentication purposes.
|
username: Username for authentication purposes.
|
||||||
password: Password for authentication purposes.
|
password: Password for authentication purposes.
|
||||||
videopassword: Password for acces a video.
|
videopassword: Password for accessing a video.
|
||||||
usenetrc: Use netrc for authentication instead.
|
usenetrc: Use netrc for authentication instead.
|
||||||
verbose: Print additional info to stdout.
|
verbose: Print additional info to stdout.
|
||||||
quiet: Do not print messages to stdout.
|
quiet: Do not print messages to stdout.
|
||||||
@@ -135,10 +136,10 @@ class YoutubeDL(object):
|
|||||||
(or video) as a single JSON line.
|
(or video) as a single JSON line.
|
||||||
simulate: Do not download the video files.
|
simulate: Do not download the video files.
|
||||||
format: Video format code. See options.py for more information.
|
format: Video format code. See options.py for more information.
|
||||||
format_limit: Highest quality format to try.
|
|
||||||
outtmpl: Template for output names.
|
outtmpl: Template for output names.
|
||||||
restrictfilenames: Do not allow "&" and spaces in file names
|
restrictfilenames: Do not allow "&" and spaces in file names
|
||||||
ignoreerrors: Do not stop on download errors.
|
ignoreerrors: Do not stop on download errors.
|
||||||
|
force_generic_extractor: Force downloader to use the generic extractor
|
||||||
nooverwrites: Prevent overwriting files.
|
nooverwrites: Prevent overwriting files.
|
||||||
playliststart: Playlist item to start at.
|
playliststart: Playlist item to start at.
|
||||||
playlistend: Playlist item to end at.
|
playlistend: Playlist item to end at.
|
||||||
@@ -261,7 +262,8 @@ class YoutubeDL(object):
|
|||||||
The following options are used by the post processors:
|
The following options are used by the post processors:
|
||||||
prefer_ffmpeg: If True, use ffmpeg instead of avconv if both are available,
|
prefer_ffmpeg: If True, use ffmpeg instead of avconv if both are available,
|
||||||
otherwise prefer avconv.
|
otherwise prefer avconv.
|
||||||
exec_cmd: Arbitrary command to run after downloading
|
postprocessor_args: A list of additional command-line arguments for the
|
||||||
|
postprocessor.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
params = None
|
params = None
|
||||||
@@ -283,7 +285,11 @@ class YoutubeDL(object):
|
|||||||
self._num_downloads = 0
|
self._num_downloads = 0
|
||||||
self._screen_file = [sys.stdout, sys.stderr][params.get('logtostderr', False)]
|
self._screen_file = [sys.stdout, sys.stderr][params.get('logtostderr', False)]
|
||||||
self._err_file = sys.stderr
|
self._err_file = sys.stderr
|
||||||
self.params = params
|
self.params = {
|
||||||
|
# Default parameters
|
||||||
|
'nocheckcertificate': False,
|
||||||
|
}
|
||||||
|
self.params.update(params)
|
||||||
self.cache = Cache(self)
|
self.cache = Cache(self)
|
||||||
|
|
||||||
if params.get('bidi_workaround', False):
|
if params.get('bidi_workaround', False):
|
||||||
@@ -627,13 +633,16 @@ class YoutubeDL(object):
|
|||||||
info_dict.setdefault(key, value)
|
info_dict.setdefault(key, value)
|
||||||
|
|
||||||
def extract_info(self, url, download=True, ie_key=None, extra_info={},
|
def extract_info(self, url, download=True, ie_key=None, extra_info={},
|
||||||
process=True):
|
process=True, force_generic_extractor=False):
|
||||||
'''
|
'''
|
||||||
Returns a list with a dictionary for each video we find.
|
Returns a list with a dictionary for each video we find.
|
||||||
If 'download', also downloads the videos.
|
If 'download', also downloads the videos.
|
||||||
extra_info is a dict containing the extra values to add to each result
|
extra_info is a dict containing the extra values to add to each result
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
if not ie_key and force_generic_extractor:
|
||||||
|
ie_key = 'Generic'
|
||||||
|
|
||||||
if ie_key:
|
if ie_key:
|
||||||
ies = [self.get_info_extractor(ie_key)]
|
ies = [self.get_info_extractor(ie_key)]
|
||||||
else:
|
else:
|
||||||
@@ -761,7 +770,9 @@ class YoutubeDL(object):
|
|||||||
if isinstance(ie_entries, list):
|
if isinstance(ie_entries, list):
|
||||||
n_all_entries = len(ie_entries)
|
n_all_entries = len(ie_entries)
|
||||||
if playlistitems:
|
if playlistitems:
|
||||||
entries = [ie_entries[i - 1] for i in playlistitems]
|
entries = [
|
||||||
|
ie_entries[i - 1] for i in playlistitems
|
||||||
|
if -n_all_entries <= i - 1 < n_all_entries]
|
||||||
else:
|
else:
|
||||||
entries = ie_entries[playliststart:playlistend]
|
entries = ie_entries[playliststart:playlistend]
|
||||||
n_entries = len(entries)
|
n_entries = len(entries)
|
||||||
@@ -846,8 +857,8 @@ class YoutubeDL(object):
|
|||||||
else:
|
else:
|
||||||
raise Exception('Invalid result type: %s' % result_type)
|
raise Exception('Invalid result type: %s' % result_type)
|
||||||
|
|
||||||
def _apply_format_filter(self, format_spec, available_formats):
|
def _build_format_filter(self, filter_spec):
|
||||||
" Returns a tuple of the remaining format_spec and filtered formats "
|
" Returns a function to filter the formats according to the filter_spec "
|
||||||
|
|
||||||
OPERATORS = {
|
OPERATORS = {
|
||||||
'<': operator.lt,
|
'<': operator.lt,
|
||||||
@@ -857,13 +868,13 @@ class YoutubeDL(object):
|
|||||||
'=': operator.eq,
|
'=': operator.eq,
|
||||||
'!=': operator.ne,
|
'!=': operator.ne,
|
||||||
}
|
}
|
||||||
operator_rex = re.compile(r'''(?x)\s*\[
|
operator_rex = re.compile(r'''(?x)\s*
|
||||||
(?P<key>width|height|tbr|abr|vbr|asr|filesize|fps)
|
(?P<key>width|height|tbr|abr|vbr|asr|filesize|fps)
|
||||||
\s*(?P<op>%s)(?P<none_inclusive>\s*\?)?\s*
|
\s*(?P<op>%s)(?P<none_inclusive>\s*\?)?\s*
|
||||||
(?P<value>[0-9.]+(?:[kKmMgGtTpPeEzZyY]i?[Bb]?)?)
|
(?P<value>[0-9.]+(?:[kKmMgGtTpPeEzZyY]i?[Bb]?)?)
|
||||||
\]$
|
$
|
||||||
''' % '|'.join(map(re.escape, OPERATORS.keys())))
|
''' % '|'.join(map(re.escape, OPERATORS.keys())))
|
||||||
m = operator_rex.search(format_spec)
|
m = operator_rex.search(filter_spec)
|
||||||
if m:
|
if m:
|
||||||
try:
|
try:
|
||||||
comparison_value = int(m.group('value'))
|
comparison_value = int(m.group('value'))
|
||||||
@@ -874,7 +885,7 @@ class YoutubeDL(object):
|
|||||||
if comparison_value is None:
|
if comparison_value is None:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'Invalid value %r in format specification %r' % (
|
'Invalid value %r in format specification %r' % (
|
||||||
m.group('value'), format_spec))
|
m.group('value'), filter_spec))
|
||||||
op = OPERATORS[m.group('op')]
|
op = OPERATORS[m.group('op')]
|
||||||
|
|
||||||
if not m:
|
if not m:
|
||||||
@@ -882,78 +893,283 @@ class YoutubeDL(object):
|
|||||||
'=': operator.eq,
|
'=': operator.eq,
|
||||||
'!=': operator.ne,
|
'!=': operator.ne,
|
||||||
}
|
}
|
||||||
str_operator_rex = re.compile(r'''(?x)\s*\[
|
str_operator_rex = re.compile(r'''(?x)
|
||||||
\s*(?P<key>ext|acodec|vcodec|container|protocol)
|
\s*(?P<key>ext|acodec|vcodec|container|protocol)
|
||||||
\s*(?P<op>%s)(?P<none_inclusive>\s*\?)?
|
\s*(?P<op>%s)(?P<none_inclusive>\s*\?)?
|
||||||
\s*(?P<value>[a-zA-Z0-9_-]+)
|
\s*(?P<value>[a-zA-Z0-9_-]+)
|
||||||
\s*\]$
|
\s*$
|
||||||
''' % '|'.join(map(re.escape, STR_OPERATORS.keys())))
|
''' % '|'.join(map(re.escape, STR_OPERATORS.keys())))
|
||||||
m = str_operator_rex.search(format_spec)
|
m = str_operator_rex.search(filter_spec)
|
||||||
if m:
|
if m:
|
||||||
comparison_value = m.group('value')
|
comparison_value = m.group('value')
|
||||||
op = STR_OPERATORS[m.group('op')]
|
op = STR_OPERATORS[m.group('op')]
|
||||||
|
|
||||||
if not m:
|
if not m:
|
||||||
raise ValueError('Invalid format specification %r' % format_spec)
|
raise ValueError('Invalid filter specification %r' % filter_spec)
|
||||||
|
|
||||||
def _filter(f):
|
def _filter(f):
|
||||||
actual_value = f.get(m.group('key'))
|
actual_value = f.get(m.group('key'))
|
||||||
if actual_value is None:
|
if actual_value is None:
|
||||||
return m.group('none_inclusive')
|
return m.group('none_inclusive')
|
||||||
return op(actual_value, comparison_value)
|
return op(actual_value, comparison_value)
|
||||||
new_formats = [f for f in available_formats if _filter(f)]
|
return _filter
|
||||||
|
|
||||||
new_format_spec = format_spec[:-len(m.group(0))]
|
def build_format_selector(self, format_spec):
|
||||||
if not new_format_spec:
|
def syntax_error(note, start):
|
||||||
new_format_spec = 'best'
|
message = (
|
||||||
|
'Invalid format specification: '
|
||||||
|
'{0}\n\t{1}\n\t{2}^'.format(note, format_spec, ' ' * start[1]))
|
||||||
|
return SyntaxError(message)
|
||||||
|
|
||||||
return (new_format_spec, new_formats)
|
PICKFIRST = 'PICKFIRST'
|
||||||
|
MERGE = 'MERGE'
|
||||||
|
SINGLE = 'SINGLE'
|
||||||
|
GROUP = 'GROUP'
|
||||||
|
FormatSelector = collections.namedtuple('FormatSelector', ['type', 'selector', 'filters'])
|
||||||
|
|
||||||
def select_format(self, format_spec, available_formats):
|
def _parse_filter(tokens):
|
||||||
while format_spec.endswith(']'):
|
filter_parts = []
|
||||||
format_spec, available_formats = self._apply_format_filter(
|
for type, string, start, _, _ in tokens:
|
||||||
format_spec, available_formats)
|
if type == tokenize.OP and string == ']':
|
||||||
if not available_formats:
|
return ''.join(filter_parts)
|
||||||
return None
|
else:
|
||||||
|
filter_parts.append(string)
|
||||||
|
|
||||||
if format_spec == 'best' or format_spec is None:
|
def _remove_unused_ops(tokens):
|
||||||
return available_formats[-1]
|
# Remove operators that we don't use and join them with the sourrounding strings
|
||||||
elif format_spec == 'worst':
|
# for example: 'mp4' '-' 'baseline' '-' '16x9' is converted to 'mp4-baseline-16x9'
|
||||||
return available_formats[0]
|
ALLOWED_OPS = ('/', '+', ',', '(', ')')
|
||||||
elif format_spec == 'bestaudio':
|
last_string, last_start, last_end, last_line = None, None, None, None
|
||||||
audio_formats = [
|
for type, string, start, end, line in tokens:
|
||||||
f for f in available_formats
|
if type == tokenize.OP and string == '[':
|
||||||
if f.get('vcodec') == 'none']
|
if last_string:
|
||||||
if audio_formats:
|
yield tokenize.NAME, last_string, last_start, last_end, last_line
|
||||||
return audio_formats[-1]
|
last_string = None
|
||||||
elif format_spec == 'worstaudio':
|
yield type, string, start, end, line
|
||||||
audio_formats = [
|
# everything inside brackets will be handled by _parse_filter
|
||||||
f for f in available_formats
|
for type, string, start, end, line in tokens:
|
||||||
if f.get('vcodec') == 'none']
|
yield type, string, start, end, line
|
||||||
if audio_formats:
|
if type == tokenize.OP and string == ']':
|
||||||
return audio_formats[0]
|
break
|
||||||
elif format_spec == 'bestvideo':
|
elif type == tokenize.OP and string in ALLOWED_OPS:
|
||||||
video_formats = [
|
if last_string:
|
||||||
f for f in available_formats
|
yield tokenize.NAME, last_string, last_start, last_end, last_line
|
||||||
if f.get('acodec') == 'none']
|
last_string = None
|
||||||
if video_formats:
|
yield type, string, start, end, line
|
||||||
return video_formats[-1]
|
elif type in [tokenize.NAME, tokenize.NUMBER, tokenize.OP]:
|
||||||
elif format_spec == 'worstvideo':
|
if not last_string:
|
||||||
video_formats = [
|
last_string = string
|
||||||
f for f in available_formats
|
last_start = start
|
||||||
if f.get('acodec') == 'none']
|
last_end = end
|
||||||
if video_formats:
|
else:
|
||||||
return video_formats[0]
|
last_string += string
|
||||||
else:
|
if last_string:
|
||||||
extensions = ['mp4', 'flv', 'webm', '3gp', 'm4a', 'mp3', 'ogg', 'aac', 'wav']
|
yield tokenize.NAME, last_string, last_start, last_end, last_line
|
||||||
if format_spec in extensions:
|
|
||||||
filter_f = lambda f: f['ext'] == format_spec
|
def _parse_format_selection(tokens, inside_merge=False, inside_choice=False, inside_group=False):
|
||||||
else:
|
selectors = []
|
||||||
filter_f = lambda f: f['format_id'] == format_spec
|
current_selector = None
|
||||||
matches = list(filter(filter_f, available_formats))
|
for type, string, start, _, _ in tokens:
|
||||||
if matches:
|
# ENCODING is only defined in python 3.x
|
||||||
return matches[-1]
|
if type == getattr(tokenize, 'ENCODING', None):
|
||||||
return None
|
continue
|
||||||
|
elif type in [tokenize.NAME, tokenize.NUMBER]:
|
||||||
|
current_selector = FormatSelector(SINGLE, string, [])
|
||||||
|
elif type == tokenize.OP:
|
||||||
|
if string == ')':
|
||||||
|
if not inside_group:
|
||||||
|
# ')' will be handled by the parentheses group
|
||||||
|
tokens.restore_last_token()
|
||||||
|
break
|
||||||
|
elif inside_merge and string in ['/', ',']:
|
||||||
|
tokens.restore_last_token()
|
||||||
|
break
|
||||||
|
elif inside_choice and string == ',':
|
||||||
|
tokens.restore_last_token()
|
||||||
|
break
|
||||||
|
elif string == ',':
|
||||||
|
if not current_selector:
|
||||||
|
raise syntax_error('"," must follow a format selector', start)
|
||||||
|
selectors.append(current_selector)
|
||||||
|
current_selector = None
|
||||||
|
elif string == '/':
|
||||||
|
if not current_selector:
|
||||||
|
raise syntax_error('"/" must follow a format selector', start)
|
||||||
|
first_choice = current_selector
|
||||||
|
second_choice = _parse_format_selection(tokens, inside_choice=True)
|
||||||
|
current_selector = FormatSelector(PICKFIRST, (first_choice, second_choice), [])
|
||||||
|
elif string == '[':
|
||||||
|
if not current_selector:
|
||||||
|
current_selector = FormatSelector(SINGLE, 'best', [])
|
||||||
|
format_filter = _parse_filter(tokens)
|
||||||
|
current_selector.filters.append(format_filter)
|
||||||
|
elif string == '(':
|
||||||
|
if current_selector:
|
||||||
|
raise syntax_error('Unexpected "("', start)
|
||||||
|
group = _parse_format_selection(tokens, inside_group=True)
|
||||||
|
current_selector = FormatSelector(GROUP, group, [])
|
||||||
|
elif string == '+':
|
||||||
|
video_selector = current_selector
|
||||||
|
audio_selector = _parse_format_selection(tokens, inside_merge=True)
|
||||||
|
if not video_selector or not audio_selector:
|
||||||
|
raise syntax_error('"+" must be between two format selectors', start)
|
||||||
|
current_selector = FormatSelector(MERGE, (video_selector, audio_selector), [])
|
||||||
|
else:
|
||||||
|
raise syntax_error('Operator not recognized: "{0}"'.format(string), start)
|
||||||
|
elif type == tokenize.ENDMARKER:
|
||||||
|
break
|
||||||
|
if current_selector:
|
||||||
|
selectors.append(current_selector)
|
||||||
|
return selectors
|
||||||
|
|
||||||
|
def _build_selector_function(selector):
|
||||||
|
if isinstance(selector, list):
|
||||||
|
fs = [_build_selector_function(s) for s in selector]
|
||||||
|
|
||||||
|
def selector_function(formats):
|
||||||
|
for f in fs:
|
||||||
|
for format in f(formats):
|
||||||
|
yield format
|
||||||
|
return selector_function
|
||||||
|
elif selector.type == GROUP:
|
||||||
|
selector_function = _build_selector_function(selector.selector)
|
||||||
|
elif selector.type == PICKFIRST:
|
||||||
|
fs = [_build_selector_function(s) for s in selector.selector]
|
||||||
|
|
||||||
|
def selector_function(formats):
|
||||||
|
for f in fs:
|
||||||
|
picked_formats = list(f(formats))
|
||||||
|
if picked_formats:
|
||||||
|
return picked_formats
|
||||||
|
return []
|
||||||
|
elif selector.type == SINGLE:
|
||||||
|
format_spec = selector.selector
|
||||||
|
|
||||||
|
def selector_function(formats):
|
||||||
|
formats = list(formats)
|
||||||
|
if not formats:
|
||||||
|
return
|
||||||
|
if format_spec == 'all':
|
||||||
|
for f in formats:
|
||||||
|
yield f
|
||||||
|
elif format_spec in ['best', 'worst', None]:
|
||||||
|
format_idx = 0 if format_spec == 'worst' else -1
|
||||||
|
audiovideo_formats = [
|
||||||
|
f for f in formats
|
||||||
|
if f.get('vcodec') != 'none' and f.get('acodec') != 'none']
|
||||||
|
if audiovideo_formats:
|
||||||
|
yield audiovideo_formats[format_idx]
|
||||||
|
# for audio only (soundcloud) or video only (imgur) urls, select the best/worst audio format
|
||||||
|
elif (all(f.get('acodec') != 'none' for f in formats) or
|
||||||
|
all(f.get('vcodec') != 'none' for f in formats)):
|
||||||
|
yield formats[format_idx]
|
||||||
|
elif format_spec == 'bestaudio':
|
||||||
|
audio_formats = [
|
||||||
|
f for f in formats
|
||||||
|
if f.get('vcodec') == 'none']
|
||||||
|
if audio_formats:
|
||||||
|
yield audio_formats[-1]
|
||||||
|
elif format_spec == 'worstaudio':
|
||||||
|
audio_formats = [
|
||||||
|
f for f in formats
|
||||||
|
if f.get('vcodec') == 'none']
|
||||||
|
if audio_formats:
|
||||||
|
yield audio_formats[0]
|
||||||
|
elif format_spec == 'bestvideo':
|
||||||
|
video_formats = [
|
||||||
|
f for f in formats
|
||||||
|
if f.get('acodec') == 'none']
|
||||||
|
if video_formats:
|
||||||
|
yield video_formats[-1]
|
||||||
|
elif format_spec == 'worstvideo':
|
||||||
|
video_formats = [
|
||||||
|
f for f in formats
|
||||||
|
if f.get('acodec') == 'none']
|
||||||
|
if video_formats:
|
||||||
|
yield video_formats[0]
|
||||||
|
else:
|
||||||
|
extensions = ['mp4', 'flv', 'webm', '3gp', 'm4a', 'mp3', 'ogg', 'aac', 'wav']
|
||||||
|
if format_spec in extensions:
|
||||||
|
filter_f = lambda f: f['ext'] == format_spec
|
||||||
|
else:
|
||||||
|
filter_f = lambda f: f['format_id'] == format_spec
|
||||||
|
matches = list(filter(filter_f, formats))
|
||||||
|
if matches:
|
||||||
|
yield matches[-1]
|
||||||
|
elif selector.type == MERGE:
|
||||||
|
def _merge(formats_info):
|
||||||
|
format_1, format_2 = [f['format_id'] for f in formats_info]
|
||||||
|
# The first format must contain the video and the
|
||||||
|
# second the audio
|
||||||
|
if formats_info[0].get('vcodec') == 'none':
|
||||||
|
self.report_error('The first format must '
|
||||||
|
'contain the video, try using '
|
||||||
|
'"-f %s+%s"' % (format_2, format_1))
|
||||||
|
return
|
||||||
|
output_ext = (
|
||||||
|
formats_info[0]['ext']
|
||||||
|
if self.params.get('merge_output_format') is None
|
||||||
|
else self.params['merge_output_format'])
|
||||||
|
return {
|
||||||
|
'requested_formats': formats_info,
|
||||||
|
'format': '%s+%s' % (formats_info[0].get('format'),
|
||||||
|
formats_info[1].get('format')),
|
||||||
|
'format_id': '%s+%s' % (formats_info[0].get('format_id'),
|
||||||
|
formats_info[1].get('format_id')),
|
||||||
|
'width': formats_info[0].get('width'),
|
||||||
|
'height': formats_info[0].get('height'),
|
||||||
|
'resolution': formats_info[0].get('resolution'),
|
||||||
|
'fps': formats_info[0].get('fps'),
|
||||||
|
'vcodec': formats_info[0].get('vcodec'),
|
||||||
|
'vbr': formats_info[0].get('vbr'),
|
||||||
|
'stretched_ratio': formats_info[0].get('stretched_ratio'),
|
||||||
|
'acodec': formats_info[1].get('acodec'),
|
||||||
|
'abr': formats_info[1].get('abr'),
|
||||||
|
'ext': output_ext,
|
||||||
|
}
|
||||||
|
video_selector, audio_selector = map(_build_selector_function, selector.selector)
|
||||||
|
|
||||||
|
def selector_function(formats):
|
||||||
|
formats = list(formats)
|
||||||
|
for pair in itertools.product(video_selector(formats), audio_selector(formats)):
|
||||||
|
yield _merge(pair)
|
||||||
|
|
||||||
|
filters = [self._build_format_filter(f) for f in selector.filters]
|
||||||
|
|
||||||
|
def final_selector(formats):
|
||||||
|
for _filter in filters:
|
||||||
|
formats = list(filter(_filter, formats))
|
||||||
|
return selector_function(formats)
|
||||||
|
return final_selector
|
||||||
|
|
||||||
|
stream = io.BytesIO(format_spec.encode('utf-8'))
|
||||||
|
try:
|
||||||
|
tokens = list(_remove_unused_ops(compat_tokenize_tokenize(stream.readline)))
|
||||||
|
except tokenize.TokenError:
|
||||||
|
raise syntax_error('Missing closing/opening brackets or parenthesis', (0, len(format_spec)))
|
||||||
|
|
||||||
|
class TokenIterator(object):
|
||||||
|
def __init__(self, tokens):
|
||||||
|
self.tokens = tokens
|
||||||
|
self.counter = 0
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
if self.counter >= len(self.tokens):
|
||||||
|
raise StopIteration()
|
||||||
|
value = self.tokens[self.counter]
|
||||||
|
self.counter += 1
|
||||||
|
return value
|
||||||
|
|
||||||
|
next = __next__
|
||||||
|
|
||||||
|
def restore_last_token(self):
|
||||||
|
self.counter -= 1
|
||||||
|
|
||||||
|
parsed_selector = _parse_format_selection(iter(TokenIterator(tokens)))
|
||||||
|
return _build_selector_function(parsed_selector)
|
||||||
|
|
||||||
def _calc_headers(self, info_dict):
|
def _calc_headers(self, info_dict):
|
||||||
res = std_headers.copy()
|
res = std_headers.copy()
|
||||||
@@ -996,7 +1212,7 @@ class YoutubeDL(object):
|
|||||||
t.get('preference'), t.get('width'), t.get('height'),
|
t.get('preference'), t.get('width'), t.get('height'),
|
||||||
t.get('id'), t.get('url')))
|
t.get('id'), t.get('url')))
|
||||||
for i, t in enumerate(thumbnails):
|
for i, t in enumerate(thumbnails):
|
||||||
if 'width' in t and 'height' in t:
|
if t.get('width') and t.get('height'):
|
||||||
t['resolution'] = '%dx%d' % (t['width'], t['height'])
|
t['resolution'] = '%dx%d' % (t['width'], t['height'])
|
||||||
if t.get('id') is None:
|
if t.get('id') is None:
|
||||||
t['id'] = '%d' % i
|
t['id'] = '%d' % i
|
||||||
@@ -1008,13 +1224,13 @@ class YoutubeDL(object):
|
|||||||
info_dict['display_id'] = info_dict['id']
|
info_dict['display_id'] = info_dict['id']
|
||||||
|
|
||||||
if info_dict.get('upload_date') is None and info_dict.get('timestamp') is not None:
|
if info_dict.get('upload_date') is None and info_dict.get('timestamp') is not None:
|
||||||
# Working around negative timestamps in Windows
|
# Working around out-of-range timestamp values (e.g. negative ones on Windows,
|
||||||
# (see http://bugs.python.org/issue1646728)
|
# see http://bugs.python.org/issue1646728)
|
||||||
if info_dict['timestamp'] < 0 and os.name == 'nt':
|
try:
|
||||||
info_dict['timestamp'] = 0
|
upload_date = datetime.datetime.utcfromtimestamp(info_dict['timestamp'])
|
||||||
upload_date = datetime.datetime.utcfromtimestamp(
|
info_dict['upload_date'] = upload_date.strftime('%Y%m%d')
|
||||||
info_dict['timestamp'])
|
except (ValueError, OverflowError, OSError):
|
||||||
info_dict['upload_date'] = upload_date.strftime('%Y%m%d')
|
pass
|
||||||
|
|
||||||
if self.params.get('listsubtitles', False):
|
if self.params.get('listsubtitles', False):
|
||||||
if 'automatic_captions' in info_dict:
|
if 'automatic_captions' in info_dict:
|
||||||
@@ -1025,12 +1241,6 @@ class YoutubeDL(object):
|
|||||||
info_dict['id'], info_dict.get('subtitles'),
|
info_dict['id'], info_dict.get('subtitles'),
|
||||||
info_dict.get('automatic_captions'))
|
info_dict.get('automatic_captions'))
|
||||||
|
|
||||||
# This extractors handle format selection themselves
|
|
||||||
if info_dict['extractor'] in ['Youku']:
|
|
||||||
if download:
|
|
||||||
self.process_info(info_dict)
|
|
||||||
return info_dict
|
|
||||||
|
|
||||||
# We now pick which formats have to be downloaded
|
# We now pick which formats have to be downloaded
|
||||||
if info_dict.get('formats') is None:
|
if info_dict.get('formats') is None:
|
||||||
# There's only one format available
|
# There's only one format available
|
||||||
@@ -1041,6 +1251,8 @@ class YoutubeDL(object):
|
|||||||
if not formats:
|
if not formats:
|
||||||
raise ExtractorError('No video formats found!')
|
raise ExtractorError('No video formats found!')
|
||||||
|
|
||||||
|
formats_dict = {}
|
||||||
|
|
||||||
# We check that all the formats have the format and format_id fields
|
# We check that all the formats have the format and format_id fields
|
||||||
for i, format in enumerate(formats):
|
for i, format in enumerate(formats):
|
||||||
if 'url' not in format:
|
if 'url' not in format:
|
||||||
@@ -1048,6 +1260,18 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
if format.get('format_id') is None:
|
if format.get('format_id') is None:
|
||||||
format['format_id'] = compat_str(i)
|
format['format_id'] = compat_str(i)
|
||||||
|
format_id = format['format_id']
|
||||||
|
if format_id not in formats_dict:
|
||||||
|
formats_dict[format_id] = []
|
||||||
|
formats_dict[format_id].append(format)
|
||||||
|
|
||||||
|
# Make sure all formats have unique format_id
|
||||||
|
for format_id, ambiguous_formats in formats_dict.items():
|
||||||
|
if len(ambiguous_formats) > 1:
|
||||||
|
for i, format in enumerate(ambiguous_formats):
|
||||||
|
format['format_id'] = '%s-%d' % (format_id, i)
|
||||||
|
|
||||||
|
for i, format in enumerate(formats):
|
||||||
if format.get('format') is None:
|
if format.get('format') is None:
|
||||||
format['format'] = '{id} - {res}{note}'.format(
|
format['format'] = '{id} - {res}{note}'.format(
|
||||||
id=format['format_id'],
|
id=format['format_id'],
|
||||||
@@ -1063,12 +1287,6 @@ class YoutubeDL(object):
|
|||||||
full_format_info.update(format)
|
full_format_info.update(format)
|
||||||
format['http_headers'] = self._calc_headers(full_format_info)
|
format['http_headers'] = self._calc_headers(full_format_info)
|
||||||
|
|
||||||
format_limit = self.params.get('format_limit', None)
|
|
||||||
if format_limit:
|
|
||||||
formats = list(takewhile_inclusive(
|
|
||||||
lambda f: f['format_id'] != format_limit, formats
|
|
||||||
))
|
|
||||||
|
|
||||||
# TODO Central sorting goes here
|
# TODO Central sorting goes here
|
||||||
|
|
||||||
if formats[0] is not info_dict:
|
if formats[0] is not info_dict:
|
||||||
@@ -1086,57 +1304,17 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
req_format = self.params.get('format')
|
req_format = self.params.get('format')
|
||||||
if req_format is None:
|
if req_format is None:
|
||||||
req_format = 'best'
|
req_format_list = []
|
||||||
formats_to_download = []
|
if (self.params.get('outtmpl', DEFAULT_OUTTMPL) != '-' and
|
||||||
if req_format == 'all':
|
info_dict['extractor'] in ['youtube', 'ted'] and
|
||||||
formats_to_download = formats
|
not info_dict.get('is_live')):
|
||||||
else:
|
merger = FFmpegMergerPP(self)
|
||||||
for rfstr in req_format.split(','):
|
if merger.available and merger.can_merge():
|
||||||
# We can accept formats requested in the format: 34/5/best, we pick
|
req_format_list.append('bestvideo+bestaudio')
|
||||||
# the first that is available, starting from left
|
req_format_list.append('best')
|
||||||
req_formats = rfstr.split('/')
|
req_format = '/'.join(req_format_list)
|
||||||
for rf in req_formats:
|
format_selector = self.build_format_selector(req_format)
|
||||||
if re.match(r'.+?\+.+?', rf) is not None:
|
formats_to_download = list(format_selector(formats))
|
||||||
# Two formats have been requested like '137+139'
|
|
||||||
format_1, format_2 = rf.split('+')
|
|
||||||
formats_info = (self.select_format(format_1, formats),
|
|
||||||
self.select_format(format_2, formats))
|
|
||||||
if all(formats_info):
|
|
||||||
# The first format must contain the video and the
|
|
||||||
# second the audio
|
|
||||||
if formats_info[0].get('vcodec') == 'none':
|
|
||||||
self.report_error('The first format must '
|
|
||||||
'contain the video, try using '
|
|
||||||
'"-f %s+%s"' % (format_2, format_1))
|
|
||||||
return
|
|
||||||
output_ext = (
|
|
||||||
formats_info[0]['ext']
|
|
||||||
if self.params.get('merge_output_format') is None
|
|
||||||
else self.params['merge_output_format'])
|
|
||||||
selected_format = {
|
|
||||||
'requested_formats': formats_info,
|
|
||||||
'format': '%s+%s' % (formats_info[0].get('format'),
|
|
||||||
formats_info[1].get('format')),
|
|
||||||
'format_id': '%s+%s' % (formats_info[0].get('format_id'),
|
|
||||||
formats_info[1].get('format_id')),
|
|
||||||
'width': formats_info[0].get('width'),
|
|
||||||
'height': formats_info[0].get('height'),
|
|
||||||
'resolution': formats_info[0].get('resolution'),
|
|
||||||
'fps': formats_info[0].get('fps'),
|
|
||||||
'vcodec': formats_info[0].get('vcodec'),
|
|
||||||
'vbr': formats_info[0].get('vbr'),
|
|
||||||
'stretched_ratio': formats_info[0].get('stretched_ratio'),
|
|
||||||
'acodec': formats_info[1].get('acodec'),
|
|
||||||
'abr': formats_info[1].get('abr'),
|
|
||||||
'ext': output_ext,
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
selected_format = None
|
|
||||||
else:
|
|
||||||
selected_format = self.select_format(rf, formats)
|
|
||||||
if selected_format is not None:
|
|
||||||
formats_to_download.append(selected_format)
|
|
||||||
break
|
|
||||||
if not formats_to_download:
|
if not formats_to_download:
|
||||||
raise ExtractorError('requested format not available',
|
raise ExtractorError('requested format not available',
|
||||||
expected=True)
|
expected=True)
|
||||||
@@ -1268,7 +1446,7 @@ class YoutubeDL(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if self.params.get('writedescription', False):
|
if self.params.get('writedescription', False):
|
||||||
descfn = filename + '.description'
|
descfn = replace_extension(filename, 'description', info_dict.get('ext'))
|
||||||
if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(descfn)):
|
if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(descfn)):
|
||||||
self.to_screen('[info] Video description is already present')
|
self.to_screen('[info] Video description is already present')
|
||||||
elif info_dict.get('description') is None:
|
elif info_dict.get('description') is None:
|
||||||
@@ -1283,7 +1461,7 @@ class YoutubeDL(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if self.params.get('writeannotations', False):
|
if self.params.get('writeannotations', False):
|
||||||
annofn = filename + '.annotations.xml'
|
annofn = replace_extension(filename, 'annotations.xml', info_dict.get('ext'))
|
||||||
if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(annofn)):
|
if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(annofn)):
|
||||||
self.to_screen('[info] Video annotations are already present')
|
self.to_screen('[info] Video annotations are already present')
|
||||||
else:
|
else:
|
||||||
@@ -1330,13 +1508,13 @@ class YoutubeDL(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if self.params.get('writeinfojson', False):
|
if self.params.get('writeinfojson', False):
|
||||||
infofn = os.path.splitext(filename)[0] + '.info.json'
|
infofn = replace_extension(filename, 'info.json', info_dict.get('ext'))
|
||||||
if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(infofn)):
|
if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(infofn)):
|
||||||
self.to_screen('[info] Video description metadata is already present')
|
self.to_screen('[info] Video description metadata is already present')
|
||||||
else:
|
else:
|
||||||
self.to_screen('[info] Writing video description metadata as JSON to: ' + infofn)
|
self.to_screen('[info] Writing video description metadata as JSON to: ' + infofn)
|
||||||
try:
|
try:
|
||||||
write_json_file(info_dict, infofn)
|
write_json_file(self.filter_requested_info(info_dict), infofn)
|
||||||
except (OSError, IOError):
|
except (OSError, IOError):
|
||||||
self.report_error('Cannot write metadata to JSON file ' + infofn)
|
self.report_error('Cannot write metadata to JSON file ' + infofn)
|
||||||
return
|
return
|
||||||
@@ -1356,24 +1534,57 @@ class YoutubeDL(object):
|
|||||||
if info_dict.get('requested_formats') is not None:
|
if info_dict.get('requested_formats') is not None:
|
||||||
downloaded = []
|
downloaded = []
|
||||||
success = True
|
success = True
|
||||||
merger = FFmpegMergerPP(self, not self.params.get('keepvideo'))
|
merger = FFmpegMergerPP(self)
|
||||||
if not merger.available:
|
if not merger.available:
|
||||||
postprocessors = []
|
postprocessors = []
|
||||||
self.report_warning('You have requested multiple '
|
self.report_warning('You have requested multiple '
|
||||||
'formats but ffmpeg or avconv are not installed.'
|
'formats but ffmpeg or avconv are not installed.'
|
||||||
' The formats won\'t be merged')
|
' The formats won\'t be merged.')
|
||||||
else:
|
else:
|
||||||
postprocessors = [merger]
|
postprocessors = [merger]
|
||||||
for f in info_dict['requested_formats']:
|
|
||||||
new_info = dict(info_dict)
|
def compatible_formats(formats):
|
||||||
new_info.update(f)
|
video, audio = formats
|
||||||
fname = self.prepare_filename(new_info)
|
# Check extension
|
||||||
fname = prepend_extension(fname, 'f%s' % f['format_id'])
|
video_ext, audio_ext = audio.get('ext'), video.get('ext')
|
||||||
downloaded.append(fname)
|
if video_ext and audio_ext:
|
||||||
partial_success = dl(fname, new_info)
|
COMPATIBLE_EXTS = (
|
||||||
success = success and partial_success
|
('mp3', 'mp4', 'm4a', 'm4p', 'm4b', 'm4r', 'm4v'),
|
||||||
info_dict['__postprocessors'] = postprocessors
|
('webm')
|
||||||
info_dict['__files_to_merge'] = downloaded
|
)
|
||||||
|
for exts in COMPATIBLE_EXTS:
|
||||||
|
if video_ext in exts and audio_ext in exts:
|
||||||
|
return True
|
||||||
|
# TODO: Check acodec/vcodec
|
||||||
|
return False
|
||||||
|
|
||||||
|
filename_real_ext = os.path.splitext(filename)[1][1:]
|
||||||
|
filename_wo_ext = (
|
||||||
|
os.path.splitext(filename)[0]
|
||||||
|
if filename_real_ext == info_dict['ext']
|
||||||
|
else filename)
|
||||||
|
requested_formats = info_dict['requested_formats']
|
||||||
|
if self.params.get('merge_output_format') is None and not compatible_formats(requested_formats):
|
||||||
|
info_dict['ext'] = 'mkv'
|
||||||
|
self.report_warning(
|
||||||
|
'Requested formats are incompatible for merge and will be merged into mkv.')
|
||||||
|
# Ensure filename always has a correct extension for successful merge
|
||||||
|
filename = '%s.%s' % (filename_wo_ext, info_dict['ext'])
|
||||||
|
if os.path.exists(encodeFilename(filename)):
|
||||||
|
self.to_screen(
|
||||||
|
'[download] %s has already been downloaded and '
|
||||||
|
'merged' % filename)
|
||||||
|
else:
|
||||||
|
for f in requested_formats:
|
||||||
|
new_info = dict(info_dict)
|
||||||
|
new_info.update(f)
|
||||||
|
fname = self.prepare_filename(new_info)
|
||||||
|
fname = prepend_extension(fname, 'f%s' % f['format_id'], new_info['ext'])
|
||||||
|
downloaded.append(fname)
|
||||||
|
partial_success = dl(fname, new_info)
|
||||||
|
success = success and partial_success
|
||||||
|
info_dict['__postprocessors'] = postprocessors
|
||||||
|
info_dict['__files_to_merge'] = downloaded
|
||||||
else:
|
else:
|
||||||
# Just a single file
|
# Just a single file
|
||||||
success = dl(filename, info_dict)
|
success = dl(filename, info_dict)
|
||||||
@@ -1443,7 +1654,8 @@ class YoutubeDL(object):
|
|||||||
for url in url_list:
|
for url in url_list:
|
||||||
try:
|
try:
|
||||||
# It also downloads the videos
|
# It also downloads the videos
|
||||||
res = self.extract_info(url)
|
res = self.extract_info(
|
||||||
|
url, force_generic_extractor=self.params.get('force_generic_extractor', False))
|
||||||
except UnavailableVideoError:
|
except UnavailableVideoError:
|
||||||
self.report_error('unable to download video')
|
self.report_error('unable to download video')
|
||||||
except MaxDownloadsReached:
|
except MaxDownloadsReached:
|
||||||
@@ -1460,7 +1672,7 @@ class YoutubeDL(object):
|
|||||||
[info_filename], mode='r',
|
[info_filename], mode='r',
|
||||||
openhook=fileinput.hook_encoded('utf-8'))) as f:
|
openhook=fileinput.hook_encoded('utf-8'))) as f:
|
||||||
# FileInput doesn't have a read method, we can't call json.load
|
# FileInput doesn't have a read method, we can't call json.load
|
||||||
info = json.loads('\n'.join(f))
|
info = self.filter_requested_info(json.loads('\n'.join(f)))
|
||||||
try:
|
try:
|
||||||
self.process_ie_result(info, download=True)
|
self.process_ie_result(info, download=True)
|
||||||
except DownloadError:
|
except DownloadError:
|
||||||
@@ -1472,6 +1684,12 @@ class YoutubeDL(object):
|
|||||||
raise
|
raise
|
||||||
return self._download_retcode
|
return self._download_retcode
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def filter_requested_info(info_dict):
|
||||||
|
return dict(
|
||||||
|
(k, v) for k, v in info_dict.items()
|
||||||
|
if k not in ['requested_formats', 'requested_subtitles'])
|
||||||
|
|
||||||
def post_process(self, filename, ie_info):
|
def post_process(self, filename, ie_info):
|
||||||
"""Run all the postprocessors on the given file."""
|
"""Run all the postprocessors on the given file."""
|
||||||
info = dict(ie_info)
|
info = dict(ie_info)
|
||||||
@@ -1481,24 +1699,18 @@ class YoutubeDL(object):
|
|||||||
pps_chain.extend(ie_info['__postprocessors'])
|
pps_chain.extend(ie_info['__postprocessors'])
|
||||||
pps_chain.extend(self._pps)
|
pps_chain.extend(self._pps)
|
||||||
for pp in pps_chain:
|
for pp in pps_chain:
|
||||||
keep_video = None
|
files_to_delete = []
|
||||||
old_filename = info['filepath']
|
|
||||||
try:
|
try:
|
||||||
keep_video_wish, info = pp.run(info)
|
files_to_delete, info = pp.run(info)
|
||||||
if keep_video_wish is not None:
|
|
||||||
if keep_video_wish:
|
|
||||||
keep_video = keep_video_wish
|
|
||||||
elif keep_video is None:
|
|
||||||
# No clear decision yet, let IE decide
|
|
||||||
keep_video = keep_video_wish
|
|
||||||
except PostProcessingError as e:
|
except PostProcessingError as e:
|
||||||
self.report_error(e.msg)
|
self.report_error(e.msg)
|
||||||
if keep_video is False and not self.params.get('keepvideo', False):
|
if files_to_delete and not self.params.get('keepvideo', False):
|
||||||
try:
|
for old_filename in files_to_delete:
|
||||||
self.to_screen('Deleting original file %s (pass -k to keep)' % old_filename)
|
self.to_screen('Deleting original file %s (pass -k to keep)' % old_filename)
|
||||||
os.remove(encodeFilename(old_filename))
|
try:
|
||||||
except (IOError, OSError):
|
os.remove(encodeFilename(old_filename))
|
||||||
self.report_warning('Unable to remove downloaded video file')
|
except (IOError, OSError):
|
||||||
|
self.report_warning('Unable to remove downloaded original file')
|
||||||
|
|
||||||
def _make_archive_id(self, info_dict):
|
def _make_archive_id(self, info_dict):
|
||||||
# Future-proof against any change in case
|
# Future-proof against any change in case
|
||||||
@@ -1650,26 +1862,6 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
def urlopen(self, req):
|
def urlopen(self, req):
|
||||||
""" Start an HTTP download """
|
""" Start an HTTP download """
|
||||||
|
|
||||||
# According to RFC 3986, URLs can not contain non-ASCII characters, however this is not
|
|
||||||
# always respected by websites, some tend to give out URLs with non percent-encoded
|
|
||||||
# non-ASCII characters (see telemb.py, ard.py [#3412])
|
|
||||||
# urllib chokes on URLs with non-ASCII characters (see http://bugs.python.org/issue3991)
|
|
||||||
# To work around aforementioned issue we will replace request's original URL with
|
|
||||||
# percent-encoded one
|
|
||||||
req_is_string = isinstance(req, compat_basestring)
|
|
||||||
url = req if req_is_string else req.get_full_url()
|
|
||||||
url_escaped = escape_url(url)
|
|
||||||
|
|
||||||
# Substitute URL if any change after escaping
|
|
||||||
if url != url_escaped:
|
|
||||||
if req_is_string:
|
|
||||||
req = url_escaped
|
|
||||||
else:
|
|
||||||
req = compat_urllib_request.Request(
|
|
||||||
url_escaped, data=req.data, headers=req.headers,
|
|
||||||
origin_req_host=req.origin_req_host, unverifiable=req.unverifiable)
|
|
||||||
|
|
||||||
return self._opener.open(req, timeout=self._socket_timeout)
|
return self._opener.open(req, timeout=self._socket_timeout)
|
||||||
|
|
||||||
def print_debug_header(self):
|
def print_debug_header(self):
|
||||||
@@ -1752,8 +1944,7 @@ class YoutubeDL(object):
|
|||||||
if os.access(opts_cookiefile, os.R_OK):
|
if os.access(opts_cookiefile, os.R_OK):
|
||||||
self.cookiejar.load()
|
self.cookiejar.load()
|
||||||
|
|
||||||
cookie_processor = compat_urllib_request.HTTPCookieProcessor(
|
cookie_processor = YoutubeDLCookieProcessor(self.cookiejar)
|
||||||
self.cookiejar)
|
|
||||||
if opts_proxy is not None:
|
if opts_proxy is not None:
|
||||||
if opts_proxy == '':
|
if opts_proxy == '':
|
||||||
proxies = {}
|
proxies = {}
|
||||||
@@ -1812,7 +2003,7 @@ class YoutubeDL(object):
|
|||||||
thumb_ext = determine_ext(t['url'], 'jpg')
|
thumb_ext = determine_ext(t['url'], 'jpg')
|
||||||
suffix = '_%s' % t['id'] if len(thumbnails) > 1 else ''
|
suffix = '_%s' % t['id'] if len(thumbnails) > 1 else ''
|
||||||
thumb_display_id = '%s ' % t['id'] if len(thumbnails) > 1 else ''
|
thumb_display_id = '%s ' % t['id'] if len(thumbnails) > 1 else ''
|
||||||
thumb_filename = os.path.splitext(filename)[0] + suffix + '.' + thumb_ext
|
t['filename'] = thumb_filename = os.path.splitext(filename)[0] + suffix + '.' + thumb_ext
|
||||||
|
|
||||||
if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(thumb_filename)):
|
if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(thumb_filename)):
|
||||||
self.to_screen('[%s] %s: Thumbnail %sis already present' %
|
self.to_screen('[%s] %s: Thumbnail %sis already present' %
|
||||||
@@ -1822,7 +2013,7 @@ class YoutubeDL(object):
|
|||||||
(info_dict['extractor'], info_dict['id'], thumb_display_id))
|
(info_dict['extractor'], info_dict['id'], thumb_display_id))
|
||||||
try:
|
try:
|
||||||
uf = self.urlopen(t['url'])
|
uf = self.urlopen(t['url'])
|
||||||
with open(thumb_filename, 'wb') as thumbf:
|
with open(encodeFilename(thumb_filename), 'wb') as thumbf:
|
||||||
shutil.copyfileobj(uf, thumbf)
|
shutil.copyfileobj(uf, thumbf)
|
||||||
self.to_screen('[%s] %s: Writing thumbnail %sto: %s' %
|
self.to_screen('[%s] %s: Writing thumbnail %sto: %s' %
|
||||||
(info_dict['extractor'], info_dict['id'], thumb_display_id, thumb_filename))
|
(info_dict['extractor'], info_dict['id'], thumb_display_id, thumb_filename))
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import codecs
|
|||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import shlex
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
@@ -20,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 (
|
||||||
@@ -169,7 +169,7 @@ 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 is not None:
|
||||||
if opts.convertsubtitles not in ['srt', 'vtt', 'ass']:
|
if opts.convertsubtitles not in ['srt', 'vtt', 'ass']:
|
||||||
@@ -189,10 +189,6 @@ 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,):
|
|
||||||
# In Python 2, sys.argv is a bytestring (also note http://bugs.python.org/issue2128 for Windows systems)
|
|
||||||
if opts.outtmpl is not None:
|
|
||||||
opts.outtmpl = opts.outtmpl.decode(preferredencoding())
|
|
||||||
outtmpl = ((opts.outtmpl is not None and opts.outtmpl) or
|
outtmpl = ((opts.outtmpl is not None and opts.outtmpl) or
|
||||||
(opts.format == '-1' and opts.usetitle and '%(title)s-%(id)s-%(format)s.%(ext)s') or
|
(opts.format == '-1' and opts.usetitle and '%(title)s-%(id)s-%(format)s.%(ext)s') or
|
||||||
(opts.format == '-1' and '%(id)s-%(format)s.%(ext)s') or
|
(opts.format == '-1' and '%(id)s-%(format)s.%(ext)s') or
|
||||||
@@ -244,15 +240,18 @@ def _real_main(argv=None):
|
|||||||
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:
|
||||||
@@ -263,7 +262,10 @@ def _real_main(argv=None):
|
|||||||
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
|
external_downloader_args = None
|
||||||
if opts.external_downloader_args:
|
if opts.external_downloader_args:
|
||||||
external_downloader_args = shlex.split(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))
|
||||||
@@ -289,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,
|
||||||
@@ -352,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,
|
||||||
@@ -369,6 +370,7 @@ def _real_main(argv=None):
|
|||||||
'ffmpeg_location': opts.ffmpeg_location,
|
'ffmpeg_location': opts.ffmpeg_location,
|
||||||
'hls_prefer_native': opts.hls_prefer_native,
|
'hls_prefer_native': opts.hls_prefer_native,
|
||||||
'external_downloader_args': external_downloader_args,
|
'external_downloader_args': external_downloader_args,
|
||||||
|
'postprocessor_args': postprocessor_args,
|
||||||
'cn_verification_proxy': opts.cn_verification_proxy,
|
'cn_verification_proxy': opts.cn_verification_proxy,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import getpass
|
|||||||
import optparse
|
import optparse
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -41,16 +43,16 @@ try:
|
|||||||
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
|
||||||
@@ -79,42 +81,74 @@ except ImportError:
|
|||||||
import BaseHTTPServer as compat_http_server
|
import BaseHTTPServer as compat_http_server
|
||||||
|
|
||||||
try:
|
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, unicode):
|
||||||
|
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
|
compat_str = unicode # Python 2
|
||||||
@@ -194,6 +228,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, unicode):
|
||||||
|
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
|
||||||
@@ -393,30 +438,49 @@ else:
|
|||||||
pass
|
pass
|
||||||
return _terminal_size(columns, 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_expanduser',
|
'compat_expanduser',
|
||||||
'compat_get_terminal_size',
|
'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_urlparse',
|
'compat_urlparse',
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import time
|
|||||||
from ..compat import compat_str
|
from ..compat import compat_str
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
encodeFilename,
|
encodeFilename,
|
||||||
|
decodeArgument,
|
||||||
format_bytes,
|
format_bytes,
|
||||||
timeconvert,
|
timeconvert,
|
||||||
)
|
)
|
||||||
@@ -353,19 +354,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))
|
||||||
|
|||||||
66
youtube_dl/downloader/dash.py
Normal file
66
youtube_dl/downloader/dash.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import FileDownloader
|
||||||
|
from ..compat import compat_urllib_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 = compat_urllib_request.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:
|
||||||
|
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,32 +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=[]):
|
def _configuration_args(self, default=[]):
|
||||||
ex_args = self.params.get('external_downloader_args')
|
return cli_configuration_args(self.params, 'external_downloader_args', default)
|
||||||
if ex_args is None:
|
|
||||||
return default
|
|
||||||
assert isinstance(ex_args, list)
|
|
||||||
return ex_args
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -85,7 +80,19 @@ 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 += self._configuration_args()
|
||||||
cmd += ['--', info_dict['url']]
|
cmd += ['--', info_dict['url']]
|
||||||
return cmd
|
return cmd
|
||||||
@@ -96,7 +103,9 @@ 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 += self._configuration_args()
|
||||||
cmd += ['--', info_dict['url']]
|
cmd += ['--', info_dict['url']]
|
||||||
return cmd
|
return cmd
|
||||||
@@ -113,10 +122,20 @@ class Aria2cFD(ExternalFD):
|
|||||||
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()
|
||||||
@@ -131,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]
|
||||||
|
|||||||
@@ -7,17 +7,16 @@ import os
|
|||||||
import time
|
import time
|
||||||
import xml.etree.ElementTree as etree
|
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_urlparse,
|
compat_urlparse,
|
||||||
compat_urllib_error,
|
compat_urllib_error,
|
||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
struct_pack,
|
|
||||||
struct_unpack,
|
|
||||||
encodeFilename,
|
encodeFilename,
|
||||||
sanitize_open,
|
sanitize_open,
|
||||||
|
struct_pack,
|
||||||
|
struct_unpack,
|
||||||
xpath_text,
|
xpath_text,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -226,16 +225,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:
|
||||||
@@ -288,7 +284,7 @@ class F4mFD(FileDownloader):
|
|||||||
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()
|
manifest = self.ydl.urlopen(man_url).read()
|
||||||
|
|
||||||
doc = etree.fromstring(manifest)
|
doc = etree.fromstring(manifest)
|
||||||
@@ -320,67 +316,20 @@ class F4mFD(FileDownloader):
|
|||||||
# 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,
|
||||||
{
|
}
|
||||||
'continuedl': True,
|
|
||||||
'quiet': True,
|
self._prepare_frag_download(ctx)
|
||||||
'noprogress': True,
|
|
||||||
'ratelimit': self.params.get('ratelimit', None),
|
dest_stream = ctx['dest_stream']
|
||||||
'test': self.params.get('test', False),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
tmpfilename = self.temp_name(filename)
|
|
||||||
(dest_stream, tmpfilename) = sanitize_open(tmpfilename, 'wb')
|
|
||||||
|
|
||||||
write_flv_header(dest_stream)
|
write_flv_header(dest_stream)
|
||||||
if not live:
|
if not live:
|
||||||
write_metadata_tag(dest_stream, metadata)
|
write_metadata_tag(dest_stream, metadata)
|
||||||
|
|
||||||
# This dict stores the download progress, it's updated by the progress
|
self._start_frag_download(ctx)
|
||||||
# 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):
|
|
||||||
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 = []
|
||||||
while fragments_list:
|
while fragments_list:
|
||||||
@@ -389,23 +338,26 @@ class F4mFD(FileDownloader):
|
|||||||
url = base_url + name
|
url = base_url + name
|
||||||
if akamai_pv:
|
if akamai_pv:
|
||||||
url += '?' + akamai_pv.strip(';')
|
url += '?' + akamai_pv.strip(';')
|
||||||
frag_filename = '%s-%s' % (tmpfilename, name)
|
if info_dict.get('extra_param_to_segment_url'):
|
||||||
|
url += info_dict.get('extra_param_to_segment_url')
|
||||||
|
frag_filename = '%s-%s' % (ctx['tmpfilename'], name)
|
||||||
try:
|
try:
|
||||||
success = http_dl.download(frag_filename, {'url': url})
|
success = ctx['dl'].download(frag_filename, {'url': url})
|
||||||
if not success:
|
if not success:
|
||||||
return False
|
return False
|
||||||
with open(frag_filename, 'rb') as down:
|
(down, frag_sanitized) = sanitize_open(frag_filename, 'rb')
|
||||||
down_data = down.read()
|
down_data = down.read()
|
||||||
reader = FlvReader(down_data)
|
down.close()
|
||||||
while True:
|
reader = FlvReader(down_data)
|
||||||
_, box_type, box_data = reader.read_box_info()
|
while True:
|
||||||
if box_type == b'mdat':
|
_, box_type, box_data = reader.read_box_info()
|
||||||
dest_stream.write(box_data)
|
if box_type == b'mdat':
|
||||||
break
|
dest_stream.write(box_data)
|
||||||
|
break
|
||||||
if live:
|
if live:
|
||||||
os.remove(frag_filename)
|
os.remove(encodeFilename(frag_sanitized))
|
||||||
else:
|
else:
|
||||||
frags_filenames.append(frag_filename)
|
frags_filenames.append(frag_sanitized)
|
||||||
except (compat_urllib_error.HTTPError, ) as err:
|
except (compat_urllib_error.HTTPError, ) as err:
|
||||||
if live and (err.code == 404 or err.code == 410):
|
if live and (err.code == 404 or err.code == 410):
|
||||||
# We didn't keep up with the live window. Continue
|
# We didn't keep up with the live window. Continue
|
||||||
@@ -423,20 +375,9 @@ class F4mFD(FileDownloader):
|
|||||||
msg = 'Missed %d fragments' % (fragments_list[0][1] - (frag_i + 1))
|
msg = 'Missed %d fragments' % (fragments_list[0][1] - (frag_i + 1))
|
||||||
self.report_warning(msg)
|
self.report_warning(msg)
|
||||||
|
|
||||||
dest_stream.close()
|
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
|
||||||
|
|||||||
111
youtube_dl/downloader/fragment.py
Normal file
111
youtube_dl/downloader/fragment.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
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):
|
||||||
|
self.to_screen('[%s] Total fragments: %d' % (self.FD_NAME, ctx['total_frags']))
|
||||||
|
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['started'] = start
|
||||||
|
|
||||||
|
def frag_progress_hook(s):
|
||||||
|
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)
|
||||||
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
@@ -4,15 +4,15 @@ import os
|
|||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -33,6 +33,8 @@ class HlsFD(FileDownloader):
|
|||||||
for opt in (ffpp.executable, '-y', '-i', url, '-f', 'mp4', '-c', 'copy', '-bsf:a', 'aac_adtstoasc')]
|
for opt in (ffpp.executable, '-y', '-i', url, '-f', 'mp4', '-c', 'copy', '-bsf:a', 'aac_adtstoasc')]
|
||||||
args.append(encodeFilename(tmpfilename, True))
|
args.append(encodeFilename(tmpfilename, True))
|
||||||
|
|
||||||
|
self._debug_cmd(args)
|
||||||
|
|
||||||
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))
|
||||||
@@ -51,54 +53,50 @@ 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())
|
||||||
|
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,6 +4,7 @@ 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 (
|
||||||
@@ -28,13 +29,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 = compat_urllib_request.Request(url, None, headers)
|
||||||
http_method = info_dict.get('http_method')
|
request = compat_urllib_request.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)
|
||||||
|
|
||||||
@@ -62,6 +58,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:
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ class RtmpFD(FileDownloader):
|
|||||||
# possible. This is part of rtmpdump's normal usage, AFAIK.
|
# possible. This is part of rtmpdump's normal usage, AFAIK.
|
||||||
basic_args = [
|
basic_args = [
|
||||||
'rtmpdump', '--verbose', '-r', url,
|
'rtmpdump', '--verbose', '-r', url,
|
||||||
'-o', encodeFilename(tmpfilename, True)]
|
'-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:
|
||||||
@@ -131,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:
|
||||||
@@ -154,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
|
||||||
@@ -180,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
|
||||||
@@ -4,7 +4,10 @@ from .abc import ABCIE
|
|||||||
from .abc7news import Abc7NewsIE
|
from .abc7news import Abc7NewsIE
|
||||||
from .academicearth import AcademicEarthCourseIE
|
from .academicearth import AcademicEarthCourseIE
|
||||||
from .addanime import AddAnimeIE
|
from .addanime import AddAnimeIE
|
||||||
from .adobetv import AdobeTVIE
|
from .adobetv import (
|
||||||
|
AdobeTVIE,
|
||||||
|
AdobeTVVideoIE,
|
||||||
|
)
|
||||||
from .adultswim import AdultSwimIE
|
from .adultswim import AdultSwimIE
|
||||||
from .aftenposten import AftenpostenIE
|
from .aftenposten import AftenpostenIE
|
||||||
from .aftonbladet import AftonbladetIE
|
from .aftonbladet import AftonbladetIE
|
||||||
@@ -16,9 +19,14 @@ 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 .appleconnect import AppleConnectIE
|
||||||
from .appletrailers import AppleTrailersIE
|
from .appletrailers import AppleTrailersIE
|
||||||
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,
|
||||||
@@ -32,9 +40,13 @@ from .atresplayer import AtresPlayerIE
|
|||||||
from .atttechchannel import ATTTechChannelIE
|
from .atttechchannel import ATTTechChannelIE
|
||||||
from .audiomack import AudiomackIE, AudiomackAlbumIE
|
from .audiomack import AudiomackIE, AudiomackAlbumIE
|
||||||
from .azubu import AzubuIE
|
from .azubu import AzubuIE
|
||||||
|
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,
|
||||||
|
BBCIE,
|
||||||
|
)
|
||||||
from .beeg import BeegIE
|
from .beeg import BeegIE
|
||||||
from .behindkink import BehindKinkIE
|
from .behindkink import BehindKinkIE
|
||||||
from .beatportpro import BeatportProIE
|
from .beatportpro import BeatportProIE
|
||||||
@@ -70,6 +82,7 @@ from .chirbit import (
|
|||||||
ChirbitProfileIE,
|
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
|
||||||
@@ -90,6 +103,7 @@ 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
|
||||||
@@ -100,20 +114,28 @@ from .dailymotion import (
|
|||||||
DailymotionIE,
|
DailymotionIE,
|
||||||
DailymotionPlaylistIE,
|
DailymotionPlaylistIE,
|
||||||
DailymotionUserIE,
|
DailymotionUserIE,
|
||||||
|
DailymotionCloudIE,
|
||||||
)
|
)
|
||||||
from .daum import DaumIE
|
from .daum import DaumIE
|
||||||
from .dbtv import DBTVIE
|
from .dbtv import DBTVIE
|
||||||
|
from .dcn import DCNIE
|
||||||
from .dctp import DctpTvIE
|
from .dctp import DctpTvIE
|
||||||
from .deezer import DeezerPlaylistIE
|
from .deezer import DeezerPlaylistIE
|
||||||
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 .douyutv import DouyuTVIE
|
||||||
|
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 .divxstage import DivxStageIE
|
||||||
@@ -131,11 +153,12 @@ 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 .everyonesmixtape import EveryonesMixtapeIE
|
from .everyonesmixtape import EveryonesMixtapeIE
|
||||||
from .exfm import ExfmIE
|
from .exfm import ExfmIE
|
||||||
from .expotv import ExpoTVIE
|
from .expotv import ExpoTVIE
|
||||||
@@ -143,10 +166,10 @@ from .extremetube import ExtremeTubeIE
|
|||||||
from .facebook import FacebookIE
|
from .facebook import FacebookIE
|
||||||
from .faz import FazIE
|
from .faz import FazIE
|
||||||
from .fc2 import FC2IE
|
from .fc2 import FC2IE
|
||||||
from .firedrive import FiredriveIE
|
|
||||||
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 .fivetv import FiveTVIE
|
||||||
from .fktv import (
|
from .fktv import (
|
||||||
FKTVIE,
|
FKTVIE,
|
||||||
FKTVPosteckeIE,
|
FKTVPosteckeIE,
|
||||||
@@ -157,6 +180,7 @@ from .footyroom import FootyRoomIE
|
|||||||
from .fourtube import FourTubeIE
|
from .fourtube import FourTubeIE
|
||||||
from .foxgay import FoxgayIE
|
from .foxgay import FoxgayIE
|
||||||
from .foxnews import FoxNewsIE
|
from .foxnews import FoxNewsIE
|
||||||
|
from .foxsports import FoxSportsIE
|
||||||
from .franceculture import FranceCultureIE
|
from .franceculture import FranceCultureIE
|
||||||
from .franceinter import FranceInterIE
|
from .franceinter import FranceInterIE
|
||||||
from .francetv import (
|
from .francetv import (
|
||||||
@@ -175,12 +199,14 @@ 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 .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
|
||||||
@@ -192,7 +218,6 @@ from .googleplus import GooglePlusIE
|
|||||||
from .googlesearch import GoogleSearchIE
|
from .googlesearch import GoogleSearchIE
|
||||||
from .gorillavid import GorillaVidIE
|
from .gorillavid import GorillaVidIE
|
||||||
from .goshgay import GoshgayIE
|
from .goshgay import GoshgayIE
|
||||||
from .grooveshark import GroovesharkIE
|
|
||||||
from .groupon import GrouponIE
|
from .groupon import GrouponIE
|
||||||
from .hark import HarkIE
|
from .hark import HarkIE
|
||||||
from .hearthisat import HearThisAtIE
|
from .hearthisat import HearThisAtIE
|
||||||
@@ -216,12 +241,21 @@ from .imdb import (
|
|||||||
ImdbIE,
|
ImdbIE,
|
||||||
ImdbListIE
|
ImdbListIE
|
||||||
)
|
)
|
||||||
from .imgur import ImgurIE
|
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
|
||||||
@@ -236,6 +270,7 @@ from .kaltura import KalturaIE
|
|||||||
from .kanalplay import KanalPlayIE
|
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
|
||||||
@@ -243,15 +278,27 @@ from .keek import KeekIE
|
|||||||
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 .lecture2go import Lecture2GoIE
|
||||||
from .letv import (
|
from .letv import (
|
||||||
LetvIE,
|
LetvIE,
|
||||||
LetvTvIE,
|
LetvTvIE,
|
||||||
LetvPlaylistIE
|
LetvPlaylistIE
|
||||||
)
|
)
|
||||||
from .libsyn import LibsynIE
|
from .libsyn import LibsynIE
|
||||||
from .lifenews import LifeNewsIE
|
from .lifenews import (
|
||||||
|
LifeNewsIE,
|
||||||
|
LifeEmbedIE,
|
||||||
|
)
|
||||||
from .liveleak import LiveLeakIE
|
from .liveleak import LiveLeakIE
|
||||||
from .livestream import (
|
from .livestream import (
|
||||||
LivestreamIE,
|
LivestreamIE,
|
||||||
@@ -269,11 +316,13 @@ from .macgamestore import MacGameStoreIE
|
|||||||
from .mailru import MailRuIE
|
from .mailru import MailRuIE
|
||||||
from .malemotion import MalemotionIE
|
from .malemotion import MalemotionIE
|
||||||
from .mdr import MDRIE
|
from .mdr import MDRIE
|
||||||
|
from .megavideoz import MegaVideozIE
|
||||||
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
|
||||||
@@ -294,13 +343,15 @@ 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 .nationalgeographic import NationalGeographicIE
|
||||||
@@ -309,19 +360,33 @@ from .nba import NBAIE
|
|||||||
from .nbc import (
|
from .nbc import (
|
||||||
NBCIE,
|
NBCIE,
|
||||||
NBCNewsIE,
|
NBCNewsIE,
|
||||||
|
NBCSportsIE,
|
||||||
|
NBCSportsVPlayerIE,
|
||||||
|
MSNBCIE,
|
||||||
|
)
|
||||||
|
from .ndr import (
|
||||||
|
NDRIE,
|
||||||
|
NJoyIE,
|
||||||
)
|
)
|
||||||
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 .nfb import NFBIE
|
from .nfb import NFBIE
|
||||||
from .nfl import NFLIE
|
from .nfl import NFLIE
|
||||||
@@ -335,15 +400,18 @@ 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 .nova import NovaIE
|
||||||
from .novamov import NovaMovIE
|
from .novamov import NovaMovIE
|
||||||
from .nowness import NownessIE
|
from .nowness import NownessIE
|
||||||
|
from .nowtv import NowTVIE
|
||||||
from .nowvideo import NowVideoIE
|
from .nowvideo import NowVideoIE
|
||||||
from .npo import (
|
from .npo import (
|
||||||
NPOIE,
|
NPOIE,
|
||||||
NPOLiveIE,
|
NPOLiveIE,
|
||||||
NPORadioIE,
|
NPORadioIE,
|
||||||
NPORadioFragmentIE,
|
NPORadioFragmentIE,
|
||||||
TegenlichtVproIE,
|
VPROIE,
|
||||||
|
WNLIE
|
||||||
)
|
)
|
||||||
from .nrk import (
|
from .nrk import (
|
||||||
NRKIE,
|
NRKIE,
|
||||||
@@ -352,11 +420,18 @@ from .nrk import (
|
|||||||
)
|
)
|
||||||
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 .odnoklassniki import OdnoklassnikiIE
|
||||||
from .oktoberfesttv import OktoberfestTVIE
|
from .oktoberfesttv import OktoberfestTVIE
|
||||||
from .ooyala import OoyalaIE
|
from .onionstudios import OnionStudiosIE
|
||||||
|
from .ooyala import (
|
||||||
|
OoyalaIE,
|
||||||
|
OoyalaExternalIE,
|
||||||
|
)
|
||||||
from .openfilm import OpenFilmIE
|
from .openfilm import OpenFilmIE
|
||||||
from .orf import (
|
from .orf import (
|
||||||
ORFTVthekIE,
|
ORFTVthekIE,
|
||||||
@@ -367,34 +442,56 @@ from .orf import (
|
|||||||
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,
|
||||||
|
QuickscopeIE,
|
||||||
|
)
|
||||||
|
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 .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 .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 (
|
from .pornhub import (
|
||||||
PornHubIE,
|
PornHubIE,
|
||||||
PornHubPlaylistIE,
|
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 .primesharetv import PrimeShareTVIE
|
||||||
from .promptfile import PromptFileIE
|
from .promptfile import PromptFileIE
|
||||||
from .prosiebensat1 import ProSiebenSat1IE
|
from .prosiebensat1 import ProSiebenSat1IE
|
||||||
from .puls4 import Puls4IE
|
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 .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 RaiIE
|
||||||
from .rbmaradio import RBMARadioIE
|
from .rbmaradio import RBMARadioIE
|
||||||
|
from .rds import RDSIE
|
||||||
from .redtube import RedTubeIE
|
from .redtube import RedTubeIE
|
||||||
from .restudy import RestudyIE
|
from .restudy import RestudyIE
|
||||||
from .reverbnation import ReverbNationIE
|
from .reverbnation import ReverbNationIE
|
||||||
@@ -405,11 +502,11 @@ from .roxwel import RoxwelIE
|
|||||||
from .rtbf import RTBFIE
|
from .rtbf import RTBFIE
|
||||||
from .rte import RteIE
|
from .rte import RteIE
|
||||||
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, RTVEInfantilIE
|
from .rtve import RTVEALaCartaIE, RTVELiveIE, RTVEInfantilIE
|
||||||
|
from .rtvnh import RTVNHIE
|
||||||
from .ruhd import RUHDIE
|
from .ruhd import RUHDIE
|
||||||
from .rutube import (
|
from .rutube import (
|
||||||
RutubeIE,
|
RutubeIE,
|
||||||
@@ -419,6 +516,7 @@ 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 (
|
from .safari import (
|
||||||
SafariIE,
|
SafariIE,
|
||||||
@@ -430,10 +528,12 @@ 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
|
||||||
@@ -445,9 +545,16 @@ 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 .soompi import (
|
||||||
|
SoompiIE,
|
||||||
|
SoompiShowIE,
|
||||||
|
)
|
||||||
from .soundcloud import (
|
from .soundcloud import (
|
||||||
SoundcloudIE,
|
SoundcloudIE,
|
||||||
SoundcloudSetIE,
|
SoundcloudSetIE,
|
||||||
@@ -460,16 +567,24 @@ from .soundgasm import (
|
|||||||
)
|
)
|
||||||
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 .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 .srf import SrfIE
|
||||||
from .srmediathek import SRMediathekIE
|
from .srmediathek import SRMediathekIE
|
||||||
from .ssa import SSAIE
|
from .ssa import SSAIE
|
||||||
from .stanfordoc import StanfordOpenClassroomIE
|
from .stanfordoc import StanfordOpenClassroomIE
|
||||||
@@ -478,7 +593,10 @@ 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
|
||||||
@@ -495,6 +613,7 @@ from .techtalks import TechTalksIE
|
|||||||
from .ted import TEDIE
|
from .ted import TEDIE
|
||||||
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
|
||||||
@@ -502,13 +621,24 @@ from .testurl import TestURLIE
|
|||||||
from .testtube import TestTubeIE
|
from .testtube import TestTubeIE
|
||||||
from .tf1 import TF1IE
|
from .tf1 import TF1IE
|
||||||
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 TlcIE, TlcDeIE
|
||||||
from .tmz import TMZIE
|
from .tmz import (
|
||||||
from .tnaflix import TNAFlixIE
|
TMZIE,
|
||||||
|
TMZArticleIE,
|
||||||
|
)
|
||||||
|
from .tnaflix import (
|
||||||
|
TNAFlixIE,
|
||||||
|
EMPFlixIE,
|
||||||
|
MovieFapIE,
|
||||||
|
)
|
||||||
from .thvideo import (
|
from .thvideo import (
|
||||||
THVideoIE,
|
THVideoIE,
|
||||||
THVideoPlaylistIE
|
THVideoPlaylistIE
|
||||||
@@ -519,12 +649,21 @@ from .traileraddict import TrailerAddictIE
|
|||||||
from .trilulilu import TriluliluIE
|
from .trilulilu import TriluliluIE
|
||||||
from .trutube import TruTubeIE
|
from .trutube import TruTubeIE
|
||||||
from .tube8 import Tube8IE
|
from .tube8 import Tube8IE
|
||||||
|
from .tubitv import TubiTvIE
|
||||||
from .tudou import TudouIE
|
from .tudou import TudouIE
|
||||||
from .tumblr import TumblrIE
|
from .tumblr import TumblrIE
|
||||||
from .tunein import TuneInIE
|
from .tunein import TuneInIE
|
||||||
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 .tv4 import TV4IE
|
||||||
|
from .tvc import (
|
||||||
|
TVCIE,
|
||||||
|
TVCArticleIE,
|
||||||
|
)
|
||||||
from .tvigle import TvigleIE
|
from .tvigle import TvigleIE
|
||||||
from .tvp import TvpIE, TvpSeriesIE
|
from .tvp import TvpIE, TvpSeriesIE
|
||||||
from .tvplay import TVPlayIE
|
from .tvplay import TVPlayIE
|
||||||
@@ -543,26 +682,32 @@ from .twitch import (
|
|||||||
TwitchBookmarksIE,
|
TwitchBookmarksIE,
|
||||||
TwitchStreamIE,
|
TwitchStreamIE,
|
||||||
)
|
)
|
||||||
|
from .twitter import TwitterCardIE
|
||||||
from .ubu import UbuIE
|
from .ubu import UbuIE
|
||||||
from .udemy import (
|
from .udemy import (
|
||||||
UdemyIE,
|
UdemyIE,
|
||||||
UdemyCourseIE
|
UdemyCourseIE
|
||||||
)
|
)
|
||||||
|
from .udn import UDNEmbedIE
|
||||||
from .ultimedia import UltimediaIE
|
from .ultimedia import UltimediaIE
|
||||||
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 .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 .videolecturesnet import VideoLecturesNetIE
|
||||||
from .videofyme import VideofyMeIE
|
from .videofyme import VideofyMeIE
|
||||||
@@ -589,12 +734,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
|
||||||
@@ -609,7 +759,10 @@ 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 .wimp import WimpIE
|
from .wimp import WimpIE
|
||||||
from .wistia import WistiaIE
|
from .wistia import WistiaIE
|
||||||
@@ -618,12 +771,16 @@ 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 .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,
|
||||||
@@ -636,6 +793,7 @@ from .yandexmusic import (
|
|||||||
YandexMusicPlaylistIE,
|
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
|
||||||
|
|||||||
@@ -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/[^/]+/[^/]+/(?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,22 +23,47 @@ 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',
|
||||||
},
|
},
|
||||||
}
|
}, {
|
||||||
|
'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'],
|
||||||
|
}]
|
||||||
|
|
||||||
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|YouTube)Data\.push\((?P<json_data>[^)]+)\);',
|
||||||
flags=re.DOTALL)
|
webpage)
|
||||||
urls_info = json.loads(urls_info_json.replace('\'', '"'))
|
if mobj is None:
|
||||||
|
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']),
|
'width': int_or_none(url_info.get('width')),
|
||||||
'height': int(url_info['height']),
|
'height': int_or_none(url_info.get('height')),
|
||||||
'tbr': int(url_info['bitrate']),
|
'tbr': int_or_none(url_info.get('bitrate')),
|
||||||
'filesize': int(url_info['filesize']),
|
'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)
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ from ..utils import (
|
|||||||
parse_duration,
|
parse_duration,
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
str_to_int,
|
str_to_int,
|
||||||
|
float_or_none,
|
||||||
|
ISO639Utils,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -69,3 +71,61 @@ class AdobeTVIE(InfoExtractor):
|
|||||||
'view_count': view_count,
|
'view_count': view_count,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
player_params = self._parse_json(self._search_regex(
|
||||||
|
r'var\s+bridge\s*=\s*([^;]+);', webpage, 'player parameters'),
|
||||||
|
video_id)
|
||||||
|
|
||||||
|
formats = [{
|
||||||
|
'url': source['src'],
|
||||||
|
'width': source.get('width'),
|
||||||
|
'height': source.get('height'),
|
||||||
|
'tbr': source.get('bitrate'),
|
||||||
|
} for source in player_params['sources']]
|
||||||
|
|
||||||
|
# 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 player_params['sources']]))
|
||||||
|
|
||||||
|
subtitles = {}
|
||||||
|
for translation in player_params.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': player_params['title'],
|
||||||
|
'description': self._og_search_description(webpage),
|
||||||
|
'duration': duration,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
}
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ 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')
|
||||||
|
|
||||||
for file_el in file_els:
|
for file_el in file_els:
|
||||||
bitrate = file_el.attrib.get('bitrate')
|
bitrate = file_el.attrib.get('bitrate')
|
||||||
|
|||||||
@@ -1,21 +1,11 @@
|
|||||||
# 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 (
|
|
||||||
int_or_none,
|
|
||||||
parse_iso8601,
|
|
||||||
xpath_with_ns,
|
|
||||||
xpath_text,
|
|
||||||
find_xpath_attr,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AftenpostenIE(InfoExtractor):
|
class AftenpostenIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?aftenposten\.no/webtv/(?:#!/)?video/(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:www\.)?aftenposten\.no/webtv/(?:#!/)?video/(?P<id>\d+)'
|
||||||
|
|
||||||
_TEST = {
|
_TEST = {
|
||||||
'url': 'http://www.aftenposten.no/webtv/#!/video/21039/trailer-sweatshop-i-can-t-take-any-more',
|
'url': 'http://www.aftenposten.no/webtv/#!/video/21039/trailer-sweatshop-i-can-t-take-any-more',
|
||||||
'md5': 'fd828cd29774a729bf4d4425fe192972',
|
'md5': 'fd828cd29774a729bf4d4425fe192972',
|
||||||
@@ -30,69 +20,4 @@ class AftenpostenIE(InfoExtractor):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
return self.url_result('xstream:ap:%s' % self._match_id(url), 'Xstream')
|
||||||
|
|
||||||
data = self._download_xml(
|
|
||||||
'http://frontend.xstream.dk/ap/feed/video/?platform=web&id=%s' % video_id, video_id)
|
|
||||||
|
|
||||||
NS_MAP = {
|
|
||||||
'atom': 'http://www.w3.org/2005/Atom',
|
|
||||||
'xt': 'http://xstream.dk/',
|
|
||||||
'media': 'http://search.yahoo.com/mrss/',
|
|
||||||
}
|
|
||||||
|
|
||||||
entry = data.find(xpath_with_ns('./atom:entry', NS_MAP))
|
|
||||||
|
|
||||||
title = xpath_text(
|
|
||||||
entry, xpath_with_ns('./atom:title', NS_MAP), 'title')
|
|
||||||
description = xpath_text(
|
|
||||||
entry, xpath_with_ns('./atom:summary', NS_MAP), 'description')
|
|
||||||
timestamp = parse_iso8601(xpath_text(
|
|
||||||
entry, xpath_with_ns('./atom:published', NS_MAP), 'upload date'))
|
|
||||||
|
|
||||||
formats = []
|
|
||||||
media_group = entry.find(xpath_with_ns('./media:group', NS_MAP))
|
|
||||||
for media_content in media_group.findall(xpath_with_ns('./media:content', NS_MAP)):
|
|
||||||
media_url = media_content.get('url')
|
|
||||||
if not media_url:
|
|
||||||
continue
|
|
||||||
tbr = int_or_none(media_content.get('bitrate'))
|
|
||||||
mobj = re.search(r'^(?P<url>rtmp://[^/]+/(?P<app>[^/]+))/(?P<playpath>.+)$', media_url)
|
|
||||||
if mobj:
|
|
||||||
formats.append({
|
|
||||||
'url': mobj.group('url'),
|
|
||||||
'play_path': 'mp4:%s' % mobj.group('playpath'),
|
|
||||||
'app': mobj.group('app'),
|
|
||||||
'ext': 'flv',
|
|
||||||
'tbr': tbr,
|
|
||||||
'format_id': 'rtmp-%d' % tbr,
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
formats.append({
|
|
||||||
'url': media_url,
|
|
||||||
'tbr': tbr,
|
|
||||||
})
|
|
||||||
self._sort_formats(formats)
|
|
||||||
|
|
||||||
link = find_xpath_attr(
|
|
||||||
entry, xpath_with_ns('./atom:link', NS_MAP), 'rel', 'original')
|
|
||||||
if link is not None:
|
|
||||||
formats.append({
|
|
||||||
'url': link.get('href'),
|
|
||||||
'format_id': link.get('rel'),
|
|
||||||
})
|
|
||||||
|
|
||||||
thumbnails = [{
|
|
||||||
'url': splash.get('url'),
|
|
||||||
'width': int_or_none(splash.get('width')),
|
|
||||||
'height': int_or_none(splash.get('height')),
|
|
||||||
} for splash in media_group.findall(xpath_with_ns('./xt:splash', NS_MAP))]
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': video_id,
|
|
||||||
'title': title,
|
|
||||||
'description': description,
|
|
||||||
'timestamp': timestamp,
|
|
||||||
'formats': formats,
|
|
||||||
'thumbnails': thumbnails,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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')),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ class AirMozillaIE(InfoExtractor):
|
|||||||
'id': '6x4q2w',
|
'id': '6x4q2w',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Privacy Lab - a meetup for privacy minded people in San Francisco',
|
'title': 'Privacy Lab - a meetup for privacy minded people in San Francisco',
|
||||||
'thumbnail': 're:https://\w+\.cloudfront\.net/6x4q2w/poster\.jpg\?t=\d+',
|
'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...',
|
'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,
|
'timestamp': 1422487800,
|
||||||
'upload_date': '20150128',
|
'upload_date': '20150128',
|
||||||
'location': 'SFO Commons',
|
'location': 'SFO Commons',
|
||||||
'duration': 3780,
|
'duration': 3780,
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
'categories': ['Main'],
|
'categories': ['Main', 'Privacy'],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class AlJazeeraIE(InfoExtractor):
|
|||||||
'uploader': 'Al Jazeera English',
|
'uploader': 'Al Jazeera English',
|
||||||
},
|
},
|
||||||
'add_ie': ['Brightcove'],
|
'add_ie': ['Brightcove'],
|
||||||
|
'skip': 'Not accessible from Travis CI server',
|
||||||
}
|
}
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
|||||||
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,
|
||||||
|
}
|
||||||
@@ -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,6 +8,7 @@ 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,
|
||||||
@@ -22,19 +23,125 @@ 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 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'))
|
||||||
|
elif ext == 'm3u8':
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
stream_url, video_id, 'mp4', preference=1, m3u8_id='hls'))
|
||||||
|
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)
|
||||||
@@ -92,46 +199,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):
|
||||||
@@ -189,3 +272,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
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from .common import InfoExtractor
|
|||||||
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,
|
||||||
qualities,
|
qualities,
|
||||||
@@ -195,7 +194,9 @@ class ArteTVFutureIE(ArteTVPlus7IE):
|
|||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
anchor_id, lang = self._extract_url_info(url)
|
anchor_id, lang = self._extract_url_info(url)
|
||||||
webpage = self._download_webpage(url, anchor_id)
|
webpage = self._download_webpage(url, anchor_id)
|
||||||
row = get_element_by_id(anchor_id, webpage)
|
row = self._search_regex(
|
||||||
|
r'(?s)id="%s"[^>]*>.+?(<div[^>]*arte_vp_url[^>]*>)' % anchor_id,
|
||||||
|
webpage, 'row')
|
||||||
return self._extract_from_webpage(row, anchor_id, lang)
|
return self._extract_from_webpage(row, anchor_id, lang)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
69
youtube_dl/extractor/baidu.py
Normal file
69
youtube_dl/extractor/baidu.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..compat import compat_urlparse
|
||||||
|
|
||||||
|
|
||||||
|
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版 (全52集)',
|
||||||
|
'description': 'md5:395a419e41215e531c857bb037bbaf80',
|
||||||
|
},
|
||||||
|
'playlist_count': 52,
|
||||||
|
}, {
|
||||||
|
'url': 'http://v.baidu.com/show/11595.htm?frp=bdbrand',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '11595',
|
||||||
|
'title': 're:^奔跑吧兄弟',
|
||||||
|
'description': 'md5:1bf88bad6d850930f542d51547c089b8',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 3,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
mobj = re.match(self._VALID_URL, url)
|
||||||
|
playlist_id = mobj.group('id')
|
||||||
|
category = category2 = mobj.group('type')
|
||||||
|
if category == 'show':
|
||||||
|
category2 = 'tvshow'
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, playlist_id)
|
||||||
|
|
||||||
|
playlist_title = self._html_search_regex(
|
||||||
|
r'title\s*:\s*(["\'])(?P<title>[^\']+)\1', webpage,
|
||||||
|
'playlist title', group='title')
|
||||||
|
playlist_description = self._html_search_regex(
|
||||||
|
r'<input[^>]+class="j-data-intro"[^>]+value="([^"]+)"/>', webpage,
|
||||||
|
playlist_id, 'playlist description')
|
||||||
|
|
||||||
|
site = self._html_search_regex(
|
||||||
|
r'filterSite\s*:\s*["\']([^"]*)["\']', webpage,
|
||||||
|
'primary provider site')
|
||||||
|
api_result = self._download_json(
|
||||||
|
'http://v.baidu.com/%s_intro/?dtype=%sPlayUrl&id=%s&site=%s' % (
|
||||||
|
category, category2, playlist_id, site),
|
||||||
|
playlist_id, 'Get playlist links')
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
for episode in api_result[0]['episodes']:
|
||||||
|
episode_id = '%s_%s' % (playlist_id, episode['episode'])
|
||||||
|
|
||||||
|
redirect_page = self._download_webpage(
|
||||||
|
compat_urlparse.urljoin(url, episode['url']), episode_id,
|
||||||
|
note='Download Baidu redirect page')
|
||||||
|
real_url = self._html_search_regex(
|
||||||
|
r'location\.replace\("([^"]+)"\)', redirect_page, 'real URL')
|
||||||
|
|
||||||
|
entries.append(self.url_result(
|
||||||
|
real_url, video_title=episode['single_title']))
|
||||||
|
|
||||||
|
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_parse,
|
||||||
compat_urllib_request,
|
compat_urllib_request,
|
||||||
|
compat_str,
|
||||||
|
)
|
||||||
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
int_or_none,
|
||||||
|
float_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -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 = compat_urllib_request.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')),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class BandcampIE(InfoExtractor):
|
|||||||
|
|
||||||
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')
|
||||||
|
|||||||
780
youtube_dl/extractor/bbc.py
Normal file
780
youtube_dl/extractor/bbc.py
Normal file
@@ -0,0 +1,780 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
import xml.etree.ElementTree
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
float_or_none,
|
||||||
|
int_or_none,
|
||||||
|
parse_duration,
|
||||||
|
parse_iso8601,
|
||||||
|
)
|
||||||
|
from ..compat import compat_HTTPError
|
||||||
|
|
||||||
|
|
||||||
|
class BBCCoUkIE(InfoExtractor):
|
||||||
|
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})'
|
||||||
|
|
||||||
|
_MEDIASELECTOR_URLS = [
|
||||||
|
'http://open.live.bbc.co.uk/mediaselector/5/select/version/2.0/mediaset/pc/vpid/%s',
|
||||||
|
]
|
||||||
|
|
||||||
|
_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/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',
|
||||||
|
}, {
|
||||||
|
'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,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
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 = []
|
||||||
|
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
|
||||||
|
# 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 BBCCoUkIE.MediaSelectionError(error.get('id'))
|
||||||
|
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_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({
|
||||||
|
'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_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 == 'notukerror':
|
||||||
|
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 == 403:
|
||||||
|
media_selection = xml.etree.ElementTree.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('./{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
|
||||||
|
|
||||||
|
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('./{http://bbc.co.uk/2008/emp/playlist}mediator')
|
||||||
|
if mediator is not None:
|
||||||
|
return get_from_attributes(mediator)
|
||||||
|
|
||||||
|
programme_id = get_programme_id(item)
|
||||||
|
duration = int_or_none(item.get('duration'))
|
||||||
|
# TODO: programme_id can be None and media items can be incorporated right inside
|
||||||
|
# playlist's item (e.g. http://www.bbc.com/turkce/haberler/2015/06/150615_telabyad_kentin_cogu)
|
||||||
|
# as f4m and m3u8
|
||||||
|
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 = 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*"([\da-z]{8})"', webpage, 'vpid', fatal=False, default=None)
|
||||||
|
|
||||||
|
if programme_id:
|
||||||
|
formats, subtitles = self._download_media_selector(programme_id)
|
||||||
|
title = self._og_search_title(webpage)
|
||||||
|
description = self._search_regex(
|
||||||
|
r'<p class="[^"]*medium-description[^"]*">([^<]+)</p>',
|
||||||
|
webpage, 'description', fatal=False)
|
||||||
|
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 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-media-meta containing
|
||||||
|
# playlist.sxml, externalId and no direct video links
|
||||||
|
'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-media-meta (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()`
|
||||||
|
'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 mediaAssetPage.init()
|
||||||
|
'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',
|
||||||
|
'duration': 47,
|
||||||
|
'timestamp': 1427219242,
|
||||||
|
'upload_date': '20150324',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# rtmp download
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
# article with single video embedded with data-media-meta containing
|
||||||
|
# direct video links (for now these are extracted) and playlist.xml (with
|
||||||
|
# media items as f4m and m3u8 - currently unsupported)
|
||||||
|
'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",
|
||||||
|
'duration': 47,
|
||||||
|
'timestamp': 1434397334,
|
||||||
|
'upload_date': '20150615',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
# single video embedded with mediaAssetPage.init() (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',
|
||||||
|
'duration': 87,
|
||||||
|
'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,
|
||||||
|
},
|
||||||
|
'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': 1368473503,
|
||||||
|
'upload_date': '20130513',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# rtmp download
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
# single video with playlist.sxml URL
|
||||||
|
'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': 'md5:398fca0e2e701c609d726e034fa1fc89',
|
||||||
|
'duration': 140,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# rtmp download
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
# 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) 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 _real_extract(self, url):
|
||||||
|
playlist_id = self._match_id(url)
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, playlist_id)
|
||||||
|
|
||||||
|
timestamp = parse_iso8601(self._search_regex(
|
||||||
|
[r'"datePublished":\s*"([^"]+)',
|
||||||
|
r'<meta[^>]+property="article:published_time"[^>]+content="([^"]+)"',
|
||||||
|
r'itemprop="datePublished"[^>]+datetime="([^"]+)"'],
|
||||||
|
webpage, 'date', default=None))
|
||||||
|
|
||||||
|
# single video with playlist.sxml URL (e.g. http://www.bbc.com/sport/0/football/3365340ng)
|
||||||
|
playlist = self._search_regex(
|
||||||
|
r'<param[^>]+name="playlist"[^>]+value="([^"]+)"',
|
||||||
|
webpage, 'playlist', default=None)
|
||||||
|
if playlist:
|
||||||
|
programme_id, title, description, duration, formats, subtitles = \
|
||||||
|
self._process_legacy_playlist_url(playlist, playlist_id)
|
||||||
|
self._sort_formats(formats)
|
||||||
|
return {
|
||||||
|
'id': programme_id,
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'duration': duration,
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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="([\da-z]{8})"',
|
||||||
|
r'<param[^>]+name="externalIdentifier"[^>]+value="([\da-z]{8})"'],
|
||||||
|
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/(?:[^/]+/)+[\da-z]{8}(?:\b[^"]+)?'
|
||||||
|
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)
|
||||||
@@ -1,351 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import xml.etree.ElementTree
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
|
||||||
from ..utils import ExtractorError
|
|
||||||
from ..compat import compat_HTTPError
|
|
||||||
|
|
||||||
|
|
||||||
class BBCCoUkIE(InfoExtractor):
|
|
||||||
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 _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')
|
|
||||||
ps = captions.findall('./{0}body/{0}div/{0}p'.format('{http://www.w3.org/2006/10/ttaf1}'))
|
|
||||||
srt = ''
|
|
||||||
|
|
||||||
def _extract_text(p):
|
|
||||||
if p.text is not None:
|
|
||||||
stripped_text = p.text.strip()
|
|
||||||
if stripped_text:
|
|
||||||
return stripped_text
|
|
||||||
return ' '.join(span.text.strip() for span in p.findall('{http://www.w3.org/2006/10/ttaf1}span'))
|
|
||||||
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'), _extract_text(p))
|
|
||||||
subtitles[lang] = [
|
|
||||||
{
|
|
||||||
'url': connection.get('href'),
|
|
||||||
'ext': 'ttml',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'data': srt,
|
|
||||||
'ext': '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_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(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)
|
|
||||||
|
|
||||||
self._sort_formats(formats)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': programme_id,
|
|
||||||
'title': title,
|
|
||||||
'description': description,
|
|
||||||
'duration': duration,
|
|
||||||
'formats': formats,
|
|
||||||
'subtitles': subtitles,
|
|
||||||
}
|
|
||||||
@@ -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))
|
||||||
|
|||||||
@@ -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,
|
||||||
|
fix_xml_ampersands,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BildIE(InfoExtractor):
|
class BildIE(InfoExtractor):
|
||||||
@@ -15,7 +18,7 @@ class BildIE(InfoExtractor):
|
|||||||
'id': '38184146',
|
'id': '38184146',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'BILD hat sie getestet',
|
'title': 'BILD hat sie getestet',
|
||||||
'thumbnail': 'http://bilder.bild.de/fotos/stand-das-koennen-die-neuen-ipads-38184138/Bild/1.bild.jpg',
|
'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. ',
|
'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. ',
|
||||||
}
|
}
|
||||||
@@ -25,7 +28,7 @@ class BildIE(InfoExtractor):
|
|||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
xml_url = url.split(".bild.html")[0] + ",view=xml.bild.xml"
|
xml_url = url.split(".bild.html")[0] + ",view=xml.bild.xml"
|
||||||
doc = self._download_xml(xml_url, video_id)
|
doc = self._download_xml(xml_url, video_id, transform_source=fix_xml_ampersands)
|
||||||
|
|
||||||
duration = int_or_none(doc.attrib.get('duration'), scale=1000)
|
duration = int_or_none(doc.attrib.get('duration'), scale=1000)
|
||||||
|
|
||||||
|
|||||||
@@ -2,34 +2,54 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
import itertools
|
||||||
|
import json
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
int_or_none,
|
int_or_none,
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
|
ExtractorError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
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>[0-9]+)/'
|
||||||
|
|
||||||
_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': '1074402_part1',
|
||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
'title': '【金坷垃】金泡沫',
|
'title': '【金坷垃】金泡沫',
|
||||||
'duration': 308,
|
'duration': 308,
|
||||||
'upload_date': '20140420',
|
'upload_date': '20140420',
|
||||||
'thumbnail': 're:^https?://.+\.jpg',
|
'thumbnail': 're:^https?://.+\.jpg',
|
||||||
},
|
},
|
||||||
}
|
}, {
|
||||||
|
'url': 'http://www.bilibili.com/video/av1041170/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1041170',
|
||||||
|
'title': '【BD1080P】刀语【诸神&异域】',
|
||||||
|
},
|
||||||
|
'playlist_count': 9,
|
||||||
|
}]
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
if '(此视频不存在或被删除)' in webpage:
|
||||||
|
raise ExtractorError(
|
||||||
|
'The video does not exist or was deleted', expected=True)
|
||||||
|
|
||||||
|
if '>你没有权限浏览! 由于版权相关问题 我们不对您所在的地区提供服务<' in webpage:
|
||||||
|
raise ExtractorError(
|
||||||
|
'The video is not available in your region due to copyright reasons',
|
||||||
|
expected=True)
|
||||||
|
|
||||||
video_code = self._search_regex(
|
video_code = self._search_regex(
|
||||||
r'(?s)<div itemprop="video".*?>(.*?)</div>', webpage, 'video code')
|
r'(?s)<div itemprop="video".*?>(.*?)</div>', webpage, 'video code')
|
||||||
|
|
||||||
@@ -54,19 +74,22 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
|
|
||||||
cid = self._search_regex(r'cid=(\d+)', webpage, 'cid')
|
cid = self._search_regex(r'cid=(\d+)', webpage, 'cid')
|
||||||
|
|
||||||
lq_doc = self._download_xml(
|
entries = []
|
||||||
|
|
||||||
|
lq_page = self._download_webpage(
|
||||||
'http://interface.bilibili.com/v_cdn_play?appkey=1&cid=%s' % cid,
|
'http://interface.bilibili.com/v_cdn_play?appkey=1&cid=%s' % cid,
|
||||||
video_id,
|
video_id,
|
||||||
note='Downloading LQ video info'
|
note='Downloading LQ video info'
|
||||||
)
|
)
|
||||||
lq_durl = lq_doc.find('./durl')
|
try:
|
||||||
formats = [{
|
err_info = json.loads(lq_page)
|
||||||
'format_id': 'lq',
|
raise ExtractorError(
|
||||||
'quality': 1,
|
'BiliBili said: ' + err_info['error_text'], expected=True)
|
||||||
'url': lq_durl.find('./url').text,
|
except ValueError:
|
||||||
'filesize': int_or_none(
|
pass
|
||||||
lq_durl.find('./size'), get_attr='text'),
|
|
||||||
}]
|
lq_doc = ET.fromstring(lq_page)
|
||||||
|
lq_durls = lq_doc.findall('./durl')
|
||||||
|
|
||||||
hq_doc = self._download_xml(
|
hq_doc = self._download_xml(
|
||||||
'http://interface.bilibili.com/playurl?appkey=1&cid=%s' % cid,
|
'http://interface.bilibili.com/playurl?appkey=1&cid=%s' % cid,
|
||||||
@@ -75,22 +98,45 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
fatal=False,
|
fatal=False,
|
||||||
)
|
)
|
||||||
if hq_doc is not False:
|
if hq_doc is not False:
|
||||||
hq_durl = hq_doc.find('./durl')
|
hq_durls = hq_doc.findall('./durl')
|
||||||
formats.append({
|
assert len(lq_durls) == len(hq_durls)
|
||||||
'format_id': 'hq',
|
else:
|
||||||
'quality': 2,
|
hq_durls = itertools.repeat(None)
|
||||||
'ext': 'flv',
|
|
||||||
'url': hq_durl.find('./url').text,
|
i = 1
|
||||||
|
for lq_durl, hq_durl in zip(lq_durls, hq_durls):
|
||||||
|
formats = [{
|
||||||
|
'format_id': 'lq',
|
||||||
|
'quality': 1,
|
||||||
|
'url': lq_durl.find('./url').text,
|
||||||
'filesize': int_or_none(
|
'filesize': int_or_none(
|
||||||
hq_durl.find('./size'), get_attr='text'),
|
lq_durl.find('./size'), get_attr='text'),
|
||||||
|
}]
|
||||||
|
if hq_durl is not None:
|
||||||
|
formats.append({
|
||||||
|
'format_id': 'hq',
|
||||||
|
'quality': 2,
|
||||||
|
'ext': 'flv',
|
||||||
|
'url': hq_durl.find('./url').text,
|
||||||
|
'filesize': int_or_none(
|
||||||
|
hq_durl.find('./size'), get_attr='text'),
|
||||||
|
})
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
entries.append({
|
||||||
|
'id': '%s_part%d' % (video_id, i),
|
||||||
|
'title': title,
|
||||||
|
'formats': formats,
|
||||||
|
'duration': duration,
|
||||||
|
'upload_date': upload_date,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
})
|
})
|
||||||
|
|
||||||
self._sort_formats(formats)
|
i += 1
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
'_type': 'multi_video',
|
||||||
|
'entries': entries,
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': title,
|
'title': title
|
||||||
'formats': formats,
|
|
||||||
'duration': duration,
|
|
||||||
'upload_date': upload_date,
|
|
||||||
'thumbnail': thumbnail,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import re
|
|||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
compat_str,
|
|
||||||
compat_urllib_request,
|
compat_urllib_request,
|
||||||
compat_urlparse,
|
compat_urlparse,
|
||||||
)
|
)
|
||||||
@@ -14,6 +13,8 @@ from ..utils import (
|
|||||||
int_or_none,
|
int_or_none,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
unescapeHTML,
|
unescapeHTML,
|
||||||
|
xpath_text,
|
||||||
|
xpath_with_ns,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -23,10 +24,10 @@ class BlipTVIE(InfoExtractor):
|
|||||||
_TESTS = [
|
_TESTS = [
|
||||||
{
|
{
|
||||||
'url': 'http://blip.tv/cbr/cbr-exclusive-gotham-city-imposters-bats-vs-jokerz-short-3-5796352',
|
'url': 'http://blip.tv/cbr/cbr-exclusive-gotham-city-imposters-bats-vs-jokerz-short-3-5796352',
|
||||||
'md5': 'c6934ad0b6acf2bd920720ec888eb812',
|
'md5': '80baf1ec5c3d2019037c1c707d676b9f',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '5779306',
|
'id': '5779306',
|
||||||
'ext': 'mov',
|
'ext': 'm4v',
|
||||||
'title': 'CBR EXCLUSIVE: "Gotham City Imposters" Bats VS Jokerz Short 3',
|
'title': 'CBR EXCLUSIVE: "Gotham City Imposters" Bats VS Jokerz Short 3',
|
||||||
'description': 'md5:9bc31f227219cde65e47eeec8d2dc596',
|
'description': 'md5:9bc31f227219cde65e47eeec8d2dc596',
|
||||||
'timestamp': 1323138843,
|
'timestamp': 1323138843,
|
||||||
@@ -100,8 +101,31 @@ class BlipTVIE(InfoExtractor):
|
|||||||
'vcodec': 'none',
|
'vcodec': 'none',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
# missing duration
|
||||||
|
'url': 'http://blip.tv/rss/flash/6700880',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '6684191',
|
||||||
|
'ext': 'm4v',
|
||||||
|
'title': 'Cowboy Bebop: Gateway Shuffle Review',
|
||||||
|
'description': 'md5:3acc480c0f9ae157f5fe88547ecaf3f8',
|
||||||
|
'timestamp': 1386639757,
|
||||||
|
'upload_date': '20131210',
|
||||||
|
'uploader': 'sfdebris',
|
||||||
|
'uploader_id': '706520',
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_url(webpage):
|
||||||
|
mobj = re.search(r'<meta\s[^>]*https?://api\.blip\.tv/\w+/redirect/\w+/(\d+)', webpage)
|
||||||
|
if mobj:
|
||||||
|
return 'http://blip.tv/a/a-' + mobj.group(1)
|
||||||
|
mobj = re.search(r'<(?:iframe|embed|object)\s[^>]*(https?://(?:\w+\.)?blip\.tv/(?:play/|api\.swf#)[a-zA-Z0-9_]+)', webpage)
|
||||||
|
if mobj:
|
||||||
|
return mobj.group(1)
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
lookup_id = mobj.group('lookup_id')
|
lookup_id = mobj.group('lookup_id')
|
||||||
@@ -119,35 +143,34 @@ class BlipTVIE(InfoExtractor):
|
|||||||
|
|
||||||
rss = self._download_xml('http://blip.tv/rss/flash/%s' % video_id, video_id, 'Downloading video RSS')
|
rss = self._download_xml('http://blip.tv/rss/flash/%s' % video_id, video_id, 'Downloading video RSS')
|
||||||
|
|
||||||
def blip(s):
|
def _x(p):
|
||||||
return '{http://blip.tv/dtd/blip/1.0}%s' % s
|
return xpath_with_ns(p, {
|
||||||
|
'blip': 'http://blip.tv/dtd/blip/1.0',
|
||||||
def media(s):
|
'media': 'http://search.yahoo.com/mrss/',
|
||||||
return '{http://search.yahoo.com/mrss/}%s' % s
|
'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd',
|
||||||
|
})
|
||||||
def itunes(s):
|
|
||||||
return '{http://www.itunes.com/dtds/podcast-1.0.dtd}%s' % s
|
|
||||||
|
|
||||||
item = rss.find('channel/item')
|
item = rss.find('channel/item')
|
||||||
|
|
||||||
video_id = item.find(blip('item_id')).text
|
video_id = xpath_text(item, _x('blip:item_id'), 'video id') or lookup_id
|
||||||
title = item.find('./title').text
|
title = xpath_text(item, 'title', 'title', fatal=True)
|
||||||
description = clean_html(compat_str(item.find(blip('puredescription')).text))
|
description = clean_html(xpath_text(item, _x('blip:puredescription'), 'description'))
|
||||||
timestamp = parse_iso8601(item.find(blip('datestamp')).text)
|
timestamp = parse_iso8601(xpath_text(item, _x('blip:datestamp'), 'timestamp'))
|
||||||
uploader = item.find(blip('user')).text
|
uploader = xpath_text(item, _x('blip:user'), 'uploader')
|
||||||
uploader_id = item.find(blip('userid')).text
|
uploader_id = xpath_text(item, _x('blip:userid'), 'uploader id')
|
||||||
duration = int(item.find(blip('runtime')).text)
|
duration = int_or_none(xpath_text(item, _x('blip:runtime'), 'duration'))
|
||||||
media_thumbnail = item.find(media('thumbnail'))
|
media_thumbnail = item.find(_x('media:thumbnail'))
|
||||||
thumbnail = media_thumbnail.get('url') if media_thumbnail is not None else item.find(itunes('image')).text
|
thumbnail = (media_thumbnail.get('url') if media_thumbnail is not None
|
||||||
categories = [category.text for category in item.findall('category')]
|
else xpath_text(item, 'image', 'thumbnail'))
|
||||||
|
categories = [category.text for category in item.findall('category') if category is not None]
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
subtitles_urls = {}
|
subtitles_urls = {}
|
||||||
|
|
||||||
media_group = item.find(media('group'))
|
media_group = item.find(_x('media:group'))
|
||||||
for media_content in media_group.findall(media('content')):
|
for media_content in media_group.findall(_x('media:content')):
|
||||||
url = media_content.get('url')
|
url = media_content.get('url')
|
||||||
role = media_content.get(blip('role'))
|
role = media_content.get(_x('blip:role'))
|
||||||
msg = self._download_webpage(
|
msg = self._download_webpage(
|
||||||
url + '?showplayer=20140425131715&referrer=http://blip.tv&mask=7&skin=flashvars&view=url',
|
url + '?showplayer=20140425131715&referrer=http://blip.tv&mask=7&skin=flashvars&view=url',
|
||||||
video_id, 'Resolving URL for %s' % role)
|
video_id, 'Resolving URL for %s' % role)
|
||||||
@@ -166,12 +189,13 @@ class BlipTVIE(InfoExtractor):
|
|||||||
'url': real_url,
|
'url': real_url,
|
||||||
'format_id': role,
|
'format_id': role,
|
||||||
'format_note': media_type,
|
'format_note': media_type,
|
||||||
'vcodec': media_content.get(blip('vcodec')) or 'none',
|
'vcodec': media_content.get(_x('blip:vcodec')) or 'none',
|
||||||
'acodec': media_content.get(blip('acodec')),
|
'acodec': media_content.get(_x('blip:acodec')),
|
||||||
'filesize': media_content.get('filesize'),
|
'filesize': media_content.get('filesize'),
|
||||||
'width': int_or_none(media_content.get('width')),
|
'width': int_or_none(media_content.get('width')),
|
||||||
'height': int_or_none(media_content.get('height')),
|
'height': int_or_none(media_content.get('height')),
|
||||||
})
|
})
|
||||||
|
self._check_formats(formats, video_id)
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
subtitles = self.extract_subtitles(video_id, subtitles_urls)
|
subtitles = self.extract_subtitles(video_id, subtitles_urls)
|
||||||
|
|||||||
@@ -6,32 +6,39 @@ from .common import InfoExtractor
|
|||||||
|
|
||||||
|
|
||||||
class BloombergIE(InfoExtractor):
|
class BloombergIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://www\.bloomberg\.com/video/(?P<id>.+?)\.html'
|
_VALID_URL = r'https?://www\.bloomberg\.com/news/videos/[^/]+/(?P<id>[^/?#]+)'
|
||||||
|
|
||||||
_TEST = {
|
_TEST = {
|
||||||
'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',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
name = self._match_id(url)
|
name = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, name)
|
webpage = self._download_webpage(url, name)
|
||||||
|
video_id = self._search_regex(r'"bmmrId":"(.+?)"', webpage, 'id')
|
||||||
f4m_url = self._search_regex(
|
|
||||||
r'<source src="(https?://[^"]+\.f4m.*?)"', webpage,
|
|
||||||
'f4m 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']:
|
||||||
|
if stream["muxing_format"] == "TS":
|
||||||
|
formats.extend(self._extract_m3u8_formats(stream['url'], video_id))
|
||||||
|
else:
|
||||||
|
formats.extend(self._extract_f4m_formats(stream['url'], video_id))
|
||||||
|
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),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,27 +16,38 @@ class BRIE(InfoExtractor):
|
|||||||
|
|
||||||
_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': 'Betriebliche Altersvorsorge: Die böse Überraschung',
|
||||||
'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': 'a44396d73ab6a68a69a568fae10705bb',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'c6aae3de-2cf9-43f2-957f-f17fef9afaab',
|
'id': 'a4b83e34-123d-4b81-9f4e-c0d3121a4e05',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Manfred Schreiber ist tot',
|
||||||
|
'description': 'Abendschau kompakt: Manfred Schreiber ist tot',
|
||||||
|
'duration': 26,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'http://www.br.de/radio/br-klassik/sendungen/allegro/premiere-urauffuehrung-the-land-2015-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': '"The Land" von Peeping Tom: Kurzweilig und sehr bewegend',
|
||||||
'duration': 64,
|
'duration': 296,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from ..compat import (
|
|||||||
compat_urllib_parse_urlparse,
|
compat_urllib_parse_urlparse,
|
||||||
compat_urllib_request,
|
compat_urllib_request,
|
||||||
compat_urlparse,
|
compat_urlparse,
|
||||||
|
compat_xml_parse_error,
|
||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
determine_ext,
|
determine_ext,
|
||||||
@@ -117,7 +118,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 = xml.etree.ElementTree.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 +157,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 +195,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 +209,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, {})
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class BYUtvIE(InfoExtractor):
|
|||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'description': 'md5:5438d33774b6bdc662f9485a340401cc',
|
'description': 'md5:5438d33774b6bdc662f9485a340401cc',
|
||||||
'title': 'Season 5 Episode 5',
|
'title': 'Season 5 Episode 5',
|
||||||
'thumbnail': 're:^https?://.*promo.*'
|
'thumbnail': 're:^https?://.*\.jpg$'
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
|
|||||||
@@ -25,14 +25,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': 'b3481d7ca972f61e37420798d0a9d934',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '922470',
|
'id': '1263092',
|
||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
'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,7 +56,7 @@ 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': 'f3a46edcdf28006598ffaf5b30e6a2d4',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '1213714',
|
'id': '1213714',
|
||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
@@ -106,15 +106,11 @@ class CanalplusIE(InfoExtractor):
|
|||||||
continue
|
continue
|
||||||
format_id = fmt.tag
|
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', preference=preference(format_id)))
|
||||||
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, preference=preference(format_id)))
|
||||||
fmt['preference'] = preference(format_id)
|
|
||||||
formats.extend(hds_formats)
|
|
||||||
else:
|
else:
|
||||||
formats.append({
|
formats.append({
|
||||||
'url': format_url,
|
'url': format_url,
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ from .common import InfoExtractor
|
|||||||
|
|
||||||
|
|
||||||
class CBSIE(InfoExtractor):
|
class CBSIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?cbs\.com/shows/[^/]+/(?:video|artist)/(?P<id>[^/]+)/.*'
|
_VALID_URL = r'https?://(?:www\.)?(?:cbs\.com/shows/[^/]+/(?:video|artist)|colbertlateshow\.com/(?:video|podcasts))/[^/]+/(?P<id>[^/]+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.cbs.com/shows/garth-brooks/video/_u7W953k6la293J7EPTd9oHkSPs6Xn6_/connect-chat-feat-garth-brooks/',
|
'url': 'http://www.cbs.com/shows/garth-brooks/video/_u7W953k6la293J7EPTd9oHkSPs6Xn6_/connect-chat-feat-garth-brooks/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '4JUVEwq3wUT7',
|
'id': '4JUVEwq3wUT7',
|
||||||
|
'display_id': 'connect-chat-feat-garth-brooks',
|
||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
'title': 'Connect Chat feat. Garth Brooks',
|
'title': 'Connect Chat feat. Garth Brooks',
|
||||||
'description': 'Connect with country music singer Garth Brooks, as he chats with fans on Wednesday November 27, 2013. Be sure to tune in to Garth Brooks: Live from Las Vegas, Friday November 29, at 9/8c on CBS!',
|
'description': 'Connect with country music singer Garth Brooks, as he chats with fans on Wednesday November 27, 2013. Be sure to tune in to Garth Brooks: Live from Las Vegas, Friday November 29, at 9/8c on CBS!',
|
||||||
@@ -24,6 +25,7 @@ class CBSIE(InfoExtractor):
|
|||||||
'url': 'http://www.cbs.com/shows/liveonletterman/artist/221752/st-vincent/',
|
'url': 'http://www.cbs.com/shows/liveonletterman/artist/221752/st-vincent/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'WWF_5KqY3PK1',
|
'id': 'WWF_5KqY3PK1',
|
||||||
|
'display_id': 'st-vincent',
|
||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
'title': 'Live on Letterman - St. Vincent',
|
'title': 'Live on Letterman - St. Vincent',
|
||||||
'description': 'Live On Letterman: St. Vincent in concert from New York\'s Ed Sullivan Theater on Tuesday, July 16, 2014.',
|
'description': 'Live On Letterman: St. Vincent in concert from New York\'s Ed Sullivan Theater on Tuesday, July 16, 2014.',
|
||||||
@@ -34,12 +36,23 @@ class CBSIE(InfoExtractor):
|
|||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
'_skip': 'Blocked outside the US',
|
'_skip': 'Blocked outside the US',
|
||||||
|
}, {
|
||||||
|
'url': 'http://colbertlateshow.com/video/8GmB0oY0McANFvp2aEffk9jZZZ2YyXxy/the-colbeard/',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.colbertlateshow.com/podcasts/dYSwjqPs_X1tvbV_P2FcPWRa_qT6akTC/in-the-bad-room-with-stephen/',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
display_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, display_id)
|
||||||
real_id = self._search_regex(
|
real_id = self._search_regex(
|
||||||
r"video\.settings\.pid\s*=\s*'([^']+)';",
|
[r"video\.settings\.pid\s*=\s*'([^']+)';", r"cbsplayer\.pid\s*=\s*'([^']+)';"],
|
||||||
webpage, 'real video ID')
|
webpage, 'real video ID')
|
||||||
return self.url_result('theplatform:%s' % real_id)
|
return {
|
||||||
|
'_type': 'url_transparent',
|
||||||
|
'ie_key': 'ThePlatform',
|
||||||
|
'url': 'theplatform:%s' % real_id,
|
||||||
|
'display_id': display_id,
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class CBSNewsIE(InfoExtractor):
|
|||||||
'id': 'fort-hood-shooting-army-downplays-mental-illness-as-cause-of-attack',
|
'id': 'fort-hood-shooting-army-downplays-mental-illness-as-cause-of-attack',
|
||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
'title': 'Fort Hood shooting: Army downplays mental illness as cause of attack',
|
'title': 'Fort Hood shooting: Army downplays mental illness as cause of attack',
|
||||||
'thumbnail': 'http://cbsnews2.cbsistatic.com/hub/i/r/2014/04/04/0c9fbc66-576b-41ca-8069-02d122060dd2/thumbnail/140x90/6dad7a502f88875ceac38202984b6d58/en-0404-werner-replace-640x360.jpg',
|
'thumbnail': 're:^https?://.*\.jpg$',
|
||||||
'duration': 205,
|
'duration': 205,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class CCCIE(InfoExtractor):
|
|||||||
|
|
||||||
_TEST = {
|
_TEST = {
|
||||||
'url': 'http://media.ccc.de/browse/congress/2013/30C3_-_5443_-_en_-_saal_g_-_201312281830_-_introduction_to_processor_design_-_byterazor.html#video',
|
'url': 'http://media.ccc.de/browse/congress/2013/30C3_-_5443_-_en_-_saal_g_-_201312281830_-_introduction_to_processor_design_-_byterazor.html#video',
|
||||||
'md5': '205a365d0d57c0b1e43a12c9ffe8f9be',
|
'md5': '3a1eda8f3a29515d27f5adb967d7e740',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '20131228183',
|
'id': '20131228183',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
@@ -51,7 +51,7 @@ class CCCIE(InfoExtractor):
|
|||||||
|
|
||||||
matches = re.finditer(r'''(?xs)
|
matches = re.finditer(r'''(?xs)
|
||||||
<(?:span|div)\s+class='label\s+filetype'>(?P<format>.*?)</(?:span|div)>\s*
|
<(?:span|div)\s+class='label\s+filetype'>(?P<format>.*?)</(?:span|div)>\s*
|
||||||
<a\s+href='(?P<http_url>[^']+)'>\s*
|
<a\s+download\s+href='(?P<http_url>[^']+)'>\s*
|
||||||
(?:
|
(?:
|
||||||
.*?
|
.*?
|
||||||
<a\s+href='(?P<torrent_url>[^']+\.torrent)'
|
<a\s+href='(?P<torrent_url>[^']+\.torrent)'
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from .common import InfoExtractor
|
|||||||
from ..compat import (
|
from ..compat import (
|
||||||
compat_urllib_request,
|
compat_urllib_request,
|
||||||
compat_urllib_parse,
|
compat_urllib_parse,
|
||||||
|
compat_urllib_parse_unquote,
|
||||||
compat_urllib_parse_urlparse,
|
compat_urllib_parse_urlparse,
|
||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
@@ -16,55 +17,81 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class CeskaTelevizeIE(InfoExtractor):
|
class CeskaTelevizeIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://www\.ceskatelevize\.cz/(porady|ivysilani)/(.+/)?(?P<id>[^?#]+)'
|
_VALID_URL = r'https?://www\.ceskatelevize\.cz/(porady|ivysilani)/(?:[^/]+/)*(?P<id>[^/#?]+)/*(?:[#?].*)?$'
|
||||||
|
_TESTS = [{
|
||||||
_TESTS = [
|
'url': 'http://www.ceskatelevize.cz/ivysilani/ivysilani/10441294653-hyde-park-civilizace/214411058091220',
|
||||||
{
|
'info_dict': {
|
||||||
'url': 'http://www.ceskatelevize.cz/ivysilani/ivysilani/10441294653-hyde-park-civilizace/214411058091220',
|
'id': '61924494876951776',
|
||||||
'info_dict': {
|
'ext': 'mp4',
|
||||||
'id': '214411058091220',
|
'title': 'Hyde Park Civilizace',
|
||||||
'ext': 'mp4',
|
'description': 'md5:fe93f6eda372d150759d11644ebbfb4a',
|
||||||
'title': 'Hyde Park Civilizace',
|
'thumbnail': 're:^https?://.*\.jpg',
|
||||||
'description': 'Věda a současná civilizace. Interaktivní pořad - prostor pro vaše otázky a komentáře',
|
'duration': 3350,
|
||||||
'thumbnail': 're:^https?://.*\.jpg',
|
|
||||||
'duration': 3350,
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
# m3u8 download
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
'params': {
|
||||||
'url': 'http://www.ceskatelevize.cz/ivysilani/10532695142-prvni-republika/bonus/14716-zpevacka-z-duparny-bobina',
|
# m3u8 download
|
||||||
'info_dict': {
|
'skip_download': True,
|
||||||
'id': '14716',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'První republika: Zpěvačka z Dupárny Bobina',
|
|
||||||
'description': 'Sága mapující atmosféru první republiky od r. 1918 do r. 1945.',
|
|
||||||
'thumbnail': 're:^https?://.*\.jpg',
|
|
||||||
'duration': 88.4,
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
# m3u8 download
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]
|
}, {
|
||||||
|
'url': 'http://www.ceskatelevize.cz/ivysilani/10532695142-prvni-republika/bonus/14716-zpevacka-z-duparny-bobina',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '61924494876844374',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'První republika: Zpěvačka z Dupárny Bobina',
|
||||||
|
'description': 'Sága mapující atmosféru první republiky od r. 1918 do r. 1945.',
|
||||||
|
'thumbnail': 're:^https?://.*\.jpg',
|
||||||
|
'duration': 88.4,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# m3u8 download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# video with 18+ caution trailer
|
||||||
|
'url': 'http://www.ceskatelevize.cz/porady/10520528904-queer/215562210900007-bogotart/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '215562210900007-bogotart',
|
||||||
|
'title': 'Queer: Bogotart',
|
||||||
|
'description': 'Alternativní průvodce současným queer světem',
|
||||||
|
},
|
||||||
|
'playlist': [{
|
||||||
|
'info_dict': {
|
||||||
|
'id': '61924494876844842',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Queer: Bogotart (Varování 18+)',
|
||||||
|
'duration': 10.2,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'info_dict': {
|
||||||
|
'id': '61924494877068022',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Queer: Bogotart (Queer)',
|
||||||
|
'thumbnail': 're:^https?://.*\.jpg',
|
||||||
|
'duration': 1558.3,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
'params': {
|
||||||
|
# m3u8 download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
url = url.replace('/porady/', '/ivysilani/').replace('/video/', '')
|
url = url.replace('/porady/', '/ivysilani/').replace('/video/', '')
|
||||||
|
|
||||||
mobj = re.match(self._VALID_URL, url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
video_id = mobj.group('id')
|
playlist_id = mobj.group('id')
|
||||||
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, playlist_id)
|
||||||
|
|
||||||
NOT_AVAILABLE_STRING = 'This content is not available at your territory due to limited copyright.'
|
NOT_AVAILABLE_STRING = 'This content is not available at your territory due to limited copyright.'
|
||||||
if '%s</p>' % NOT_AVAILABLE_STRING in webpage:
|
if '%s</p>' % NOT_AVAILABLE_STRING in webpage:
|
||||||
raise ExtractorError(NOT_AVAILABLE_STRING, expected=True)
|
raise ExtractorError(NOT_AVAILABLE_STRING, expected=True)
|
||||||
|
|
||||||
typ = self._html_search_regex(r'getPlaylistUrl\(\[\{"type":"(.+?)","id":".+?"\}\],', webpage, 'type')
|
typ = self._html_search_regex(
|
||||||
episode_id = self._html_search_regex(r'getPlaylistUrl\(\[\{"type":".+?","id":"(.+?)"\}\],', webpage, 'episode_id')
|
r'getPlaylistUrl\(\[\{"type":"(.+?)","id":".+?"\}\],', webpage, 'type')
|
||||||
|
episode_id = self._html_search_regex(
|
||||||
|
r'getPlaylistUrl\(\[\{"type":".+?","id":"(.+?)"\}\],', webpage, 'episode_id')
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'playlist[0][type]': typ,
|
'playlist[0][type]': typ,
|
||||||
@@ -82,42 +109,52 @@ class CeskaTelevizeIE(InfoExtractor):
|
|||||||
req.add_header('X-Requested-With', 'XMLHttpRequest')
|
req.add_header('X-Requested-With', 'XMLHttpRequest')
|
||||||
req.add_header('Referer', url)
|
req.add_header('Referer', url)
|
||||||
|
|
||||||
playlistpage = self._download_json(req, video_id)
|
playlistpage = self._download_json(req, playlist_id)
|
||||||
|
|
||||||
playlist_url = playlistpage['url']
|
playlist_url = playlistpage['url']
|
||||||
if playlist_url == 'error_region':
|
if playlist_url == 'error_region':
|
||||||
raise ExtractorError(NOT_AVAILABLE_STRING, expected=True)
|
raise ExtractorError(NOT_AVAILABLE_STRING, expected=True)
|
||||||
|
|
||||||
req = compat_urllib_request.Request(compat_urllib_parse.unquote(playlist_url))
|
req = compat_urllib_request.Request(compat_urllib_parse_unquote(playlist_url))
|
||||||
req.add_header('Referer', url)
|
req.add_header('Referer', url)
|
||||||
|
|
||||||
playlist = self._download_json(req, video_id)
|
playlist_title = self._og_search_title(webpage)
|
||||||
|
playlist_description = self._og_search_description(webpage)
|
||||||
|
|
||||||
item = playlist['playlist'][0]
|
playlist = self._download_json(req, playlist_id)['playlist']
|
||||||
formats = []
|
playlist_len = len(playlist)
|
||||||
for format_id, stream_url in item['streamUrls'].items():
|
|
||||||
formats.extend(self._extract_m3u8_formats(stream_url, video_id, 'mp4'))
|
|
||||||
self._sort_formats(formats)
|
|
||||||
|
|
||||||
title = self._og_search_title(webpage)
|
entries = []
|
||||||
description = self._og_search_description(webpage)
|
for item in playlist:
|
||||||
duration = float_or_none(item.get('duration'))
|
formats = []
|
||||||
thumbnail = item.get('previewImageUrl')
|
for format_id, stream_url in item['streamUrls'].items():
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
stream_url, playlist_id, 'mp4', entry_protocol='m3u8_native'))
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
subtitles = {}
|
item_id = item.get('id') or item['assetId']
|
||||||
subs = item.get('subtitles')
|
title = item['title']
|
||||||
if subs:
|
|
||||||
subtitles = self.extract_subtitles(episode_id, subs)
|
|
||||||
|
|
||||||
return {
|
duration = float_or_none(item.get('duration'))
|
||||||
'id': episode_id,
|
thumbnail = item.get('previewImageUrl')
|
||||||
'title': title,
|
|
||||||
'description': description,
|
subtitles = {}
|
||||||
'thumbnail': thumbnail,
|
if item.get('type') == 'VOD':
|
||||||
'duration': duration,
|
subs = item.get('subtitles')
|
||||||
'formats': formats,
|
if subs:
|
||||||
'subtitles': subtitles,
|
subtitles = self.extract_subtitles(episode_id, subs)
|
||||||
}
|
|
||||||
|
entries.append({
|
||||||
|
'id': item_id,
|
||||||
|
'title': playlist_title if playlist_len == 1 else '%s (%s)' % (playlist_title, title),
|
||||||
|
'description': playlist_description if playlist_len == 1 else None,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
|
'duration': duration,
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
})
|
||||||
|
|
||||||
|
return self.playlist_result(entries, playlist_id, playlist_title, playlist_description)
|
||||||
|
|
||||||
def _get_subtitles(self, episode_id, subs):
|
def _get_subtitles(self, episode_id, subs):
|
||||||
original_subtitles = self._download_webpage(
|
original_subtitles = self._download_webpage(
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class ChilloutzoneIE(InfoExtractor):
|
|||||||
|
|
||||||
base64_video_info = self._html_search_regex(
|
base64_video_info = self._html_search_regex(
|
||||||
r'var cozVidData = "(.+?)";', webpage, 'video data')
|
r'var cozVidData = "(.+?)";', webpage, 'video data')
|
||||||
decoded_video_info = base64.b64decode(base64_video_info).decode("utf-8")
|
decoded_video_info = base64.b64decode(base64_video_info.encode('utf-8')).decode('utf-8')
|
||||||
video_info_dict = json.loads(decoded_video_info)
|
video_info_dict = json.loads(decoded_video_info)
|
||||||
|
|
||||||
# get video information from dict
|
# get video information from dict
|
||||||
|
|||||||
111
youtube_dl/extractor/cinemassacre.py
Normal file
111
youtube_dl/extractor/cinemassacre.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# encoding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import ExtractorError
|
||||||
|
from .bliptv import BlipTVIE
|
||||||
|
from .screenwavemedia import ScreenwaveMediaIE
|
||||||
|
|
||||||
|
|
||||||
|
class CinemassacreIE(InfoExtractor):
|
||||||
|
_VALID_URL = 'https?://(?:www\.)?cinemassacre\.com/(?P<date_y>[0-9]{4})/(?P<date_m>[0-9]{2})/(?P<date_d>[0-9]{2})/(?P<display_id>[^?#/]+)'
|
||||||
|
_TESTS = [
|
||||||
|
{
|
||||||
|
'url': 'http://cinemassacre.com/2012/11/10/avgn-the-movie-trailer/',
|
||||||
|
'md5': 'fde81fbafaee331785f58cd6c0d46190',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'Cinemassacre-19911',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'upload_date': '20121110',
|
||||||
|
'title': '“Angry Video Game Nerd: The Movie” – Trailer',
|
||||||
|
'description': 'md5:fb87405fcb42a331742a0dce2708560b',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'http://cinemassacre.com/2013/10/02/the-mummys-hand-1940',
|
||||||
|
'md5': 'd72f10cd39eac4215048f62ab477a511',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'Cinemassacre-521be8ef82b16',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'upload_date': '20131002',
|
||||||
|
'title': 'The Mummy’s Hand (1940)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
# blip.tv embedded video
|
||||||
|
'url': 'http://cinemassacre.com/2006/12/07/chronologically-confused-about-bad-movie-and-video-game-sequel-titles/',
|
||||||
|
'md5': 'ca9b3c8dd5a66f9375daeb5135f5a3de',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '4065369',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': 'AVGN: Chronologically Confused about Bad Movie and Video Game Sequel Titles',
|
||||||
|
'upload_date': '20061207',
|
||||||
|
'uploader': 'cinemassacre',
|
||||||
|
'uploader_id': '250778',
|
||||||
|
'timestamp': 1283233867,
|
||||||
|
'description': 'md5:0a108c78d130676b207d0f6d029ecffd',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
# Youtube embedded video
|
||||||
|
'url': 'http://cinemassacre.com/2006/09/01/mckids/',
|
||||||
|
'md5': '6eb30961fa795fedc750eac4881ad2e1',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'FnxsNhuikpo',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'upload_date': '20060901',
|
||||||
|
'uploader': 'Cinemassacre Extras',
|
||||||
|
'description': 'md5:de9b751efa9e45fbaafd9c8a1123ed53',
|
||||||
|
'uploader_id': 'Cinemassacre',
|
||||||
|
'title': 'AVGN: McKids',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'http://cinemassacre.com/2015/05/25/mario-kart-64-nintendo-64-james-mike-mondays/',
|
||||||
|
'md5': '1376908e49572389e7b06251a53cdd08',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'Cinemassacre-555779690c440',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'description': 'Let’s Play Mario Kart 64 !! Mario Kart 64 is a classic go-kart racing game released for the Nintendo 64 (N64). Today James & Mike do 4 player Battle Mode with Kyle and Bootsy!',
|
||||||
|
'title': 'Mario Kart 64 (Nintendo 64) James & Mike Mondays',
|
||||||
|
'upload_date': '20150525',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
mobj = re.match(self._VALID_URL, url)
|
||||||
|
display_id = mobj.group('display_id')
|
||||||
|
video_date = mobj.group('date_y') + mobj.group('date_m') + mobj.group('date_d')
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
|
||||||
|
playerdata_url = self._search_regex(
|
||||||
|
[
|
||||||
|
ScreenwaveMediaIE.EMBED_PATTERN,
|
||||||
|
r'<iframe[^>]+src="(?P<url>(?:https?:)?//(?:[^.]+\.)?youtube\.com/.+?)"',
|
||||||
|
],
|
||||||
|
webpage, 'player data URL', default=None, group='url')
|
||||||
|
if not playerdata_url:
|
||||||
|
playerdata_url = BlipTVIE._extract_url(webpage)
|
||||||
|
if not playerdata_url:
|
||||||
|
raise ExtractorError('Unable to find player data')
|
||||||
|
|
||||||
|
video_title = self._html_search_regex(
|
||||||
|
r'<title>(?P<title>.+?)\|', webpage, 'title')
|
||||||
|
video_description = self._html_search_regex(
|
||||||
|
r'<div class="entry-content">(?P<description>.+?)</div>',
|
||||||
|
webpage, 'description', flags=re.DOTALL, fatal=False)
|
||||||
|
video_thumbnail = self._og_search_thumbnail(webpage)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'_type': 'url_transparent',
|
||||||
|
'display_id': display_id,
|
||||||
|
'title': video_title,
|
||||||
|
'description': video_description,
|
||||||
|
'upload_date': video_date,
|
||||||
|
'thumbnail': video_thumbnail,
|
||||||
|
'url': playerdata_url,
|
||||||
|
}
|
||||||
@@ -1,53 +1,68 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import time
|
|
||||||
import xml.etree.ElementTree
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
determine_ext,
|
||||||
parse_duration,
|
int_or_none,
|
||||||
|
js_to_json,
|
||||||
|
parse_iso8601,
|
||||||
|
remove_end,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ClipfishIE(InfoExtractor):
|
class ClipfishIE(InfoExtractor):
|
||||||
IE_NAME = 'clipfish'
|
_VALID_URL = r'https?://(?:www\.)?clipfish\.de/(?:[^/]+/)+video/(?P<id>[0-9]+)'
|
||||||
|
|
||||||
_VALID_URL = r'^https?://(?:www\.)?clipfish\.de/.*?/video/(?P<id>[0-9]+)/'
|
|
||||||
_TEST = {
|
_TEST = {
|
||||||
'url': 'http://www.clipfish.de/special/game-trailer/video/3966754/fifa-14-e3-2013-trailer/',
|
'url': 'http://www.clipfish.de/special/game-trailer/video/3966754/fifa-14-e3-2013-trailer/',
|
||||||
'md5': '2521cd644e862936cf2e698206e47385',
|
'md5': '79bc922f3e8a9097b3d68a93780fd475',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '3966754',
|
'id': '3966754',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'FIFA 14 - E3 2013 Trailer',
|
'title': 'FIFA 14 - E3 2013 Trailer',
|
||||||
|
'timestamp': 1370938118,
|
||||||
|
'upload_date': '20130611',
|
||||||
'duration': 82,
|
'duration': 82,
|
||||||
},
|
}
|
||||||
'skip': 'Blocked in the US'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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(1)
|
|
||||||
|
|
||||||
info_url = ('http://www.clipfish.de/devxml/videoinfo/%s?ts=%d' %
|
webpage = self._download_webpage(url, video_id)
|
||||||
(video_id, int(time.time())))
|
|
||||||
doc = self._download_xml(
|
video_info = self._parse_json(
|
||||||
info_url, video_id, note='Downloading info page')
|
js_to_json(self._html_search_regex(
|
||||||
title = doc.find('title').text
|
'(?s)videoObject\s*=\s*({.+?});', webpage, 'video object')),
|
||||||
video_url = doc.find('filename').text
|
video_id)
|
||||||
if video_url is None:
|
|
||||||
xml_bytes = xml.etree.ElementTree.tostring(doc)
|
formats = []
|
||||||
raise ExtractorError('Cannot find video URL in document %r' %
|
for video_url in re.findall(r'var\s+videourl\s*=\s*"([^"]+)"', webpage):
|
||||||
xml_bytes)
|
ext = determine_ext(video_url)
|
||||||
thumbnail = doc.find('imageurl').text
|
if ext == 'm3u8':
|
||||||
duration = parse_duration(doc.find('duration').text)
|
formats.append({
|
||||||
|
'url': video_url.replace('de.hls.fra.clipfish.de', 'hls.fra.clipfish.de'),
|
||||||
|
'ext': 'mp4',
|
||||||
|
'format_id': 'hls',
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
formats.append({
|
||||||
|
'url': video_url,
|
||||||
|
'format_id': ext,
|
||||||
|
})
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
title = remove_end(self._og_search_title(webpage), ' - Video')
|
||||||
|
thumbnail = self._og_search_thumbnail(webpage)
|
||||||
|
duration = int_or_none(video_info.get('length'))
|
||||||
|
timestamp = parse_iso8601(self._html_search_meta('uploadDate', webpage, 'upload date'))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'url': video_url,
|
'formats': formats,
|
||||||
'thumbnail': thumbnail,
|
'thumbnail': thumbnail,
|
||||||
'duration': duration,
|
'duration': duration,
|
||||||
|
'timestamp': timestamp,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
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 (
|
||||||
find_xpath_attr,
|
find_xpath_attr,
|
||||||
@@ -10,9 +8,9 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class ClipsyndicateIE(InfoExtractor):
|
class ClipsyndicateIE(InfoExtractor):
|
||||||
_VALID_URL = r'http://www\.clipsyndicate\.com/video/play(list/\d+)?/(?P<id>\d+)'
|
_VALID_URL = r'http://(?:chic|www)\.clipsyndicate\.com/video/play(list/\d+)?/(?P<id>\d+)'
|
||||||
|
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'http://www.clipsyndicate.com/video/play/4629301/brick_briscoe',
|
'url': 'http://www.clipsyndicate.com/video/play/4629301/brick_briscoe',
|
||||||
'md5': '4d7d549451bad625e0ff3d7bd56d776c',
|
'md5': '4d7d549451bad625e0ff3d7bd56d776c',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -22,11 +20,13 @@ class ClipsyndicateIE(InfoExtractor):
|
|||||||
'duration': 612,
|
'duration': 612,
|
||||||
'thumbnail': 're:^https?://.+\.jpg',
|
'thumbnail': 're:^https?://.+\.jpg',
|
||||||
},
|
},
|
||||||
}
|
}, {
|
||||||
|
'url': 'http://chic.clipsyndicate.com/video/play/5844117/shark_attack',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
video_id = self._match_id(url)
|
||||||
video_id = mobj.group('id')
|
|
||||||
js_player = self._download_webpage(
|
js_player = self._download_webpage(
|
||||||
'http://eplayer.clipsyndicate.com/embed/player.js?va_id=%s' % video_id,
|
'http://eplayer.clipsyndicate.com/embed/player.js?va_id=%s' % video_id,
|
||||||
video_id, 'Downlaoding player')
|
video_id, 'Downlaoding player')
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from ..utils import (
|
|||||||
|
|
||||||
class CNETIE(InfoExtractor):
|
class CNETIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?cnet\.com/videos/(?P<id>[^/]+)/'
|
_VALID_URL = r'https?://(?:www\.)?cnet\.com/videos/(?P<id>[^/]+)/'
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'http://www.cnet.com/videos/hands-on-with-microsofts-windows-8-1-update/',
|
'url': 'http://www.cnet.com/videos/hands-on-with-microsofts-windows-8-1-update/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '56f4ea68-bd21-4852-b08c-4de5b8354c60',
|
'id': '56f4ea68-bd21-4852-b08c-4de5b8354c60',
|
||||||
@@ -25,7 +25,20 @@ class CNETIE(InfoExtractor):
|
|||||||
'params': {
|
'params': {
|
||||||
'skip_download': 'requires rtmpdump',
|
'skip_download': 'requires rtmpdump',
|
||||||
}
|
}
|
||||||
}
|
}, {
|
||||||
|
'url': 'http://www.cnet.com/videos/whiny-pothole-tweets-at-local-government-when-hit-by-cars-tomorrow-daily-187/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '56527b93-d25d-44e3-b738-f989ce2e49ba',
|
||||||
|
'ext': 'flv',
|
||||||
|
'description': 'Khail and Ashley wonder what other civic woes can be solved by self-tweeting objects, investigate a new kind of VR camera and watch an origami robot self-assemble, walk, climb, dig and dissolve. #TDPothole',
|
||||||
|
'uploader_id': 'b163284d-6b73-44fc-b3e6-3da66c392d40',
|
||||||
|
'uploader': 'Ashley Esqueda',
|
||||||
|
'title': 'Whiny potholes tweet at local government when hit by cars (Tomorrow Daily 187)',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True, # requires rtmpdump
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id = self._match_id(url)
|
display_id = self._match_id(url)
|
||||||
@@ -42,7 +55,7 @@ class CNETIE(InfoExtractor):
|
|||||||
raise ExtractorError('Cannot find video data')
|
raise ExtractorError('Cannot find video data')
|
||||||
|
|
||||||
mpx_account = data['config']['players']['default']['mpx_account']
|
mpx_account = data['config']['players']['default']['mpx_account']
|
||||||
vid = vdata['files']['rtmp']
|
vid = vdata['files'].get('rtmp', vdata['files']['hds'])
|
||||||
tp_link = 'http://link.theplatform.com/s/%s/%s' % (mpx_account, vid)
|
tp_link = 'http://link.theplatform.com/s/%s/%s' % (mpx_account, vid)
|
||||||
|
|
||||||
video_id = vdata['id']
|
video_id = vdata['id']
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from ..utils import (
|
|||||||
|
|
||||||
class CNNIE(InfoExtractor):
|
class CNNIE(InfoExtractor):
|
||||||
_VALID_URL = r'''(?x)https?://(?:(?:edition|www)\.)?cnn\.com/video/(?:data/.+?|\?)/
|
_VALID_URL = r'''(?x)https?://(?:(?:edition|www)\.)?cnn\.com/video/(?:data/.+?|\?)/
|
||||||
(?P<path>.+?/(?P<title>[^/]+?)(?:\.(?:cnn|hln)(?:-ap)?|(?=&)))'''
|
(?P<path>.+?/(?P<title>[^/]+?)(?:\.(?:[a-z\-]+)|(?=&)))'''
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://edition.cnn.com/video/?/video/sports/2013/06/09/nadal-1-on-1.cnn',
|
'url': 'http://edition.cnn.com/video/?/video/sports/2013/06/09/nadal-1-on-1.cnn',
|
||||||
@@ -45,6 +45,12 @@ class CNNIE(InfoExtractor):
|
|||||||
'description': 'md5:e7223a503315c9f150acac52e76de086',
|
'description': 'md5:e7223a503315c9f150acac52e76de086',
|
||||||
'upload_date': '20141222',
|
'upload_date': '20141222',
|
||||||
}
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'http://cnn.com/video/?/video/politics/2015/03/27/pkg-arizona-senator-church-attendance-mandatory.ktvk',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://cnn.com/video/?/video/us/2015/04/06/dnt-baker-refuses-anti-gay-order.wkmg',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class ComCarCoffIE(InfoExtractor):
|
|||||||
webpage, 'full data json'))
|
webpage, 'full data json'))
|
||||||
|
|
||||||
video_id = full_data['activeVideo']['video']
|
video_id = full_data['activeVideo']['video']
|
||||||
video_data = full_data['videos'][video_id]
|
video_data = full_data.get('videos', {}).get(video_id) or full_data['singleshots'][video_id]
|
||||||
thumbnails = [{
|
thumbnails = [{
|
||||||
'url': video_data['images']['thumb'],
|
'url': video_data['images']['thumb'],
|
||||||
}, {
|
}, {
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ class ComedyCentralShowsIE(MTVServicesInfoExtractor):
|
|||||||
|
|
||||||
uri = mMovieParams[0][1]
|
uri = mMovieParams[0][1]
|
||||||
# Correct cc.com in uri
|
# Correct cc.com in uri
|
||||||
uri = re.sub(r'(episode:[^.]+)(\.cc)?\.com', r'\1.cc.com', uri)
|
uri = re.sub(r'(episode:[^.]+)(\.cc)?\.com', r'\1.com', uri)
|
||||||
|
|
||||||
index_url = 'http://%s.cc.com/feeds/mrss?%s' % (show_name, compat_urllib_parse.urlencode({'uri': uri}))
|
index_url = 'http://%s.cc.com/feeds/mrss?%s' % (show_name, compat_urllib_parse.urlencode({'uri': uri}))
|
||||||
idoc = self._download_xml(
|
idoc = self._download_xml(
|
||||||
|
|||||||
@@ -14,25 +14,35 @@ import xml.etree.ElementTree
|
|||||||
|
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
compat_cookiejar,
|
compat_cookiejar,
|
||||||
|
compat_cookies,
|
||||||
|
compat_getpass,
|
||||||
compat_HTTPError,
|
compat_HTTPError,
|
||||||
compat_http_client,
|
compat_http_client,
|
||||||
compat_urllib_error,
|
compat_urllib_error,
|
||||||
|
compat_urllib_parse,
|
||||||
compat_urllib_parse_urlparse,
|
compat_urllib_parse_urlparse,
|
||||||
|
compat_urllib_request,
|
||||||
compat_urlparse,
|
compat_urlparse,
|
||||||
compat_str,
|
compat_str,
|
||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
NO_DEFAULT,
|
||||||
age_restricted,
|
age_restricted,
|
||||||
|
bug_reports_message,
|
||||||
clean_html,
|
clean_html,
|
||||||
compiled_regex_type,
|
compiled_regex_type,
|
||||||
|
determine_ext,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
fix_xml_ampersands,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
RegexNotFoundError,
|
RegexNotFoundError,
|
||||||
sanitize_filename,
|
sanitize_filename,
|
||||||
unescapeHTML,
|
unescapeHTML,
|
||||||
|
url_basename,
|
||||||
|
xpath_text,
|
||||||
|
xpath_with_ns,
|
||||||
)
|
)
|
||||||
_NO_DEFAULT = object()
|
|
||||||
|
|
||||||
|
|
||||||
class InfoExtractor(object):
|
class InfoExtractor(object):
|
||||||
@@ -46,7 +56,7 @@ class InfoExtractor(object):
|
|||||||
information possibly downloading the video to the file system, among
|
information possibly downloading the video to the file system, among
|
||||||
other possible outcomes.
|
other possible outcomes.
|
||||||
|
|
||||||
The type field determines the the type of the result.
|
The type field determines the type of the result.
|
||||||
By far the most common value (and the default if _type is missing) is
|
By far the most common value (and the default if _type is missing) is
|
||||||
"video", which indicates a single video.
|
"video", which indicates a single video.
|
||||||
|
|
||||||
@@ -62,7 +72,7 @@ class InfoExtractor(object):
|
|||||||
|
|
||||||
Potential fields:
|
Potential fields:
|
||||||
* url Mandatory. The URL of the video file
|
* url Mandatory. The URL of the video file
|
||||||
* ext Will be calculated from url if missing
|
* ext Will be calculated from URL if missing
|
||||||
* format A human-readable description of the format
|
* format A human-readable description of the format
|
||||||
("mp4 container with h264/opus").
|
("mp4 container with h264/opus").
|
||||||
Calculated from the format_id, width, height.
|
Calculated from the format_id, width, height.
|
||||||
@@ -110,11 +120,8 @@ class InfoExtractor(object):
|
|||||||
(quality takes higher priority)
|
(quality takes higher priority)
|
||||||
-1 for default (order by other properties),
|
-1 for default (order by other properties),
|
||||||
-2 or smaller for less than default.
|
-2 or smaller for less than default.
|
||||||
* http_method HTTP method to use for the download.
|
|
||||||
* http_headers A dictionary of additional HTTP headers
|
* http_headers A dictionary of additional HTTP headers
|
||||||
to add to the request.
|
to add to the request.
|
||||||
* http_post_data Additional data to send with a POST
|
|
||||||
request.
|
|
||||||
* stretched_ratio If given and not 1, indicates that the
|
* stretched_ratio If given and not 1, indicates that the
|
||||||
video's pixels are not square.
|
video's pixels are not square.
|
||||||
width : height ratio as float.
|
width : height ratio as float.
|
||||||
@@ -155,7 +162,7 @@ class InfoExtractor(object):
|
|||||||
lower to higher preference, each element is a dictionary
|
lower to higher preference, each element is a dictionary
|
||||||
with the "ext" entry and one of:
|
with the "ext" entry and one of:
|
||||||
* "data": The subtitles file contents
|
* "data": The subtitles file contents
|
||||||
* "url": A url pointing to the subtitles file
|
* "url": A URL pointing to the subtitles file
|
||||||
automatic_captions: Like 'subtitles', used by the YoutubeIE for
|
automatic_captions: Like 'subtitles', used by the YoutubeIE for
|
||||||
automatically generated captions
|
automatically generated captions
|
||||||
duration: Length of the video in seconds, as an integer.
|
duration: Length of the video in seconds, as an integer.
|
||||||
@@ -176,13 +183,18 @@ class InfoExtractor(object):
|
|||||||
Set to "root" to indicate that this is a
|
Set to "root" to indicate that this is a
|
||||||
comment to the original video.
|
comment to the original video.
|
||||||
age_limit: Age restriction for the video, as an integer (years)
|
age_limit: Age restriction for the video, as an integer (years)
|
||||||
webpage_url: The url to the video webpage, if given to youtube-dl it
|
webpage_url: The URL to the video webpage, if given to youtube-dl it
|
||||||
should allow to get the same result again. (It will be set
|
should allow to get the same result again. (It will be set
|
||||||
by YoutubeDL if it's missing)
|
by YoutubeDL if it's missing)
|
||||||
categories: A list of categories that the video falls in, for example
|
categories: A list of categories that the video falls in, for example
|
||||||
["Sports", "Berlin"]
|
["Sports", "Berlin"]
|
||||||
|
tags: A list of tags assigned to the video, e.g. ["sweden", "pop music"]
|
||||||
is_live: True, False, or None (=unknown). Whether this video is a
|
is_live: True, False, or None (=unknown). Whether this video is a
|
||||||
live stream that goes on instead of a fixed-length video.
|
live stream that goes on instead of a fixed-length video.
|
||||||
|
start_time: Time in seconds where the reproduction should start, as
|
||||||
|
specified in the URL.
|
||||||
|
end_time: Time in seconds where the reproduction should end, as
|
||||||
|
specified in the URL.
|
||||||
|
|
||||||
Unless mentioned otherwise, the fields should be Unicode strings.
|
Unless mentioned otherwise, the fields should be Unicode strings.
|
||||||
|
|
||||||
@@ -193,8 +205,8 @@ class InfoExtractor(object):
|
|||||||
There must be a key "entries", which is a list, an iterable, or a PagedList
|
There must be a key "entries", which is a list, an iterable, or a PagedList
|
||||||
object, each element of which is a valid dictionary by this specification.
|
object, each element of which is a valid dictionary by this specification.
|
||||||
|
|
||||||
Additionally, playlists can have "title" and "id" attributes with the same
|
Additionally, playlists can have "title", "description" and "id" attributes
|
||||||
semantics as videos (see above).
|
with the same semantics as videos (see above).
|
||||||
|
|
||||||
|
|
||||||
_type "multi_video" indicates that there are multiple videos that
|
_type "multi_video" indicates that there are multiple videos that
|
||||||
@@ -324,7 +336,7 @@ class InfoExtractor(object):
|
|||||||
self._downloader.report_warning(errmsg)
|
self._downloader.report_warning(errmsg)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _download_webpage_handle(self, url_or_request, video_id, note=None, errnote=None, fatal=True):
|
def _download_webpage_handle(self, url_or_request, video_id, note=None, errnote=None, fatal=True, encoding=None):
|
||||||
""" Returns a tuple (page content as string, URL handle) """
|
""" Returns a tuple (page content as string, URL handle) """
|
||||||
# Strip hashes from the URL (#1038)
|
# Strip hashes from the URL (#1038)
|
||||||
if isinstance(url_or_request, (compat_str, str)):
|
if isinstance(url_or_request, (compat_str, str)):
|
||||||
@@ -334,14 +346,11 @@ class InfoExtractor(object):
|
|||||||
if urlh is False:
|
if urlh is False:
|
||||||
assert not fatal
|
assert not fatal
|
||||||
return False
|
return False
|
||||||
content = self._webpage_read_content(urlh, url_or_request, video_id, note, errnote, fatal)
|
content = self._webpage_read_content(urlh, url_or_request, video_id, note, errnote, fatal, encoding=encoding)
|
||||||
return (content, urlh)
|
return (content, urlh)
|
||||||
|
|
||||||
def _webpage_read_content(self, urlh, url_or_request, video_id, note=None, errnote=None, fatal=True, prefix=None):
|
@staticmethod
|
||||||
content_type = urlh.headers.get('Content-Type', '')
|
def _guess_encoding_from_content(content_type, webpage_bytes):
|
||||||
webpage_bytes = urlh.read()
|
|
||||||
if prefix is not None:
|
|
||||||
webpage_bytes = prefix + webpage_bytes
|
|
||||||
m = re.match(r'[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+\s*;\s*charset=(.+)', content_type)
|
m = re.match(r'[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+\s*;\s*charset=(.+)', content_type)
|
||||||
if m:
|
if m:
|
||||||
encoding = m.group(1)
|
encoding = m.group(1)
|
||||||
@@ -354,6 +363,16 @@ class InfoExtractor(object):
|
|||||||
encoding = 'utf-16'
|
encoding = 'utf-16'
|
||||||
else:
|
else:
|
||||||
encoding = 'utf-8'
|
encoding = 'utf-8'
|
||||||
|
|
||||||
|
return encoding
|
||||||
|
|
||||||
|
def _webpage_read_content(self, urlh, url_or_request, video_id, note=None, errnote=None, fatal=True, prefix=None, encoding=None):
|
||||||
|
content_type = urlh.headers.get('Content-Type', '')
|
||||||
|
webpage_bytes = urlh.read()
|
||||||
|
if prefix is not None:
|
||||||
|
webpage_bytes = prefix + webpage_bytes
|
||||||
|
if not encoding:
|
||||||
|
encoding = self._guess_encoding_from_content(content_type, webpage_bytes)
|
||||||
if self._downloader.params.get('dump_intermediate_pages', False):
|
if self._downloader.params.get('dump_intermediate_pages', False):
|
||||||
try:
|
try:
|
||||||
url = url_or_request.get_full_url()
|
url = url_or_request.get_full_url()
|
||||||
@@ -410,13 +429,13 @@ class InfoExtractor(object):
|
|||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
def _download_webpage(self, url_or_request, video_id, note=None, errnote=None, fatal=True, tries=1, timeout=5):
|
def _download_webpage(self, url_or_request, video_id, note=None, errnote=None, fatal=True, tries=1, timeout=5, encoding=None):
|
||||||
""" Returns the data of the page as a string """
|
""" Returns the data of the page as a string """
|
||||||
success = False
|
success = False
|
||||||
try_count = 0
|
try_count = 0
|
||||||
while success is False:
|
while success is False:
|
||||||
try:
|
try:
|
||||||
res = self._download_webpage_handle(url_or_request, video_id, note, errnote, fatal)
|
res = self._download_webpage_handle(url_or_request, video_id, note, errnote, fatal, encoding=encoding)
|
||||||
success = True
|
success = True
|
||||||
except compat_http_client.IncompleteRead as e:
|
except compat_http_client.IncompleteRead as e:
|
||||||
try_count += 1
|
try_count += 1
|
||||||
@@ -431,10 +450,10 @@ class InfoExtractor(object):
|
|||||||
|
|
||||||
def _download_xml(self, url_or_request, video_id,
|
def _download_xml(self, url_or_request, video_id,
|
||||||
note='Downloading XML', errnote='Unable to download XML',
|
note='Downloading XML', errnote='Unable to download XML',
|
||||||
transform_source=None, fatal=True):
|
transform_source=None, fatal=True, encoding=None):
|
||||||
"""Return the xml as an xml.etree.ElementTree.Element"""
|
"""Return the xml as an xml.etree.ElementTree.Element"""
|
||||||
xml_string = self._download_webpage(
|
xml_string = self._download_webpage(
|
||||||
url_or_request, video_id, note, errnote, fatal=fatal)
|
url_or_request, video_id, note, errnote, fatal=fatal, encoding=encoding)
|
||||||
if xml_string is False:
|
if xml_string is False:
|
||||||
return xml_string
|
return xml_string
|
||||||
if transform_source:
|
if transform_source:
|
||||||
@@ -445,9 +464,10 @@ class InfoExtractor(object):
|
|||||||
note='Downloading JSON metadata',
|
note='Downloading JSON metadata',
|
||||||
errnote='Unable to download JSON metadata',
|
errnote='Unable to download JSON metadata',
|
||||||
transform_source=None,
|
transform_source=None,
|
||||||
fatal=True):
|
fatal=True, encoding=None):
|
||||||
json_string = self._download_webpage(
|
json_string = self._download_webpage(
|
||||||
url_or_request, video_id, note, errnote, fatal=fatal)
|
url_or_request, video_id, note, errnote, fatal=fatal,
|
||||||
|
encoding=encoding)
|
||||||
if (not fatal) and json_string is False:
|
if (not fatal) and json_string is False:
|
||||||
return None
|
return None
|
||||||
return self._parse_json(
|
return self._parse_json(
|
||||||
@@ -490,16 +510,24 @@ class InfoExtractor(object):
|
|||||||
"""Report attempt to log in."""
|
"""Report attempt to log in."""
|
||||||
self.to_screen('Logging in')
|
self.to_screen('Logging in')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def raise_login_required(msg='This video is only available for registered users'):
|
||||||
|
raise ExtractorError(
|
||||||
|
'%s. Use --username and --password or --netrc to provide account credentials.' % msg,
|
||||||
|
expected=True)
|
||||||
|
|
||||||
# Methods for following #608
|
# Methods for following #608
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def url_result(url, ie=None, video_id=None):
|
def url_result(url, ie=None, video_id=None, video_title=None):
|
||||||
"""Returns a url that points to a page that should be processed"""
|
"""Returns a URL that points to a page that should be processed"""
|
||||||
# TODO: ie should be the class used for getting the info
|
# TODO: ie should be the class used for getting the info
|
||||||
video_info = {'_type': 'url',
|
video_info = {'_type': 'url',
|
||||||
'url': url,
|
'url': url,
|
||||||
'ie_key': ie}
|
'ie_key': ie}
|
||||||
if video_id is not None:
|
if video_id is not None:
|
||||||
video_info['id'] = video_id
|
video_info['id'] = video_id
|
||||||
|
if video_title is not None:
|
||||||
|
video_info['title'] = video_title
|
||||||
return video_info
|
return video_info
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -515,7 +543,7 @@ class InfoExtractor(object):
|
|||||||
video_info['description'] = playlist_description
|
video_info['description'] = playlist_description
|
||||||
return video_info
|
return video_info
|
||||||
|
|
||||||
def _search_regex(self, pattern, string, name, default=_NO_DEFAULT, fatal=True, flags=0, group=None):
|
def _search_regex(self, pattern, string, name, default=NO_DEFAULT, fatal=True, flags=0, group=None):
|
||||||
"""
|
"""
|
||||||
Perform a regex search on the given string, using a single or a list of
|
Perform a regex search on the given string, using a single or a list of
|
||||||
patterns returning the first matching group.
|
patterns returning the first matching group.
|
||||||
@@ -541,16 +569,15 @@ class InfoExtractor(object):
|
|||||||
return next(g for g in mobj.groups() if g is not None)
|
return next(g for g in mobj.groups() if g is not None)
|
||||||
else:
|
else:
|
||||||
return mobj.group(group)
|
return mobj.group(group)
|
||||||
elif default is not _NO_DEFAULT:
|
elif default is not NO_DEFAULT:
|
||||||
return default
|
return default
|
||||||
elif fatal:
|
elif fatal:
|
||||||
raise RegexNotFoundError('Unable to extract %s' % _name)
|
raise RegexNotFoundError('Unable to extract %s' % _name)
|
||||||
else:
|
else:
|
||||||
self._downloader.report_warning('unable to extract %s; '
|
self._downloader.report_warning('unable to extract %s' % _name + bug_reports_message())
|
||||||
'please report this issue on http://yt-dl.org/bug' % _name)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _html_search_regex(self, pattern, string, name, default=_NO_DEFAULT, fatal=True, flags=0, group=None):
|
def _html_search_regex(self, pattern, string, name, default=NO_DEFAULT, fatal=True, flags=0, group=None):
|
||||||
"""
|
"""
|
||||||
Like _search_regex, but strips HTML tags and unescapes entities.
|
Like _search_regex, but strips HTML tags and unescapes entities.
|
||||||
"""
|
"""
|
||||||
@@ -562,7 +589,7 @@ class InfoExtractor(object):
|
|||||||
|
|
||||||
def _get_login_info(self):
|
def _get_login_info(self):
|
||||||
"""
|
"""
|
||||||
Get the the login info as (username, password)
|
Get the login info as (username, password)
|
||||||
It will look in the netrc file using the _NETRC_MACHINE value
|
It will look in the netrc file using the _NETRC_MACHINE value
|
||||||
If there's no info available, return (None, None)
|
If there's no info available, return (None, None)
|
||||||
"""
|
"""
|
||||||
@@ -590,7 +617,7 @@ class InfoExtractor(object):
|
|||||||
|
|
||||||
return (username, password)
|
return (username, password)
|
||||||
|
|
||||||
def _get_tfa_info(self):
|
def _get_tfa_info(self, note='two-factor verification code'):
|
||||||
"""
|
"""
|
||||||
Get the two-factor authentication info
|
Get the two-factor authentication info
|
||||||
TODO - asking the user will be required for sms/phone verify
|
TODO - asking the user will be required for sms/phone verify
|
||||||
@@ -604,7 +631,7 @@ class InfoExtractor(object):
|
|||||||
if downloader_params.get('twofactor', None) is not None:
|
if downloader_params.get('twofactor', None) is not None:
|
||||||
return downloader_params['twofactor']
|
return downloader_params['twofactor']
|
||||||
|
|
||||||
return None
|
return compat_getpass('Type %s and press [Return]: ' % note)
|
||||||
|
|
||||||
# Helper functions for extracting OpenGraph info
|
# Helper functions for extracting OpenGraph info
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -617,6 +644,12 @@ class InfoExtractor(object):
|
|||||||
template % (content_re, property_re),
|
template % (content_re, property_re),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _meta_regex(prop):
|
||||||
|
return r'''(?isx)<meta
|
||||||
|
(?=[^>]+(?:itemprop|name|property|id|http-equiv)=(["\']?)%s\1)
|
||||||
|
[^>]+?content=(["\'])(?P<content>.*?)\2''' % re.escape(prop)
|
||||||
|
|
||||||
def _og_search_property(self, prop, html, name=None, **kargs):
|
def _og_search_property(self, prop, html, name=None, **kargs):
|
||||||
if name is None:
|
if name is None:
|
||||||
name = 'OpenGraph %s' % prop
|
name = 'OpenGraph %s' % prop
|
||||||
@@ -626,7 +659,7 @@ class InfoExtractor(object):
|
|||||||
return unescapeHTML(escaped)
|
return unescapeHTML(escaped)
|
||||||
|
|
||||||
def _og_search_thumbnail(self, html, **kargs):
|
def _og_search_thumbnail(self, html, **kargs):
|
||||||
return self._og_search_property('image', html, 'thumbnail url', fatal=False, **kargs)
|
return self._og_search_property('image', html, 'thumbnail URL', fatal=False, **kargs)
|
||||||
|
|
||||||
def _og_search_description(self, html, **kargs):
|
def _og_search_description(self, html, **kargs):
|
||||||
return self._og_search_property('description', html, fatal=False, **kargs)
|
return self._og_search_property('description', html, fatal=False, **kargs)
|
||||||
@@ -647,9 +680,7 @@ class InfoExtractor(object):
|
|||||||
if display_name is None:
|
if display_name is None:
|
||||||
display_name = name
|
display_name = name
|
||||||
return self._html_search_regex(
|
return self._html_search_regex(
|
||||||
r'''(?isx)<meta
|
self._meta_regex(name),
|
||||||
(?=[^>]+(?:itemprop|name|property)=(["\']?)%s\1)
|
|
||||||
[^>]+?content=(["\'])(?P<content>.*?)\2''' % re.escape(name),
|
|
||||||
html, display_name, fatal=fatal, group='content', **kwargs)
|
html, display_name, fatal=fatal, group='content', **kwargs)
|
||||||
|
|
||||||
def _dc_search_uploader(self, html):
|
def _dc_search_uploader(self, html):
|
||||||
@@ -698,7 +729,28 @@ class InfoExtractor(object):
|
|||||||
return self._html_search_meta('twitter:player', html,
|
return self._html_search_meta('twitter:player', html,
|
||||||
'twitter card player')
|
'twitter card player')
|
||||||
|
|
||||||
def _sort_formats(self, formats):
|
@staticmethod
|
||||||
|
def _hidden_inputs(html):
|
||||||
|
hidden_inputs = {}
|
||||||
|
for input in re.findall(r'<input([^>]+)>', html):
|
||||||
|
if not re.search(r'type=(["\'])(?:hidden|submit)\1', input):
|
||||||
|
continue
|
||||||
|
name = re.search(r'name=(["\'])(?P<value>.+?)\1', input)
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
value = re.search(r'value=(["\'])(?P<value>.*?)\1', input)
|
||||||
|
if not value:
|
||||||
|
continue
|
||||||
|
hidden_inputs[name.group('value')] = value.group('value')
|
||||||
|
return hidden_inputs
|
||||||
|
|
||||||
|
def _form_hidden_inputs(self, form_id, html):
|
||||||
|
form = self._search_regex(
|
||||||
|
r'(?s)<form[^>]+?id=(["\'])%s\1[^>]*>(?P<form>.+?)</form>' % form_id,
|
||||||
|
html, '%s form' % form_id, group='form')
|
||||||
|
return self._hidden_inputs(form)
|
||||||
|
|
||||||
|
def _sort_formats(self, formats, field_preference=None):
|
||||||
if not formats:
|
if not formats:
|
||||||
raise ExtractorError('No video formats found')
|
raise ExtractorError('No video formats found')
|
||||||
|
|
||||||
@@ -708,6 +760,9 @@ class InfoExtractor(object):
|
|||||||
if not f.get('ext') and 'url' in f:
|
if not f.get('ext') and 'url' in f:
|
||||||
f['ext'] = determine_ext(f['url'])
|
f['ext'] = determine_ext(f['url'])
|
||||||
|
|
||||||
|
if isinstance(field_preference, (list, tuple)):
|
||||||
|
return tuple(f.get(field) if f.get(field) is not None else -1 for field in field_preference)
|
||||||
|
|
||||||
preference = f.get('preference')
|
preference = f.get('preference')
|
||||||
if preference is None:
|
if preference is None:
|
||||||
proto = f.get('protocol')
|
proto = f.get('protocol')
|
||||||
@@ -754,7 +809,7 @@ class InfoExtractor(object):
|
|||||||
f.get('fps') if f.get('fps') is not None else -1,
|
f.get('fps') if f.get('fps') is not None else -1,
|
||||||
f.get('filesize_approx') if f.get('filesize_approx') is not None else -1,
|
f.get('filesize_approx') if f.get('filesize_approx') is not None else -1,
|
||||||
f.get('source_preference') if f.get('source_preference') is not None else -1,
|
f.get('source_preference') if f.get('source_preference') is not None else -1,
|
||||||
f.get('format_id'),
|
f.get('format_id') if f.get('format_id') is not None else '',
|
||||||
)
|
)
|
||||||
formats.sort(key=_formats_key)
|
formats.sort(key=_formats_key)
|
||||||
|
|
||||||
@@ -776,8 +831,8 @@ class InfoExtractor(object):
|
|||||||
return True
|
return True
|
||||||
except ExtractorError as e:
|
except ExtractorError as e:
|
||||||
if isinstance(e.cause, compat_HTTPError):
|
if isinstance(e.cause, compat_HTTPError):
|
||||||
self.report_warning(
|
self.to_screen(
|
||||||
'%s URL is invalid, skipping' % item, video_id)
|
'%s: %s URL is invalid, skipping' % (video_id, item))
|
||||||
return False
|
return False
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@@ -805,10 +860,14 @@ class InfoExtractor(object):
|
|||||||
self.to_screen(msg)
|
self.to_screen(msg)
|
||||||
time.sleep(timeout)
|
time.sleep(timeout)
|
||||||
|
|
||||||
def _extract_f4m_formats(self, manifest_url, video_id, preference=None, f4m_id=None):
|
def _extract_f4m_formats(self, manifest_url, video_id, preference=None, f4m_id=None,
|
||||||
|
transform_source=lambda s: fix_xml_ampersands(s).strip()):
|
||||||
manifest = self._download_xml(
|
manifest = self._download_xml(
|
||||||
manifest_url, video_id, 'Downloading f4m manifest',
|
manifest_url, video_id, 'Downloading f4m manifest',
|
||||||
'Unable to download f4m manifest')
|
'Unable to download f4m manifest',
|
||||||
|
# Some manifests may be malformed, e.g. prosiebensat1 generated manifests
|
||||||
|
# (see https://github.com/rg3/youtube-dl/issues/6215#issuecomment-121704244)
|
||||||
|
transform_source=transform_source)
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
manifest_version = '1.0'
|
manifest_version = '1.0'
|
||||||
@@ -818,11 +877,22 @@ class InfoExtractor(object):
|
|||||||
media_nodes = manifest.findall('{http://ns.adobe.com/f4m/2.0}media')
|
media_nodes = manifest.findall('{http://ns.adobe.com/f4m/2.0}media')
|
||||||
for i, media_el in enumerate(media_nodes):
|
for i, media_el in enumerate(media_nodes):
|
||||||
if manifest_version == '2.0':
|
if manifest_version == '2.0':
|
||||||
manifest_url = ('/'.join(manifest_url.split('/')[:-1]) + '/' +
|
media_url = media_el.attrib.get('href') or media_el.attrib.get('url')
|
||||||
(media_el.attrib.get('href') or media_el.attrib.get('url')))
|
if not media_url:
|
||||||
|
continue
|
||||||
|
manifest_url = (
|
||||||
|
media_url if media_url.startswith('http://') or media_url.startswith('https://')
|
||||||
|
else ('/'.join(manifest_url.split('/')[:-1]) + '/' + media_url))
|
||||||
|
# If media_url is itself a f4m manifest do the recursive extraction
|
||||||
|
# since bitrates in parent manifest (this one) and media_url manifest
|
||||||
|
# may differ leading to inability to resolve the format by requested
|
||||||
|
# bitrate in f4m downloader
|
||||||
|
if determine_ext(manifest_url) == 'f4m':
|
||||||
|
formats.extend(self._extract_f4m_formats(manifest_url, video_id, preference, f4m_id))
|
||||||
|
continue
|
||||||
tbr = int_or_none(media_el.attrib.get('bitrate'))
|
tbr = int_or_none(media_el.attrib.get('bitrate'))
|
||||||
formats.append({
|
formats.append({
|
||||||
'format_id': '-'.join(filter(None, [f4m_id, 'f4m-%d' % (i if tbr is None else tbr)])),
|
'format_id': '-'.join(filter(None, [f4m_id, compat_str(i if tbr is None else tbr)])),
|
||||||
'url': manifest_url,
|
'url': manifest_url,
|
||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
'tbr': tbr,
|
'tbr': tbr,
|
||||||
@@ -836,7 +906,8 @@ class InfoExtractor(object):
|
|||||||
|
|
||||||
def _extract_m3u8_formats(self, m3u8_url, video_id, ext=None,
|
def _extract_m3u8_formats(self, m3u8_url, video_id, ext=None,
|
||||||
entry_protocol='m3u8', preference=None,
|
entry_protocol='m3u8', preference=None,
|
||||||
m3u8_id=None):
|
m3u8_id=None, note=None, errnote=None,
|
||||||
|
fatal=True):
|
||||||
|
|
||||||
formats = [{
|
formats = [{
|
||||||
'format_id': '-'.join(filter(None, [m3u8_id, 'meta'])),
|
'format_id': '-'.join(filter(None, [m3u8_id, 'meta'])),
|
||||||
@@ -855,8 +926,11 @@ class InfoExtractor(object):
|
|||||||
|
|
||||||
m3u8_doc = self._download_webpage(
|
m3u8_doc = self._download_webpage(
|
||||||
m3u8_url, video_id,
|
m3u8_url, video_id,
|
||||||
note='Downloading m3u8 information',
|
note=note or 'Downloading m3u8 information',
|
||||||
errnote='Failed to download m3u8 information')
|
errnote=errnote or 'Failed to download m3u8 information',
|
||||||
|
fatal=fatal)
|
||||||
|
if m3u8_doc is False:
|
||||||
|
return m3u8_doc
|
||||||
last_info = None
|
last_info = None
|
||||||
last_media = None
|
last_media = None
|
||||||
kv_rex = re.compile(
|
kv_rex = re.compile(
|
||||||
@@ -886,7 +960,7 @@ class InfoExtractor(object):
|
|||||||
format_id = []
|
format_id = []
|
||||||
if m3u8_id:
|
if m3u8_id:
|
||||||
format_id.append(m3u8_id)
|
format_id.append(m3u8_id)
|
||||||
last_media_name = last_media.get('NAME') if last_media else None
|
last_media_name = last_media.get('NAME') if last_media and last_media.get('TYPE') != 'SUBTITLES' else None
|
||||||
format_id.append(last_media_name if last_media_name else '%d' % (tbr if tbr else len(formats)))
|
format_id.append(last_media_name if last_media_name else '%d' % (tbr if tbr else len(formats)))
|
||||||
f = {
|
f = {
|
||||||
'format_id': '-'.join(format_id),
|
'format_id': '-'.join(format_id),
|
||||||
@@ -917,69 +991,221 @@ class InfoExtractor(object):
|
|||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
return formats
|
return formats
|
||||||
|
|
||||||
# TODO: improve extraction
|
@staticmethod
|
||||||
def _extract_smil_formats(self, smil_url, video_id, fatal=True):
|
def _xpath_ns(path, namespace=None):
|
||||||
smil = self._download_xml(
|
if not namespace:
|
||||||
smil_url, video_id, 'Downloading SMIL file',
|
return path
|
||||||
'Unable to download SMIL file', fatal=fatal)
|
out = []
|
||||||
|
for c in path.split('/'):
|
||||||
|
if not c or c == '.':
|
||||||
|
out.append(c)
|
||||||
|
else:
|
||||||
|
out.append('{%s}%s' % (namespace, c))
|
||||||
|
return '/'.join(out)
|
||||||
|
|
||||||
|
def _extract_smil_formats(self, smil_url, video_id, fatal=True, f4m_params=None):
|
||||||
|
smil = self._download_smil(smil_url, video_id, fatal=fatal)
|
||||||
|
|
||||||
if smil is False:
|
if smil is False:
|
||||||
assert not fatal
|
assert not fatal
|
||||||
return []
|
return []
|
||||||
|
|
||||||
base = smil.find('./head/meta').get('base')
|
namespace = self._parse_smil_namespace(smil)
|
||||||
|
|
||||||
|
return self._parse_smil_formats(
|
||||||
|
smil, smil_url, video_id, namespace=namespace, f4m_params=f4m_params)
|
||||||
|
|
||||||
|
def _extract_smil_info(self, smil_url, video_id, fatal=True, f4m_params=None):
|
||||||
|
smil = self._download_smil(smil_url, video_id, fatal=fatal)
|
||||||
|
if smil is False:
|
||||||
|
return {}
|
||||||
|
return self._parse_smil(smil, smil_url, video_id, f4m_params=f4m_params)
|
||||||
|
|
||||||
|
def _download_smil(self, smil_url, video_id, fatal=True):
|
||||||
|
return self._download_xml(
|
||||||
|
smil_url, video_id, 'Downloading SMIL file',
|
||||||
|
'Unable to download SMIL file', fatal=fatal)
|
||||||
|
|
||||||
|
def _parse_smil(self, smil, smil_url, video_id, f4m_params=None):
|
||||||
|
namespace = self._parse_smil_namespace(smil)
|
||||||
|
|
||||||
|
formats = self._parse_smil_formats(
|
||||||
|
smil, smil_url, video_id, namespace=namespace, f4m_params=f4m_params)
|
||||||
|
subtitles = self._parse_smil_subtitles(smil, namespace=namespace)
|
||||||
|
|
||||||
|
video_id = os.path.splitext(url_basename(smil_url))[0]
|
||||||
|
title = None
|
||||||
|
description = None
|
||||||
|
for meta in smil.findall(self._xpath_ns('./head/meta', namespace)):
|
||||||
|
name = meta.attrib.get('name')
|
||||||
|
content = meta.attrib.get('content')
|
||||||
|
if not name or not content:
|
||||||
|
continue
|
||||||
|
if not title and name == 'title':
|
||||||
|
title = content
|
||||||
|
elif not description and name in ('description', 'abstract'):
|
||||||
|
description = content
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': title or video_id,
|
||||||
|
'description': description,
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _parse_smil_namespace(self, smil):
|
||||||
|
return self._search_regex(
|
||||||
|
r'(?i)^{([^}]+)?}smil$', smil.tag, 'namespace', default=None)
|
||||||
|
|
||||||
|
def _parse_smil_formats(self, smil, smil_url, video_id, namespace=None, f4m_params=None, transform_rtmp_url=None):
|
||||||
|
base = smil_url
|
||||||
|
for meta in smil.findall(self._xpath_ns('./head/meta', namespace)):
|
||||||
|
b = meta.get('base') or meta.get('httpBase')
|
||||||
|
if b:
|
||||||
|
base = b
|
||||||
|
break
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
rtmp_count = 0
|
rtmp_count = 0
|
||||||
if smil.findall('./body/seq/video'):
|
http_count = 0
|
||||||
video = smil.findall('./body/seq/video')[0]
|
|
||||||
fmts, rtmp_count = self._parse_smil_video(video, video_id, base, rtmp_count)
|
videos = smil.findall(self._xpath_ns('.//video', namespace))
|
||||||
formats.extend(fmts)
|
for video in videos:
|
||||||
else:
|
src = video.get('src')
|
||||||
for video in smil.findall('./body/switch/video'):
|
if not src:
|
||||||
fmts, rtmp_count = self._parse_smil_video(video, video_id, base, rtmp_count)
|
continue
|
||||||
formats.extend(fmts)
|
|
||||||
|
bitrate = int_or_none(video.get('system-bitrate') or video.get('systemBitrate'), 1000)
|
||||||
|
filesize = int_or_none(video.get('size') or video.get('fileSize'))
|
||||||
|
width = int_or_none(video.get('width'))
|
||||||
|
height = int_or_none(video.get('height'))
|
||||||
|
proto = video.get('proto')
|
||||||
|
ext = video.get('ext')
|
||||||
|
src_ext = determine_ext(src)
|
||||||
|
streamer = video.get('streamer') or base
|
||||||
|
|
||||||
|
if proto == 'rtmp' or streamer.startswith('rtmp'):
|
||||||
|
rtmp_count += 1
|
||||||
|
formats.append({
|
||||||
|
'url': streamer,
|
||||||
|
'play_path': src,
|
||||||
|
'ext': 'flv',
|
||||||
|
'format_id': 'rtmp-%d' % (rtmp_count if bitrate is None else bitrate),
|
||||||
|
'tbr': bitrate,
|
||||||
|
'filesize': filesize,
|
||||||
|
'width': width,
|
||||||
|
'height': height,
|
||||||
|
})
|
||||||
|
if transform_rtmp_url:
|
||||||
|
streamer, src = transform_rtmp_url(streamer, src)
|
||||||
|
formats[-1].update({
|
||||||
|
'url': streamer,
|
||||||
|
'play_path': src,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
src_url = src if src.startswith('http') else compat_urlparse.urljoin(base, src)
|
||||||
|
|
||||||
|
if proto == 'm3u8' or src_ext == 'm3u8':
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
src_url, video_id, ext or 'mp4', m3u8_id='hls'))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if src_ext == 'f4m':
|
||||||
|
f4m_url = src_url
|
||||||
|
if not f4m_params:
|
||||||
|
f4m_params = {
|
||||||
|
'hdcore': '3.2.0',
|
||||||
|
'plugin': 'flowplayer-3.2.0.1',
|
||||||
|
}
|
||||||
|
f4m_url += '&' if '?' in f4m_url else '?'
|
||||||
|
f4m_url += compat_urllib_parse.urlencode(f4m_params)
|
||||||
|
formats.extend(self._extract_f4m_formats(f4m_url, video_id, f4m_id='hds'))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if src_url.startswith('http'):
|
||||||
|
http_count += 1
|
||||||
|
formats.append({
|
||||||
|
'url': src_url,
|
||||||
|
'ext': ext or src_ext or 'flv',
|
||||||
|
'format_id': 'http-%d' % (bitrate or http_count),
|
||||||
|
'tbr': bitrate,
|
||||||
|
'filesize': filesize,
|
||||||
|
'width': width,
|
||||||
|
'height': height,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
return formats
|
return formats
|
||||||
|
|
||||||
def _parse_smil_video(self, video, video_id, base, rtmp_count):
|
def _parse_smil_subtitles(self, smil, namespace=None, subtitles_lang='en'):
|
||||||
src = video.get('src')
|
subtitles = {}
|
||||||
if not src:
|
for num, textstream in enumerate(smil.findall(self._xpath_ns('.//textstream', namespace))):
|
||||||
return ([], rtmp_count)
|
src = textstream.get('src')
|
||||||
bitrate = int_or_none(video.get('system-bitrate') or video.get('systemBitrate'), 1000)
|
if not src:
|
||||||
width = int_or_none(video.get('width'))
|
continue
|
||||||
height = int_or_none(video.get('height'))
|
ext = textstream.get('ext') or determine_ext(src)
|
||||||
proto = video.get('proto')
|
if not ext:
|
||||||
if not proto:
|
type_ = textstream.get('type')
|
||||||
if base:
|
SUBTITLES_TYPES = {
|
||||||
if base.startswith('rtmp'):
|
'text/vtt': 'vtt',
|
||||||
proto = 'rtmp'
|
'text/srt': 'srt',
|
||||||
elif base.startswith('http'):
|
'application/smptett+xml': 'tt',
|
||||||
proto = 'http'
|
}
|
||||||
ext = video.get('ext')
|
if type_ in SUBTITLES_TYPES:
|
||||||
if proto == 'm3u8':
|
ext = SUBTITLES_TYPES[type_]
|
||||||
return (self._extract_m3u8_formats(src, video_id, ext), rtmp_count)
|
lang = textstream.get('systemLanguage') or textstream.get('systemLanguageName') or textstream.get('lang') or subtitles_lang
|
||||||
elif proto == 'rtmp':
|
subtitles.setdefault(lang, []).append({
|
||||||
rtmp_count += 1
|
'url': src,
|
||||||
streamer = video.get('streamer') or base
|
'ext': ext,
|
||||||
return ([{
|
})
|
||||||
'url': streamer,
|
return subtitles
|
||||||
'play_path': src,
|
|
||||||
'ext': 'flv',
|
def _extract_xspf_playlist(self, playlist_url, playlist_id, fatal=True):
|
||||||
'format_id': 'rtmp-%d' % (rtmp_count if bitrate is None else bitrate),
|
xspf = self._download_xml(
|
||||||
'tbr': bitrate,
|
playlist_url, playlist_id, 'Downloading xpsf playlist',
|
||||||
'width': width,
|
'Unable to download xspf manifest', fatal=fatal)
|
||||||
'height': height,
|
if xspf is False:
|
||||||
}], rtmp_count)
|
return []
|
||||||
elif proto.startswith('http'):
|
return self._parse_xspf(xspf, playlist_id)
|
||||||
return ([{
|
|
||||||
'url': base + src,
|
def _parse_xspf(self, playlist, playlist_id):
|
||||||
'ext': ext or 'flv',
|
NS_MAP = {
|
||||||
'tbr': bitrate,
|
'xspf': 'http://xspf.org/ns/0/',
|
||||||
'width': width,
|
's1': 'http://static.streamone.nl/player/ns/0',
|
||||||
'height': height,
|
}
|
||||||
}], rtmp_count)
|
|
||||||
|
entries = []
|
||||||
|
for track in playlist.findall(xpath_with_ns('./xspf:trackList/xspf:track', NS_MAP)):
|
||||||
|
title = xpath_text(
|
||||||
|
track, xpath_with_ns('./xspf:title', NS_MAP), 'title', default=playlist_id)
|
||||||
|
description = xpath_text(
|
||||||
|
track, xpath_with_ns('./xspf:annotation', NS_MAP), 'description')
|
||||||
|
thumbnail = xpath_text(
|
||||||
|
track, xpath_with_ns('./xspf:image', NS_MAP), 'thumbnail')
|
||||||
|
duration = float_or_none(
|
||||||
|
xpath_text(track, xpath_with_ns('./xspf:duration', NS_MAP), 'duration'), 1000)
|
||||||
|
|
||||||
|
formats = [{
|
||||||
|
'url': location.text,
|
||||||
|
'format_id': location.get(xpath_with_ns('s1:label', NS_MAP)),
|
||||||
|
'width': int_or_none(location.get(xpath_with_ns('s1:width', NS_MAP))),
|
||||||
|
'height': int_or_none(location.get(xpath_with_ns('s1:height', NS_MAP))),
|
||||||
|
} for location in track.findall(xpath_with_ns('./xspf:location', NS_MAP))]
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
entries.append({
|
||||||
|
'id': playlist_id,
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
|
'duration': duration,
|
||||||
|
'formats': formats,
|
||||||
|
})
|
||||||
|
return entries
|
||||||
|
|
||||||
def _live_title(self, name):
|
def _live_title(self, name):
|
||||||
""" Generate the title for a live video """
|
""" Generate the title for a live video """
|
||||||
@@ -1015,6 +1241,12 @@ class InfoExtractor(object):
|
|||||||
None, '/', True, False, expire_time, '', None, None, None)
|
None, '/', True, False, expire_time, '', None, None, None)
|
||||||
self._downloader.cookiejar.set_cookie(cookie)
|
self._downloader.cookiejar.set_cookie(cookie)
|
||||||
|
|
||||||
|
def _get_cookies(self, url):
|
||||||
|
""" Return a compat_cookies.SimpleCookie with the cookies for the url """
|
||||||
|
req = compat_urllib_request.Request(url)
|
||||||
|
self._downloader.cookiejar.add_cookie_header(req)
|
||||||
|
return compat_cookies.SimpleCookie(req.get_header('Cookie'))
|
||||||
|
|
||||||
def get_testcases(self, include_onlymatching=False):
|
def get_testcases(self, include_onlymatching=False):
|
||||||
t = getattr(self, '_TEST', None)
|
t = getattr(self, '_TEST', None)
|
||||||
if t:
|
if t:
|
||||||
@@ -1053,6 +1285,23 @@ class InfoExtractor(object):
|
|||||||
def _get_subtitles(self, *args, **kwargs):
|
def _get_subtitles(self, *args, **kwargs):
|
||||||
raise NotImplementedError("This method must be implemented by subclasses")
|
raise NotImplementedError("This method must be implemented by subclasses")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _merge_subtitle_items(subtitle_list1, subtitle_list2):
|
||||||
|
""" Merge subtitle items for one language. Items with duplicated URLs
|
||||||
|
will be dropped. """
|
||||||
|
list1_urls = set([item['url'] for item in subtitle_list1])
|
||||||
|
ret = list(subtitle_list1)
|
||||||
|
ret.extend([item for item in subtitle_list2 if item['url'] not in list1_urls])
|
||||||
|
return ret
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _merge_subtitles(cls, subtitle_dict1, subtitle_dict2):
|
||||||
|
""" Merge two subtitle dictionaries, language by language. """
|
||||||
|
ret = dict(subtitle_dict1)
|
||||||
|
for lang in subtitle_dict2:
|
||||||
|
ret[lang] = cls._merge_subtitle_items(subtitle_dict1.get(lang, []), subtitle_dict2[lang])
|
||||||
|
return ret
|
||||||
|
|
||||||
def extract_automatic_captions(self, *args, **kwargs):
|
def extract_automatic_captions(self, *args, **kwargs):
|
||||||
if (self._downloader.params.get('writeautomaticsub', False) or
|
if (self._downloader.params.get('writeautomaticsub', False) or
|
||||||
self._downloader.params.get('listsubtitles')):
|
self._downloader.params.get('listsubtitles')):
|
||||||
@@ -1062,14 +1311,11 @@ class InfoExtractor(object):
|
|||||||
def _get_automatic_captions(self, *args, **kwargs):
|
def _get_automatic_captions(self, *args, **kwargs):
|
||||||
raise NotImplementedError("This method must be implemented by subclasses")
|
raise NotImplementedError("This method must be implemented by subclasses")
|
||||||
|
|
||||||
def _subtitles_timecode(self, seconds):
|
|
||||||
return '%02d:%02d:%02d.%03d' % (seconds / 3600, (seconds % 3600) / 60, seconds % 60, (seconds % 1) * 1000)
|
|
||||||
|
|
||||||
|
|
||||||
class SearchInfoExtractor(InfoExtractor):
|
class SearchInfoExtractor(InfoExtractor):
|
||||||
"""
|
"""
|
||||||
Base class for paged search queries extractors.
|
Base class for paged search queries extractors.
|
||||||
They accept urls in the format _SEARCH_KEY(|all|[0-9]):{query}
|
They accept URLs in the format _SEARCH_KEY(|all|[0-9]):{query}
|
||||||
Instances should define _SEARCH_KEY and _MAX_RESULTS.
|
Instances should define _SEARCH_KEY and _MAX_RESULTS.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -11,39 +11,65 @@ from ..utils import (
|
|||||||
|
|
||||||
class CrackedIE(InfoExtractor):
|
class CrackedIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?cracked\.com/video_(?P<id>\d+)_[\da-z-]+\.html'
|
_VALID_URL = r'https?://(?:www\.)?cracked\.com/video_(?P<id>\d+)_[\da-z-]+\.html'
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'http://www.cracked.com/video_19006_4-plot-holes-you-didnt-notice-in-your-favorite-movies.html',
|
'url': 'http://www.cracked.com/video_19070_if-animal-actors-got-e21-true-hollywood-stories.html',
|
||||||
'md5': '4b29a5eeec292cd5eca6388c7558db9e',
|
'md5': '89b90b9824e3806ca95072c4d78f13f7',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '19006',
|
'id': '19070',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': '4 Plot Holes You Didn\'t Notice in Your Favorite Movies',
|
'title': 'If Animal Actors Got E! True Hollywood Stories',
|
||||||
'description': 'md5:3b909e752661db86007d10e5ec2df769',
|
'timestamp': 1404954000,
|
||||||
'timestamp': 1405659600,
|
'upload_date': '20140710',
|
||||||
'upload_date': '20140718',
|
|
||||||
}
|
}
|
||||||
}
|
}, {
|
||||||
|
# youtube embed
|
||||||
|
'url': 'http://www.cracked.com/video_19006_4-plot-holes-you-didnt-notice-in-your-favorite-movies.html',
|
||||||
|
'md5': 'ccd52866b50bde63a6ef3b35016ba8c7',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'EjI00A3rZD0',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': "4 Plot Holes You Didn't Notice in Your Favorite Movies - The Spit Take",
|
||||||
|
'description': 'md5:c603708c718b796fe6079e2b3351ffc7',
|
||||||
|
'upload_date': '20140725',
|
||||||
|
'uploader_id': 'Cracked',
|
||||||
|
'uploader': 'Cracked',
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
youtube_url = self._search_regex(
|
||||||
|
r'<iframe[^>]+src="((?:https?:)?//www\.youtube\.com/embed/[^"]+)"',
|
||||||
|
webpage, 'youtube url', default=None)
|
||||||
|
if youtube_url:
|
||||||
|
return self.url_result(youtube_url, 'Youtube')
|
||||||
|
|
||||||
video_url = self._html_search_regex(
|
video_url = self._html_search_regex(
|
||||||
[r'var\s+CK_vidSrc\s*=\s*"([^"]+)"', r'<video\s+src="([^"]+)"'], webpage, 'video URL')
|
[r'var\s+CK_vidSrc\s*=\s*"([^"]+)"', r'<video\s+src="([^"]+)"'],
|
||||||
|
webpage, 'video URL')
|
||||||
|
|
||||||
title = self._og_search_title(webpage)
|
title = self._search_regex(
|
||||||
description = self._og_search_description(webpage)
|
[r'property="?og:title"?\s+content="([^"]+)"', r'class="?title"?>([^<]+)'],
|
||||||
|
webpage, 'title')
|
||||||
|
|
||||||
timestamp = self._html_search_regex(r'<time datetime="([^"]+)"', webpage, 'upload date', fatal=False)
|
description = self._search_regex(
|
||||||
|
r'name="?(?:og:)?description"?\s+content="([^"]+)"',
|
||||||
|
webpage, 'description', default=None)
|
||||||
|
|
||||||
|
timestamp = self._html_search_regex(
|
||||||
|
r'"date"\s*:\s*"([^"]+)"', webpage, 'upload date', fatal=False)
|
||||||
if timestamp:
|
if timestamp:
|
||||||
timestamp = parse_iso8601(timestamp[:-6])
|
timestamp = parse_iso8601(timestamp[:-6])
|
||||||
|
|
||||||
view_count = str_to_int(self._html_search_regex(
|
view_count = str_to_int(self._html_search_regex(
|
||||||
r'<span class="views" id="viewCounts">([\d,\.]+) Views</span>', webpage, 'view count', fatal=False))
|
r'<span\s+class="?views"? id="?viewCounts"?>([\d,\.]+) Views</span>',
|
||||||
|
webpage, 'view count', fatal=False))
|
||||||
comment_count = str_to_int(self._html_search_regex(
|
comment_count = str_to_int(self._html_search_regex(
|
||||||
r'<span id="commentCounts">([\d,\.]+)</span>', webpage, 'comment count', fatal=False))
|
r'<span\s+id="?commentCounts"?>([\d,\.]+)</span>',
|
||||||
|
webpage, 'comment count', fatal=False))
|
||||||
|
|
||||||
m = re.search(r'_(?P<width>\d+)X(?P<height>\d+)\.mp4$', video_url)
|
m = re.search(r'_(?P<width>\d+)X(?P<height>\d+)\.mp4$', video_url)
|
||||||
if m:
|
if m:
|
||||||
|
|||||||
60
youtube_dl/extractor/crooksandliars.py
Normal file
60
youtube_dl/extractor/crooksandliars.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
int_or_none,
|
||||||
|
qualities,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CrooksAndLiarsIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://embed\.crooksandliars\.com/(?:embed|v)/(?P<id>[A-Za-z0-9]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://embed.crooksandliars.com/embed/8RUoRhRi',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '8RUoRhRi',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Fox & Friends Says Protecting Atheists From Discrimination Is Anti-Christian!',
|
||||||
|
'description': 'md5:e1a46ad1650e3a5ec7196d432799127f',
|
||||||
|
'thumbnail': 're:^https?://.*\.jpg',
|
||||||
|
'timestamp': 1428207000,
|
||||||
|
'upload_date': '20150405',
|
||||||
|
'uploader': 'Heather',
|
||||||
|
'duration': 236,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'http://embed.crooksandliars.com/v/MTE3MjUtMzQ2MzA',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
webpage = self._download_webpage(
|
||||||
|
'http://embed.crooksandliars.com/embed/%s' % video_id, video_id)
|
||||||
|
|
||||||
|
manifest = self._parse_json(
|
||||||
|
self._search_regex(
|
||||||
|
r'var\s+manifest\s*=\s*({.+?})\n', webpage, 'manifest JSON'),
|
||||||
|
video_id)
|
||||||
|
|
||||||
|
quality = qualities(('webm_low', 'mp4_low', 'webm_high', 'mp4_high'))
|
||||||
|
|
||||||
|
formats = [{
|
||||||
|
'url': item['url'],
|
||||||
|
'format_id': item['type'],
|
||||||
|
'quality': quality(item['type']),
|
||||||
|
} for item in manifest['flavors'] if item['mime'].startswith('video/')]
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'url': url,
|
||||||
|
'id': video_id,
|
||||||
|
'title': manifest['title'],
|
||||||
|
'description': manifest.get('description'),
|
||||||
|
'thumbnail': self._proto_relative_url(manifest.get('poster')),
|
||||||
|
'timestamp': int_or_none(manifest.get('created')),
|
||||||
|
'uploader': manifest.get('author'),
|
||||||
|
'duration': int_or_none(manifest.get('duration')),
|
||||||
|
'formats': formats,
|
||||||
|
}
|
||||||
@@ -12,22 +12,43 @@ from math import pow, sqrt, floor
|
|||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
compat_urllib_parse,
|
compat_urllib_parse,
|
||||||
|
compat_urllib_parse_unquote,
|
||||||
compat_urllib_request,
|
compat_urllib_request,
|
||||||
|
compat_urlparse,
|
||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
bytes_to_intlist,
|
bytes_to_intlist,
|
||||||
intlist_to_bytes,
|
intlist_to_bytes,
|
||||||
|
int_or_none,
|
||||||
|
remove_end,
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
urlencode_postdata,
|
urlencode_postdata,
|
||||||
|
xpath_text,
|
||||||
)
|
)
|
||||||
from ..aes import (
|
from ..aes import (
|
||||||
aes_cbc_decrypt,
|
aes_cbc_decrypt,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CrunchyrollIE(InfoExtractor):
|
class CrunchyrollBaseIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:(?P<prefix>www|m)\.)?(?P<url>crunchyroll\.(?:com|fr)/(?:[^/]*/[^/?&]*?|media/\?id=)(?P<video_id>[0-9]+))(?:[/?&]|$)'
|
def _download_webpage(self, url_or_request, video_id, note=None, errnote=None, fatal=True, tries=1, timeout=5, encoding=None):
|
||||||
|
request = (url_or_request if isinstance(url_or_request, compat_urllib_request.Request)
|
||||||
|
else compat_urllib_request.Request(url_or_request))
|
||||||
|
# Accept-Language must be set explicitly to accept any language to avoid issues
|
||||||
|
# similar to https://github.com/rg3/youtube-dl/issues/6797.
|
||||||
|
# Along with IP address Crunchyroll uses Accept-Language to guess whether georestriction
|
||||||
|
# should be imposed or not (from what I can see it just takes the first language
|
||||||
|
# ignoring the priority and requires it to correspond the IP). By the way this causes
|
||||||
|
# Crunchyroll to not work in georestriction cases in some browsers that don't place
|
||||||
|
# the locale lang first in header. However allowing any language seems to workaround the issue.
|
||||||
|
request.add_header('Accept-Language', '*')
|
||||||
|
return super(CrunchyrollBaseIE, self)._download_webpage(
|
||||||
|
request, video_id, note, errnote, fatal, tries, timeout, encoding)
|
||||||
|
|
||||||
|
|
||||||
|
class CrunchyrollIE(CrunchyrollBaseIE):
|
||||||
|
_VALID_URL = r'https?://(?:(?P<prefix>www|m)\.)?(?P<url>crunchyroll\.(?:com|fr)/(?:media(?:-|/\?id=)|[^/]*/[^/?&]*?)(?P<video_id>[0-9]+))(?:[/?&]|$)'
|
||||||
_NETRC_MACHINE = 'crunchyroll'
|
_NETRC_MACHINE = 'crunchyroll'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.crunchyroll.com/wanna-be-the-strongest-in-the-world/episode-1-an-idol-wrestler-is-born-645513',
|
'url': 'http://www.crunchyroll.com/wanna-be-the-strongest-in-the-world/episode-1-an-idol-wrestler-is-born-645513',
|
||||||
@@ -45,6 +66,22 @@ class CrunchyrollIE(InfoExtractor):
|
|||||||
# rtmp
|
# rtmp
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.crunchyroll.com/media-589804/culture-japan-1',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '589804',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': 'Culture Japan Episode 1 – Rebuilding Japan after the 3.11',
|
||||||
|
'description': 'md5:fe2743efedb49d279552926d0bd0cd9e',
|
||||||
|
'thumbnail': 're:^https?://.*\.jpg$',
|
||||||
|
'uploader': 'Danny Choo Network',
|
||||||
|
'upload_date': '20120213',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# rtmp
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.crunchyroll.fr/girl-friend-beta/episode-11-goodbye-la-mode-661697',
|
'url': 'http://www.crunchyroll.fr/girl-friend-beta/episode-11-goodbye-la-mode-661697',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
@@ -76,8 +113,8 @@ class CrunchyrollIE(InfoExtractor):
|
|||||||
self._login()
|
self._login()
|
||||||
|
|
||||||
def _decrypt_subtitles(self, data, iv, id):
|
def _decrypt_subtitles(self, data, iv, id):
|
||||||
data = bytes_to_intlist(data)
|
data = bytes_to_intlist(base64.b64decode(data.encode('utf-8')))
|
||||||
iv = bytes_to_intlist(iv)
|
iv = bytes_to_intlist(base64.b64decode(iv.encode('utf-8')))
|
||||||
id = int(id)
|
id = int(id)
|
||||||
|
|
||||||
def obfuscate_key_aux(count, modulo, start):
|
def obfuscate_key_aux(count, modulo, start):
|
||||||
@@ -179,6 +216,16 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
def _extract_subtitles(self, subtitle):
|
||||||
|
sub_root = xml.etree.ElementTree.fromstring(subtitle)
|
||||||
|
return [{
|
||||||
|
'ext': 'srt',
|
||||||
|
'data': self._convert_subtitles_to_srt(sub_root),
|
||||||
|
}, {
|
||||||
|
'ext': 'ass',
|
||||||
|
'data': self._convert_subtitles_to_ass(sub_root),
|
||||||
|
}]
|
||||||
|
|
||||||
def _get_subtitles(self, video_id, webpage):
|
def _get_subtitles(self, video_id, webpage):
|
||||||
subtitles = {}
|
subtitles = {}
|
||||||
for sub_id, sub_name in re.findall(r'\?ssid=([0-9]+)" title="([^"]+)', webpage):
|
for sub_id, sub_name in re.findall(r'\?ssid=([0-9]+)" title="([^"]+)', webpage):
|
||||||
@@ -190,25 +237,11 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|||||||
data = self._search_regex(r'<data>([^<]+)', sub_page, 'subtitle_data', fatal=False)
|
data = self._search_regex(r'<data>([^<]+)', sub_page, 'subtitle_data', fatal=False)
|
||||||
if not id or not iv or not data:
|
if not id or not iv or not data:
|
||||||
continue
|
continue
|
||||||
id = int(id)
|
|
||||||
iv = base64.b64decode(iv)
|
|
||||||
data = base64.b64decode(data)
|
|
||||||
|
|
||||||
subtitle = self._decrypt_subtitles(data, iv, id).decode('utf-8')
|
subtitle = self._decrypt_subtitles(data, iv, id).decode('utf-8')
|
||||||
lang_code = self._search_regex(r'lang_code=["\']([^"\']+)', subtitle, 'subtitle_lang_code', fatal=False)
|
lang_code = self._search_regex(r'lang_code=["\']([^"\']+)', subtitle, 'subtitle_lang_code', fatal=False)
|
||||||
if not lang_code:
|
if not lang_code:
|
||||||
continue
|
continue
|
||||||
sub_root = xml.etree.ElementTree.fromstring(subtitle)
|
subtitles[lang_code] = self._extract_subtitles(subtitle)
|
||||||
subtitles[lang_code] = [
|
|
||||||
{
|
|
||||||
'ext': 'srt',
|
|
||||||
'data': self._convert_subtitles_to_srt(sub_root),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'ext': 'ass',
|
|
||||||
'data': self._convert_subtitles_to_ass(sub_root),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
return subtitles
|
return subtitles
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@@ -222,7 +255,9 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|||||||
webpage_url = 'http://www.' + mobj.group('url')
|
webpage_url = 'http://www.' + mobj.group('url')
|
||||||
|
|
||||||
webpage = self._download_webpage(webpage_url, video_id, 'Downloading webpage')
|
webpage = self._download_webpage(webpage_url, video_id, 'Downloading webpage')
|
||||||
note_m = self._html_search_regex(r'<div class="showmedia-trailer-notice">(.+?)</div>', webpage, 'trailer-notice', default='')
|
note_m = self._html_search_regex(
|
||||||
|
r'<div class="showmedia-trailer-notice">(.+?)</div>',
|
||||||
|
webpage, 'trailer-notice', default='')
|
||||||
if note_m:
|
if note_m:
|
||||||
raise ExtractorError(note_m)
|
raise ExtractorError(note_m)
|
||||||
|
|
||||||
@@ -232,17 +267,24 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|||||||
if msg.get('type') == 'error':
|
if msg.get('type') == 'error':
|
||||||
raise ExtractorError('crunchyroll returned error: %s' % msg['message_body'], expected=True)
|
raise ExtractorError('crunchyroll returned error: %s' % msg['message_body'], expected=True)
|
||||||
|
|
||||||
|
if 'To view this, please log in to verify you are 18 or older.' in webpage:
|
||||||
|
self.raise_login_required()
|
||||||
|
|
||||||
video_title = self._html_search_regex(r'<h1[^>]*>(.+?)</h1>', webpage, 'video_title', flags=re.DOTALL)
|
video_title = self._html_search_regex(r'<h1[^>]*>(.+?)</h1>', webpage, 'video_title', flags=re.DOTALL)
|
||||||
video_title = re.sub(r' {2,}', ' ', video_title)
|
video_title = re.sub(r' {2,}', ' ', video_title)
|
||||||
video_description = self._html_search_regex(r'"description":"([^"]+)', webpage, 'video_description', default='')
|
video_description = self._html_search_regex(r'"description":"([^"]+)', webpage, 'video_description', default='')
|
||||||
if not video_description:
|
if not video_description:
|
||||||
video_description = None
|
video_description = None
|
||||||
video_upload_date = self._html_search_regex(r'<div>Availability for free users:(.+?)</div>', webpage, 'video_upload_date', fatal=False, flags=re.DOTALL)
|
video_upload_date = self._html_search_regex(
|
||||||
|
[r'<div>Availability for free users:(.+?)</div>', r'<div>[^<>]+<span>\s*(.+?\d{4})\s*</span></div>'],
|
||||||
|
webpage, 'video_upload_date', fatal=False, flags=re.DOTALL)
|
||||||
if video_upload_date:
|
if video_upload_date:
|
||||||
video_upload_date = unified_strdate(video_upload_date)
|
video_upload_date = unified_strdate(video_upload_date)
|
||||||
video_uploader = self._html_search_regex(r'<div>\s*Publisher:(.+?)</div>', webpage, 'video_uploader', fatal=False, flags=re.DOTALL)
|
video_uploader = self._html_search_regex(
|
||||||
|
r'<a[^>]+href="/publisher/[^"]+"[^>]*>([^<]+)</a>', webpage,
|
||||||
|
'video_uploader', fatal=False)
|
||||||
|
|
||||||
playerdata_url = compat_urllib_parse.unquote(self._html_search_regex(r'"config_url":"([^"]+)', webpage, 'playerdata_url'))
|
playerdata_url = compat_urllib_parse_unquote(self._html_search_regex(r'"config_url":"([^"]+)', webpage, 'playerdata_url'))
|
||||||
playerdata_req = compat_urllib_request.Request(playerdata_url)
|
playerdata_req = compat_urllib_request.Request(playerdata_url)
|
||||||
playerdata_req.data = compat_urllib_parse.urlencode({'current_page': webpage_url})
|
playerdata_req.data = compat_urllib_parse.urlencode({'current_page': webpage_url})
|
||||||
playerdata_req.add_header('Content-Type', 'application/x-www-form-urlencoded')
|
playerdata_req.add_header('Content-Type', 'application/x-www-form-urlencoded')
|
||||||
@@ -255,23 +297,44 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|||||||
for fmt in re.findall(r'showmedia\.([0-9]{3,4})p', webpage):
|
for fmt in re.findall(r'showmedia\.([0-9]{3,4})p', webpage):
|
||||||
stream_quality, stream_format = self._FORMAT_IDS[fmt]
|
stream_quality, stream_format = self._FORMAT_IDS[fmt]
|
||||||
video_format = fmt + 'p'
|
video_format = fmt + 'p'
|
||||||
streamdata_req = compat_urllib_request.Request('http://www.crunchyroll.com/xml/')
|
streamdata_req = compat_urllib_request.Request(
|
||||||
# urlencode doesn't work!
|
'http://www.crunchyroll.com/xml/?req=RpcApiVideoPlayer_GetStandardConfig&media_id=%s&video_format=%s&video_quality=%s'
|
||||||
streamdata_req.data = 'req=RpcApiVideoEncode%5FGetStreamInfo&video%5Fencode%5Fquality=' + stream_quality + '&media%5Fid=' + stream_id + '&video%5Fformat=' + stream_format
|
% (stream_id, stream_format, stream_quality),
|
||||||
|
compat_urllib_parse.urlencode({'current_page': url}).encode('utf-8'))
|
||||||
streamdata_req.add_header('Content-Type', 'application/x-www-form-urlencoded')
|
streamdata_req.add_header('Content-Type', 'application/x-www-form-urlencoded')
|
||||||
streamdata_req.add_header('Content-Length', str(len(streamdata_req.data)))
|
|
||||||
streamdata = self._download_xml(
|
streamdata = self._download_xml(
|
||||||
streamdata_req, video_id,
|
streamdata_req, video_id,
|
||||||
note='Downloading media info for %s' % video_format)
|
note='Downloading media info for %s' % video_format)
|
||||||
video_url = streamdata.find('.//host').text
|
stream_info = streamdata.find('./{default}preload/stream_info')
|
||||||
video_play_path = streamdata.find('.//file').text
|
video_url = stream_info.find('./host').text
|
||||||
formats.append({
|
video_play_path = stream_info.find('./file').text
|
||||||
|
metadata = stream_info.find('./metadata')
|
||||||
|
format_info = {
|
||||||
|
'format': video_format,
|
||||||
|
'format_id': video_format,
|
||||||
|
'height': int_or_none(xpath_text(metadata, './height')),
|
||||||
|
'width': int_or_none(xpath_text(metadata, './width')),
|
||||||
|
}
|
||||||
|
|
||||||
|
if '.fplive.net/' in video_url:
|
||||||
|
video_url = re.sub(r'^rtmpe?://', 'http://', video_url.strip())
|
||||||
|
parsed_video_url = compat_urlparse.urlparse(video_url)
|
||||||
|
direct_video_url = compat_urlparse.urlunparse(parsed_video_url._replace(
|
||||||
|
netloc='v.lvlt.crcdn.net',
|
||||||
|
path='%s/%s' % (remove_end(parsed_video_url.path, '/'), video_play_path.split(':')[-1])))
|
||||||
|
if self._is_valid_url(direct_video_url, video_id, video_format):
|
||||||
|
format_info.update({
|
||||||
|
'url': direct_video_url,
|
||||||
|
})
|
||||||
|
formats.append(format_info)
|
||||||
|
continue
|
||||||
|
|
||||||
|
format_info.update({
|
||||||
'url': video_url,
|
'url': video_url,
|
||||||
'play_path': video_play_path,
|
'play_path': video_play_path,
|
||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
'format': video_format,
|
|
||||||
'format_id': video_format,
|
|
||||||
})
|
})
|
||||||
|
formats.append(format_info)
|
||||||
|
|
||||||
subtitles = self.extract_subtitles(video_id, webpage)
|
subtitles = self.extract_subtitles(video_id, webpage)
|
||||||
|
|
||||||
@@ -287,7 +350,7 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class CrunchyrollShowPlaylistIE(InfoExtractor):
|
class CrunchyrollShowPlaylistIE(CrunchyrollBaseIE):
|
||||||
IE_NAME = "crunchyroll:playlist"
|
IE_NAME = "crunchyroll:playlist"
|
||||||
_VALID_URL = r'https?://(?:(?P<prefix>www|m)\.)?(?P<url>crunchyroll\.com/(?!(?:news|anime-news|library|forum|launchcalendar|lineup|store|comics|freetrial|login))(?P<id>[\w\-]+))/?$'
|
_VALID_URL = r'https?://(?:(?P<prefix>www|m)\.)?(?P<url>crunchyroll\.com/(?!(?:news|anime-news|library|forum|launchcalendar|lineup|store|comics|freetrial|login))(?P<id>[\w\-]+))/?$'
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ from ..utils import (
|
|||||||
int_or_none,
|
int_or_none,
|
||||||
unescapeHTML,
|
unescapeHTML,
|
||||||
find_xpath_attr,
|
find_xpath_attr,
|
||||||
|
smuggle_url,
|
||||||
|
determine_ext,
|
||||||
)
|
)
|
||||||
|
from .senateisvp import SenateISVPIE
|
||||||
|
|
||||||
|
|
||||||
class CSpanIE(InfoExtractor):
|
class CSpanIE(InfoExtractor):
|
||||||
@@ -35,11 +38,22 @@ class CSpanIE(InfoExtractor):
|
|||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.c-span.org/video/?318608-1/gm-ignition-switch-recall',
|
'url': 'http://www.c-span.org/video/?318608-1/gm-ignition-switch-recall',
|
||||||
|
'md5': '446562a736c6bf97118e389433ed88d4',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '342759',
|
'id': '342759',
|
||||||
|
'ext': 'mp4',
|
||||||
'title': 'General Motors Ignition Switch Recall',
|
'title': 'General Motors Ignition Switch Recall',
|
||||||
|
'duration': 14848,
|
||||||
|
'description': 'md5:70c7c3b8fa63fa60d42772440596034c'
|
||||||
},
|
},
|
||||||
'playlist_duration_sum': 14855,
|
}, {
|
||||||
|
# Video from senate.gov
|
||||||
|
'url': 'http://www.c-span.org/video/?104517-1/immigration-reforms-needed-protect-skilled-american-workers',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'judiciary031715',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': 'Immigration Reforms Needed to Protect Skilled American Workers',
|
||||||
|
}
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@@ -56,7 +70,7 @@ class CSpanIE(InfoExtractor):
|
|||||||
# present, otherwise this is a stripped version
|
# present, otherwise this is a stripped version
|
||||||
r'<p class=\'initial\'>(.*?)</p>'
|
r'<p class=\'initial\'>(.*?)</p>'
|
||||||
],
|
],
|
||||||
webpage, 'description', flags=re.DOTALL)
|
webpage, 'description', flags=re.DOTALL, default=None)
|
||||||
|
|
||||||
info_url = 'http://c-spanvideo.org/videoLibrary/assets/player/ajax-player.php?os=android&html5=program&id=' + video_id
|
info_url = 'http://c-spanvideo.org/videoLibrary/assets/player/ajax-player.php?os=android&html5=program&id=' + video_id
|
||||||
data = self._download_json(info_url, video_id)
|
data = self._download_json(info_url, video_id)
|
||||||
@@ -68,7 +82,16 @@ class CSpanIE(InfoExtractor):
|
|||||||
title = find_xpath_attr(doc, './/string', 'name', 'title').text
|
title = find_xpath_attr(doc, './/string', 'name', 'title').text
|
||||||
thumbnail = find_xpath_attr(doc, './/string', 'name', 'poster').text
|
thumbnail = find_xpath_attr(doc, './/string', 'name', 'poster').text
|
||||||
|
|
||||||
|
senate_isvp_url = SenateISVPIE._search_iframe_url(webpage)
|
||||||
|
if senate_isvp_url:
|
||||||
|
surl = smuggle_url(senate_isvp_url, {'force_title': title})
|
||||||
|
return self.url_result(surl, 'SenateISVP', video_id, title)
|
||||||
|
|
||||||
files = data['video']['files']
|
files = data['video']['files']
|
||||||
|
try:
|
||||||
|
capfile = data['video']['capfile']['#text']
|
||||||
|
except KeyError:
|
||||||
|
capfile = None
|
||||||
|
|
||||||
entries = [{
|
entries = [{
|
||||||
'id': '%s_%d' % (video_id, partnum + 1),
|
'id': '%s_%d' % (video_id, partnum + 1),
|
||||||
@@ -79,11 +102,22 @@ class CSpanIE(InfoExtractor):
|
|||||||
'description': description,
|
'description': description,
|
||||||
'thumbnail': thumbnail,
|
'thumbnail': thumbnail,
|
||||||
'duration': int_or_none(f.get('length', {}).get('#text')),
|
'duration': int_or_none(f.get('length', {}).get('#text')),
|
||||||
|
'subtitles': {
|
||||||
|
'en': [{
|
||||||
|
'url': capfile,
|
||||||
|
'ext': determine_ext(capfile, 'dfxp')
|
||||||
|
}],
|
||||||
|
} if capfile else None,
|
||||||
} for partnum, f in enumerate(files)]
|
} for partnum, f in enumerate(files)]
|
||||||
|
|
||||||
return {
|
if len(entries) == 1:
|
||||||
'_type': 'playlist',
|
entry = dict(entries[0])
|
||||||
'entries': entries,
|
entry['id'] = video_id
|
||||||
'title': title,
|
return entry
|
||||||
'id': video_id,
|
else:
|
||||||
}
|
return {
|
||||||
|
'_type': 'playlist',
|
||||||
|
'entries': entries,
|
||||||
|
'title': title,
|
||||||
|
'id': video_id,
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from ..utils import parse_iso8601, ExtractorError
|
|||||||
|
|
||||||
|
|
||||||
class CtsNewsIE(InfoExtractor):
|
class CtsNewsIE(InfoExtractor):
|
||||||
|
IE_DESC = '華視新聞'
|
||||||
# https connection failed (Connection reset)
|
# https connection failed (Connection reset)
|
||||||
_VALID_URL = r'http://news\.cts\.com\.tw/[a-z]+/[a-z]+/\d+/(?P<id>\d+)\.html'
|
_VALID_URL = r'http://news\.cts\.com\.tw/[a-z]+/[a-z]+/\d+/(?P<id>\d+)\.html'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ from ..compat import (
|
|||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
determine_ext,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
orderedSet,
|
parse_iso8601,
|
||||||
str_to_int,
|
str_to_int,
|
||||||
unescapeHTML,
|
unescapeHTML,
|
||||||
)
|
)
|
||||||
@@ -25,14 +26,19 @@ class DailymotionBaseInfoExtractor(InfoExtractor):
|
|||||||
def _build_request(url):
|
def _build_request(url):
|
||||||
"""Build a request with the family filter disabled"""
|
"""Build a request with the family filter disabled"""
|
||||||
request = compat_urllib_request.Request(url)
|
request = compat_urllib_request.Request(url)
|
||||||
request.add_header('Cookie', 'family_filter=off')
|
request.add_header('Cookie', 'family_filter=off; ff=off')
|
||||||
request.add_header('Cookie', 'ff=off')
|
|
||||||
return request
|
return request
|
||||||
|
|
||||||
|
def _download_webpage_handle_no_ff(self, url, *args, **kwargs):
|
||||||
|
request = self._build_request(url)
|
||||||
|
return self._download_webpage_handle(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def _download_webpage_no_ff(self, url, *args, **kwargs):
|
||||||
|
request = self._build_request(url)
|
||||||
|
return self._download_webpage(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class DailymotionIE(DailymotionBaseInfoExtractor):
|
class DailymotionIE(DailymotionBaseInfoExtractor):
|
||||||
"""Information Extractor for Dailymotion"""
|
|
||||||
|
|
||||||
_VALID_URL = r'(?i)(?:https?://)?(?:(www|touch)\.)?dailymotion\.[a-z]{2,3}/(?:(embed|#)/)?video/(?P<id>[^/?_]+)'
|
_VALID_URL = r'(?i)(?:https?://)?(?:(www|touch)\.)?dailymotion\.[a-z]{2,3}/(?:(embed|#)/)?video/(?P<id>[^/?_]+)'
|
||||||
IE_NAME = 'dailymotion'
|
IE_NAME = 'dailymotion'
|
||||||
|
|
||||||
@@ -51,8 +57,17 @@ class DailymotionIE(DailymotionBaseInfoExtractor):
|
|||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'x2iuewm',
|
'id': 'x2iuewm',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'uploader': 'IGN',
|
|
||||||
'title': 'Steam Machine Models, Pricing Listed on Steam Store - IGN News',
|
'title': 'Steam Machine Models, Pricing Listed on Steam Store - IGN News',
|
||||||
|
'description': 'Several come bundled with the Steam Controller.',
|
||||||
|
'thumbnail': 're:^https?:.*\.(?:jpg|png)$',
|
||||||
|
'duration': 74,
|
||||||
|
'timestamp': 1425657362,
|
||||||
|
'upload_date': '20150306',
|
||||||
|
'uploader': 'IGN',
|
||||||
|
'uploader_id': 'xijv66',
|
||||||
|
'age_limit': 0,
|
||||||
|
'view_count': int,
|
||||||
|
'comment_count': int,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
# Vevo video
|
# Vevo video
|
||||||
@@ -86,37 +101,106 @@ class DailymotionIE(DailymotionBaseInfoExtractor):
|
|||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
url = 'http://www.dailymotion.com/video/%s' % video_id
|
|
||||||
|
|
||||||
# Retrieve video webpage to extract further information
|
webpage = self._download_webpage_no_ff(
|
||||||
request = self._build_request(url)
|
'https://www.dailymotion.com/video/%s' % video_id, video_id)
|
||||||
webpage = self._download_webpage(request, video_id)
|
|
||||||
|
|
||||||
# Extract URL, uploader and title from webpage
|
|
||||||
self.report_extraction(video_id)
|
|
||||||
|
|
||||||
# It may just embed a vevo video:
|
|
||||||
m_vevo = re.search(
|
|
||||||
r'<link rel="video_src" href="[^"]*?vevo.com[^"]*?video=(?P<id>[\w]*)',
|
|
||||||
webpage)
|
|
||||||
if m_vevo is not None:
|
|
||||||
vevo_id = m_vevo.group('id')
|
|
||||||
self.to_screen('Vevo video detected: %s' % vevo_id)
|
|
||||||
return self.url_result('vevo:%s' % vevo_id, ie='Vevo')
|
|
||||||
|
|
||||||
age_limit = self._rta_search(webpage)
|
age_limit = self._rta_search(webpage)
|
||||||
|
|
||||||
video_upload_date = None
|
description = self._og_search_description(webpage) or self._html_search_meta(
|
||||||
mobj = re.search(r'<div class="[^"]*uploaded_cont[^"]*" title="[^"]*">([0-9]{2})-([0-9]{2})-([0-9]{4})</div>', webpage)
|
'description', webpage, 'description')
|
||||||
if mobj is not None:
|
|
||||||
video_upload_date = mobj.group(3) + mobj.group(2) + mobj.group(1)
|
view_count = str_to_int(self._search_regex(
|
||||||
|
[r'<meta[^>]+itemprop="interactionCount"[^>]+content="UserPlays:(\d+)"',
|
||||||
|
r'video_views_count[^>]+>\s+([\d\.,]+)'],
|
||||||
|
webpage, 'view count', fatal=False))
|
||||||
|
comment_count = int_or_none(self._search_regex(
|
||||||
|
r'<meta[^>]+itemprop="interactionCount"[^>]+content="UserComments:(\d+)"',
|
||||||
|
webpage, 'comment count', fatal=False))
|
||||||
|
|
||||||
|
player_v5 = self._search_regex(
|
||||||
|
r'playerV5\s*=\s*dmp\.create\([^,]+?,\s*({.+?})\);',
|
||||||
|
webpage, 'player v5', default=None)
|
||||||
|
if player_v5:
|
||||||
|
player = self._parse_json(player_v5, video_id)
|
||||||
|
metadata = player['metadata']
|
||||||
|
formats = []
|
||||||
|
for quality, media_list in metadata['qualities'].items():
|
||||||
|
for media in media_list:
|
||||||
|
media_url = media.get('url')
|
||||||
|
if not media_url:
|
||||||
|
continue
|
||||||
|
type_ = media.get('type')
|
||||||
|
if type_ == 'application/vnd.lumberjack.manifest':
|
||||||
|
continue
|
||||||
|
if type_ == 'application/x-mpegURL' or determine_ext(media_url) == 'm3u8':
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
media_url, video_id, 'mp4', m3u8_id='hls'))
|
||||||
|
else:
|
||||||
|
f = {
|
||||||
|
'url': media_url,
|
||||||
|
'format_id': quality,
|
||||||
|
}
|
||||||
|
m = re.search(r'H264-(?P<width>\d+)x(?P<height>\d+)', media_url)
|
||||||
|
if m:
|
||||||
|
f.update({
|
||||||
|
'width': int(m.group('width')),
|
||||||
|
'height': int(m.group('height')),
|
||||||
|
})
|
||||||
|
formats.append(f)
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
title = metadata['title']
|
||||||
|
duration = int_or_none(metadata.get('duration'))
|
||||||
|
timestamp = int_or_none(metadata.get('created_time'))
|
||||||
|
thumbnail = metadata.get('poster_url')
|
||||||
|
uploader = metadata.get('owner', {}).get('screenname')
|
||||||
|
uploader_id = metadata.get('owner', {}).get('id')
|
||||||
|
|
||||||
|
subtitles = {}
|
||||||
|
for subtitle_lang, subtitle in metadata.get('subtitles', {}).get('data', {}).items():
|
||||||
|
subtitles[subtitle_lang] = [{
|
||||||
|
'ext': determine_ext(subtitle_url),
|
||||||
|
'url': subtitle_url,
|
||||||
|
} for subtitle_url in subtitle.get('urls', [])]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
|
'duration': duration,
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'uploader': uploader,
|
||||||
|
'uploader_id': uploader_id,
|
||||||
|
'age_limit': age_limit,
|
||||||
|
'view_count': view_count,
|
||||||
|
'comment_count': comment_count,
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
}
|
||||||
|
|
||||||
|
# vevo embed
|
||||||
|
vevo_id = self._search_regex(
|
||||||
|
r'<link rel="video_src" href="[^"]*?vevo.com[^"]*?video=(?P<id>[\w]*)',
|
||||||
|
webpage, 'vevo embed', default=None)
|
||||||
|
if vevo_id:
|
||||||
|
return self.url_result('vevo:%s' % vevo_id, 'Vevo')
|
||||||
|
|
||||||
|
# fallback old player
|
||||||
|
embed_page = self._download_webpage_no_ff(
|
||||||
|
'https://www.dailymotion.com/embed/video/%s' % video_id,
|
||||||
|
video_id, 'Downloading embed page')
|
||||||
|
|
||||||
|
timestamp = parse_iso8601(self._html_search_meta(
|
||||||
|
'video:release_date', webpage, 'upload date'))
|
||||||
|
|
||||||
|
info = self._parse_json(
|
||||||
|
self._search_regex(
|
||||||
|
r'var info = ({.*?}),$', embed_page,
|
||||||
|
'video info', flags=re.MULTILINE),
|
||||||
|
video_id)
|
||||||
|
|
||||||
embed_url = 'http://www.dailymotion.com/embed/video/%s' % video_id
|
|
||||||
embed_page = self._download_webpage(embed_url, video_id,
|
|
||||||
'Downloading embed page')
|
|
||||||
info = self._search_regex(r'var info = ({.*?}),$', embed_page,
|
|
||||||
'video info', flags=re.MULTILINE)
|
|
||||||
info = json.loads(info)
|
|
||||||
if info.get('error') is not None:
|
if info.get('error') is not None:
|
||||||
msg = 'Couldn\'t get video, Dailymotion says: %s' % info['error']['title']
|
msg = 'Couldn\'t get video, Dailymotion says: %s' % info['error']['title']
|
||||||
raise ExtractorError(msg, expected=True)
|
raise ExtractorError(msg, expected=True)
|
||||||
@@ -137,16 +221,11 @@ class DailymotionIE(DailymotionBaseInfoExtractor):
|
|||||||
'width': width,
|
'width': width,
|
||||||
'height': height,
|
'height': height,
|
||||||
})
|
})
|
||||||
if not formats:
|
self._sort_formats(formats)
|
||||||
raise ExtractorError('Unable to extract video URL')
|
|
||||||
|
|
||||||
# subtitles
|
# subtitles
|
||||||
video_subtitles = self.extract_subtitles(video_id, webpage)
|
video_subtitles = self.extract_subtitles(video_id, webpage)
|
||||||
|
|
||||||
view_count = str_to_int(self._search_regex(
|
|
||||||
r'video_views_count[^>]+>\s+([\d\.,]+)',
|
|
||||||
webpage, 'view count', fatal=False))
|
|
||||||
|
|
||||||
title = self._og_search_title(webpage, default=None)
|
title = self._og_search_title(webpage, default=None)
|
||||||
if title is None:
|
if title is None:
|
||||||
title = self._html_search_regex(
|
title = self._html_search_regex(
|
||||||
@@ -157,12 +236,14 @@ class DailymotionIE(DailymotionBaseInfoExtractor):
|
|||||||
'id': video_id,
|
'id': video_id,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'uploader': info['owner.screenname'],
|
'uploader': info['owner.screenname'],
|
||||||
'upload_date': video_upload_date,
|
'timestamp': timestamp,
|
||||||
'title': title,
|
'title': title,
|
||||||
|
'description': description,
|
||||||
'subtitles': video_subtitles,
|
'subtitles': video_subtitles,
|
||||||
'thumbnail': info['thumbnail_url'],
|
'thumbnail': info['thumbnail_url'],
|
||||||
'age_limit': age_limit,
|
'age_limit': age_limit,
|
||||||
'view_count': view_count,
|
'view_count': view_count,
|
||||||
|
'duration': info['duration']
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_subtitles(self, video_id, webpage):
|
def _get_subtitles(self, video_id, webpage):
|
||||||
@@ -196,18 +277,26 @@ class DailymotionPlaylistIE(DailymotionBaseInfoExtractor):
|
|||||||
}]
|
}]
|
||||||
|
|
||||||
def _extract_entries(self, id):
|
def _extract_entries(self, id):
|
||||||
video_ids = []
|
video_ids = set()
|
||||||
|
processed_urls = set()
|
||||||
for pagenum in itertools.count(1):
|
for pagenum in itertools.count(1):
|
||||||
request = self._build_request(self._PAGE_TEMPLATE % (id, pagenum))
|
page_url = self._PAGE_TEMPLATE % (id, pagenum)
|
||||||
webpage = self._download_webpage(request,
|
webpage, urlh = self._download_webpage_handle_no_ff(
|
||||||
id, 'Downloading page %s' % pagenum)
|
page_url, id, 'Downloading page %s' % pagenum)
|
||||||
|
if urlh.geturl() in processed_urls:
|
||||||
|
self.report_warning('Stopped at duplicated page %s, which is the same as %s' % (
|
||||||
|
page_url, urlh.geturl()), id)
|
||||||
|
break
|
||||||
|
|
||||||
video_ids.extend(re.findall(r'data-xid="(.+?)"', webpage))
|
processed_urls.add(urlh.geturl())
|
||||||
|
|
||||||
|
for video_id in re.findall(r'data-xid="(.+?)"', webpage):
|
||||||
|
if video_id not in video_ids:
|
||||||
|
yield self.url_result('http://www.dailymotion.com/video/%s' % video_id, 'Dailymotion')
|
||||||
|
video_ids.add(video_id)
|
||||||
|
|
||||||
if re.search(self._MORE_PAGES_INDICATOR, webpage) is None:
|
if re.search(self._MORE_PAGES_INDICATOR, webpage) is None:
|
||||||
break
|
break
|
||||||
return [self.url_result('http://www.dailymotion.com/video/%s' % video_id, 'Dailymotion')
|
|
||||||
for video_id in orderedSet(video_ids)]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
@@ -224,7 +313,7 @@ class DailymotionPlaylistIE(DailymotionBaseInfoExtractor):
|
|||||||
|
|
||||||
class DailymotionUserIE(DailymotionPlaylistIE):
|
class DailymotionUserIE(DailymotionPlaylistIE):
|
||||||
IE_NAME = 'dailymotion:user'
|
IE_NAME = 'dailymotion:user'
|
||||||
_VALID_URL = r'https?://(?:www\.)?dailymotion\.[a-z]{2,3}/user/(?P<user>[^/]+)'
|
_VALID_URL = r'https?://(?:www\.)?dailymotion\.[a-z]{2,3}/(?!(?:embed|#|video|playlist)/)(?:(?:old/)?user/)?(?P<user>[^/]+)'
|
||||||
_PAGE_TEMPLATE = 'http://www.dailymotion.com/user/%s/%s'
|
_PAGE_TEMPLATE = 'http://www.dailymotion.com/user/%s/%s'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.dailymotion.com/user/nqtv',
|
'url': 'https://www.dailymotion.com/user/nqtv',
|
||||||
@@ -233,12 +322,24 @@ class DailymotionUserIE(DailymotionPlaylistIE):
|
|||||||
'title': 'Rémi Gaillard',
|
'title': 'Rémi Gaillard',
|
||||||
},
|
},
|
||||||
'playlist_mincount': 100,
|
'playlist_mincount': 100,
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.dailymotion.com/user/UnderProject',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'UnderProject',
|
||||||
|
'title': 'UnderProject',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 1800,
|
||||||
|
'expected_warnings': [
|
||||||
|
'Stopped at duplicated page',
|
||||||
|
],
|
||||||
|
'skip': 'Takes too long time',
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
user = mobj.group('user')
|
user = mobj.group('user')
|
||||||
webpage = self._download_webpage(url, user)
|
webpage = self._download_webpage(
|
||||||
|
'https://www.dailymotion.com/user/%s' % user, user)
|
||||||
full_user = unescapeHTML(self._html_search_regex(
|
full_user = unescapeHTML(self._html_search_regex(
|
||||||
r'<a class="nav-image" title="([^"]+)" href="/%s">' % re.escape(user),
|
r'<a class="nav-image" title="([^"]+)" href="/%s">' % re.escape(user),
|
||||||
webpage, 'user'))
|
webpage, 'user'))
|
||||||
@@ -249,3 +350,52 @@ class DailymotionUserIE(DailymotionPlaylistIE):
|
|||||||
'title': full_user,
|
'title': full_user,
|
||||||
'entries': self._extract_entries(user),
|
'entries': self._extract_entries(user),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DailymotionCloudIE(DailymotionBaseInfoExtractor):
|
||||||
|
_VALID_URL_PREFIX = r'http://api\.dmcloud\.net/(?:player/)?embed/'
|
||||||
|
_VALID_URL = r'%s[^/]+/(?P<id>[^/?]+)' % _VALID_URL_PREFIX
|
||||||
|
_VALID_EMBED_URL = r'%s[^/]+/[^\'"]+' % _VALID_URL_PREFIX
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
# From http://www.francetvinfo.fr/economie/entreprises/les-entreprises-familiales-le-secret-de-la-reussite_933271.html
|
||||||
|
# Tested at FranceTvInfo_2
|
||||||
|
'url': 'http://api.dmcloud.net/embed/4e7343f894a6f677b10006b4/556e03339473995ee145930c?auth=1464865870-0-jyhsm84b-ead4c701fb750cf9367bf4447167a3db&autoplay=1',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# http://www.francetvinfo.fr/societe/larguez-les-amarres-le-cobaturage-se-developpe_980101.html
|
||||||
|
'url': 'http://api.dmcloud.net/player/embed/4e7343f894a6f677b10006b4/559545469473996d31429f06?auth=1467430263-0-90tglw2l-a3a4b64ed41efe48d7fccad85b8b8fda&autoplay=1',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _extract_dmcloud_url(self, webpage):
|
||||||
|
mobj = re.search(r'<iframe[^>]+src=[\'"](%s)[\'"]' % self._VALID_EMBED_URL, webpage)
|
||||||
|
if mobj:
|
||||||
|
return mobj.group(1)
|
||||||
|
|
||||||
|
mobj = re.search(
|
||||||
|
r'<input[^>]+id=[\'"]dmcloudUrlEmissionSelect[\'"][^>]+value=[\'"](%s)[\'"]' % self._VALID_EMBED_URL,
|
||||||
|
webpage)
|
||||||
|
if mobj:
|
||||||
|
return mobj.group(1)
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
webpage = self._download_webpage_no_ff(url, video_id)
|
||||||
|
|
||||||
|
title = self._html_search_regex(r'<title>([^>]+)</title>', webpage, 'title')
|
||||||
|
|
||||||
|
video_info = self._parse_json(self._search_regex(
|
||||||
|
r'var\s+info\s*=\s*([^;]+);', webpage, 'video info'), video_id)
|
||||||
|
|
||||||
|
# TODO: parse ios_url, which is in fact a manifest
|
||||||
|
video_url = video_info['mp4_url']
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'url': video_url,
|
||||||
|
'title': title,
|
||||||
|
'thumbnail': video_info.get('thumbnail_url'),
|
||||||
|
}
|
||||||
|
|||||||
84
youtube_dl/extractor/dcn.py
Normal file
84
youtube_dl/extractor/dcn.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..compat import (
|
||||||
|
compat_urllib_parse,
|
||||||
|
compat_urllib_request,
|
||||||
|
)
|
||||||
|
from ..utils import (
|
||||||
|
int_or_none,
|
||||||
|
parse_iso8601,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DCNIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?dcndigital\.ae/(?:#/)?(?:video/.+|show/\d+/.+?)/(?P<id>\d+)'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'http://www.dcndigital.ae/#/show/199074/%D8%B1%D8%AD%D9%84%D8%A9-%D8%A7%D9%84%D8%B9%D9%85%D8%B1-%D8%A7%D9%84%D8%AD%D9%84%D9%82%D8%A9-1/17375/6887',
|
||||||
|
'info_dict':
|
||||||
|
{
|
||||||
|
'id': '17375',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'رحلة العمر : الحلقة 1',
|
||||||
|
'description': 'md5:0156e935d870acb8ef0a66d24070c6d6',
|
||||||
|
'thumbnail': 're:^https?://.*\.jpg$',
|
||||||
|
'duration': 2041,
|
||||||
|
'timestamp': 1227504126,
|
||||||
|
'upload_date': '20081124',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# m3u8 download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
request = compat_urllib_request.Request(
|
||||||
|
'http://admin.mangomolo.com/analytics/index.php/plus/video?id=%s' % video_id,
|
||||||
|
headers={'Origin': 'http://www.dcndigital.ae'})
|
||||||
|
|
||||||
|
video = self._download_json(request, video_id)
|
||||||
|
title = video.get('title_en') or video['title_ar']
|
||||||
|
|
||||||
|
webpage = self._download_webpage(
|
||||||
|
'http://admin.mangomolo.com/analytics/index.php/customers/embed/video?' +
|
||||||
|
compat_urllib_parse.urlencode({
|
||||||
|
'id': video['id'],
|
||||||
|
'user_id': video['user_id'],
|
||||||
|
'signature': video['signature'],
|
||||||
|
'countries': 'Q0M=',
|
||||||
|
'filter': 'DENY',
|
||||||
|
}), video_id)
|
||||||
|
|
||||||
|
m3u8_url = self._html_search_regex(r'file:\s*"([^"]+)', webpage, 'm3u8 url')
|
||||||
|
formats = self._extract_m3u8_formats(
|
||||||
|
m3u8_url, video_id, 'mp4', entry_protocol='m3u8_native', m3u8_id='hls')
|
||||||
|
|
||||||
|
rtsp_url = self._search_regex(
|
||||||
|
r'<a[^>]+href="(rtsp://[^"]+)"', webpage, 'rtsp url', fatal=False)
|
||||||
|
if rtsp_url:
|
||||||
|
formats.append({
|
||||||
|
'url': rtsp_url,
|
||||||
|
'format_id': 'rtsp',
|
||||||
|
})
|
||||||
|
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
img = video.get('img')
|
||||||
|
thumbnail = 'http://admin.mangomolo.com/analytics/%s' % img if img else None
|
||||||
|
duration = int_or_none(video.get('duration'))
|
||||||
|
description = video.get('description_en') or video.get('description_ar')
|
||||||
|
timestamp = parse_iso8601(video.get('create_time') or video.get('update_time'), ' ')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
|
'duration': duration,
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'formats': formats,
|
||||||
|
}
|
||||||
@@ -3,42 +3,47 @@ from __future__ import unicode_literals
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
from ..utils import unified_strdate
|
||||||
|
|
||||||
|
|
||||||
class DFBIE(InfoExtractor):
|
class DFBIE(InfoExtractor):
|
||||||
IE_NAME = 'tv.dfb.de'
|
IE_NAME = 'tv.dfb.de'
|
||||||
_VALID_URL = r'https?://tv\.dfb\.de/video/[^/]+/(?P<id>\d+)'
|
_VALID_URL = r'https?://tv\.dfb\.de/video/(?P<display_id>[^/]+)/(?P<id>\d+)'
|
||||||
|
|
||||||
_TEST = {
|
_TEST = {
|
||||||
'url': 'http://tv.dfb.de/video/highlights-des-empfangs-in-berlin/9070/',
|
'url': 'http://tv.dfb.de/video/u-19-em-stimmen-zum-spiel-gegen-russland/11633/',
|
||||||
# The md5 is different each time
|
# The md5 is different each time
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '9070',
|
'id': '11633',
|
||||||
|
'display_id': 'u-19-em-stimmen-zum-spiel-gegen-russland',
|
||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
'title': 'Highlights des Empfangs in Berlin',
|
'title': 'U 19-EM: Stimmen zum Spiel gegen Russland',
|
||||||
'upload_date': '20140716',
|
'upload_date': '20150714',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
video_id = mobj.group('id')
|
video_id = mobj.group('id')
|
||||||
|
display_id = mobj.group('display_id')
|
||||||
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, display_id)
|
||||||
player_info = self._download_xml(
|
player_info = self._download_xml(
|
||||||
'http://tv.dfb.de/server/hd_video.php?play=%s' % video_id,
|
'http://tv.dfb.de/server/hd_video.php?play=%s' % video_id,
|
||||||
video_id)
|
display_id)
|
||||||
video_info = player_info.find('video')
|
video_info = player_info.find('video')
|
||||||
|
|
||||||
f4m_info = self._download_xml(self._proto_relative_url(video_info.find('url').text.strip()), video_id)
|
f4m_info = self._download_xml(
|
||||||
|
self._proto_relative_url(video_info.find('url').text.strip()), display_id)
|
||||||
token_el = f4m_info.find('token')
|
token_el = f4m_info.find('token')
|
||||||
manifest_url = token_el.attrib['url'] + '?' + 'hdnea=' + token_el.attrib['auth'] + '&hdcore=3.2.0'
|
manifest_url = token_el.attrib['url'] + '?' + 'hdnea=' + token_el.attrib['auth'] + '&hdcore=3.2.0'
|
||||||
|
formats = self._extract_f4m_formats(manifest_url, display_id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
|
'display_id': display_id,
|
||||||
'title': video_info.find('title').text,
|
'title': video_info.find('title').text,
|
||||||
'url': manifest_url,
|
|
||||||
'ext': 'flv',
|
|
||||||
'thumbnail': self._og_search_thumbnail(webpage),
|
'thumbnail': self._og_search_thumbnail(webpage),
|
||||||
'upload_date': ''.join(video_info.find('time_date').text.split('.')[::-1]),
|
'upload_date': unified_strdate(video_info.find('time_date').text),
|
||||||
|
'formats': formats,
|
||||||
}
|
}
|
||||||
|
|||||||
59
youtube_dl/extractor/dhm.py
Normal file
59
youtube_dl/extractor/dhm.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import parse_duration
|
||||||
|
|
||||||
|
|
||||||
|
class DHMIE(InfoExtractor):
|
||||||
|
IE_DESC = 'Filmarchiv - Deutsches Historisches Museum'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?dhm\.de/filmarchiv/(?:[^/]+/)+(?P<id>[^/]+)'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://www.dhm.de/filmarchiv/die-filme/the-marshallplan-at-work-in-west-germany/',
|
||||||
|
'md5': '11c475f670209bf6acca0b2b7ef51827',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'the-marshallplan-at-work-in-west-germany',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': 'MARSHALL PLAN AT WORK IN WESTERN GERMANY, THE',
|
||||||
|
'description': 'md5:1fabd480c153f97b07add61c44407c82',
|
||||||
|
'duration': 660,
|
||||||
|
'thumbnail': 're:^https?://.*\.jpg$',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.dhm.de/filmarchiv/02-mapping-the-wall/peter-g/rolle-1/',
|
||||||
|
'md5': '09890226332476a3e3f6f2cb74734aa5',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'rolle-1',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': 'ROLLE 1',
|
||||||
|
'thumbnail': 're:^https?://.*\.jpg$',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
playlist_id = self._match_id(url)
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, playlist_id)
|
||||||
|
|
||||||
|
playlist_url = self._search_regex(
|
||||||
|
r"file\s*:\s*'([^']+)'", webpage, 'playlist url')
|
||||||
|
|
||||||
|
entries = self._extract_xspf_playlist(playlist_url, playlist_id)
|
||||||
|
|
||||||
|
title = self._search_regex(
|
||||||
|
[r'dc:title="([^"]+)"', r'<title> »([^<]+)</title>'],
|
||||||
|
webpage, 'title').strip()
|
||||||
|
description = self._html_search_regex(
|
||||||
|
r'<p><strong>Description:</strong>(.+?)</p>',
|
||||||
|
webpage, 'description', default=None)
|
||||||
|
duration = parse_duration(self._search_regex(
|
||||||
|
r'<em>Length\s*</em>\s*:\s*</strong>([^<]+)',
|
||||||
|
webpage, 'duration', default=None))
|
||||||
|
|
||||||
|
entries[0].update({
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'duration': duration,
|
||||||
|
})
|
||||||
|
|
||||||
|
return self.playlist_result(entries, playlist_id)
|
||||||
@@ -2,19 +2,19 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
parse_duration,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
int_or_none,
|
|
||||||
)
|
)
|
||||||
|
from ..compat import compat_str
|
||||||
|
|
||||||
|
|
||||||
class DiscoveryIE(InfoExtractor):
|
class DiscoveryIE(InfoExtractor):
|
||||||
_VALID_URL = r'http://www\.discovery\.com\/[a-zA-Z0-9\-]*/[a-zA-Z0-9\-]*/videos/(?P<id>[a-zA-Z0-9_\-]*)(?:\.htm)?'
|
_VALID_URL = r'http://www\.discovery\.com\/[a-zA-Z0-9\-]*/[a-zA-Z0-9\-]*/videos/(?P<id>[a-zA-Z0-9_\-]*)(?:\.htm)?'
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'http://www.discovery.com/tv-shows/mythbusters/videos/mission-impossible-outtakes.htm',
|
'url': 'http://www.discovery.com/tv-shows/mythbusters/videos/mission-impossible-outtakes.htm',
|
||||||
'md5': '3c69d77d9b0d82bfd5e5932a60f26504',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'mission-impossible-outtakes',
|
'id': '20769',
|
||||||
'ext': 'flv',
|
'ext': 'mp4',
|
||||||
'title': 'Mission Impossible Outtakes',
|
'title': 'Mission Impossible Outtakes',
|
||||||
'description': ('Watch Jamie Hyneman and Adam Savage practice being'
|
'description': ('Watch Jamie Hyneman and Adam Savage practice being'
|
||||||
' each other -- to the point of confusing Jamie\'s dog -- and '
|
' each other -- to the point of confusing Jamie\'s dog -- and '
|
||||||
@@ -24,22 +24,36 @@ class DiscoveryIE(InfoExtractor):
|
|||||||
'timestamp': 1303099200,
|
'timestamp': 1303099200,
|
||||||
'upload_date': '20110418',
|
'upload_date': '20110418',
|
||||||
},
|
},
|
||||||
}
|
'params': {
|
||||||
|
'skip_download': True, # requires ffmpeg
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.discovery.com/tv-shows/mythbusters/videos/mythbusters-the-simpsons',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'mythbusters-the-simpsons',
|
||||||
|
'title': 'MythBusters: The Simpsons',
|
||||||
|
},
|
||||||
|
'playlist_count': 9,
|
||||||
|
}]
|
||||||
|
|
||||||
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)
|
info = self._download_json(url + '?flat=1', video_id)
|
||||||
|
|
||||||
info = self._parse_json(self._search_regex(
|
video_title = info.get('playlist_title') or info.get('video_title')
|
||||||
r'(?s)<script type="application/ld\+json">(.*?)</script>',
|
|
||||||
webpage, 'video info'), video_id)
|
|
||||||
|
|
||||||
return {
|
entries = [{
|
||||||
'id': video_id,
|
'id': compat_str(video_info['id']),
|
||||||
'title': info['name'],
|
'formats': self._extract_m3u8_formats(
|
||||||
'url': info['contentURL'],
|
video_info['src'], video_id, ext='mp4',
|
||||||
'description': info.get('description'),
|
note='Download m3u8 information for video %d' % (idx + 1)),
|
||||||
'thumbnail': info.get('thumbnailUrl'),
|
'title': video_info['title'],
|
||||||
'timestamp': parse_iso8601(info.get('uploadDate')),
|
'description': video_info.get('description'),
|
||||||
'duration': int_or_none(info.get('duration')),
|
'duration': parse_duration(video_info.get('video_length')),
|
||||||
}
|
'webpage_url': video_info.get('href'),
|
||||||
|
'thumbnail': video_info.get('thumbnailURL'),
|
||||||
|
'alt_title': video_info.get('secondary_title'),
|
||||||
|
'timestamp': parse_iso8601(video_info.get('publishedDate')),
|
||||||
|
} for idx, video_info in enumerate(info['playlist'])]
|
||||||
|
|
||||||
|
return self.playlist_result(entries, video_id, video_title)
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ class DotsubIE(InfoExtractor):
|
|||||||
if not video_url:
|
if not video_url:
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
video_url = self._search_regex(
|
video_url = self._search_regex(
|
||||||
r'"file"\s*:\s*\'([^\']+)', webpage, 'video url')
|
[r'<source[^>]+src="([^"]+)"', r'"file"\s*:\s*\'([^\']+)'],
|
||||||
|
webpage, 'video url')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import time
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import ExtractorError
|
from ..utils import (ExtractorError, unescapeHTML)
|
||||||
|
from ..compat import (compat_str, compat_basestring)
|
||||||
|
|
||||||
|
|
||||||
class DouyuTVIE(InfoExtractor):
|
class DouyuTVIE(InfoExtractor):
|
||||||
|
IE_DESC = '斗鱼'
|
||||||
_VALID_URL = r'http://(?:www\.)?douyutv\.com/(?P<id>[A-Za-z0-9]+)'
|
_VALID_URL = r'http://(?:www\.)?douyutv\.com/(?P<id>[A-Za-z0-9]+)'
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'http://www.douyutv.com/iseven',
|
'url': 'http://www.douyutv.com/iseven',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'iseven',
|
'id': '17732',
|
||||||
|
'display_id': 'iseven',
|
||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
'title': 're:^清晨醒脑!T-ara根本停不下来! [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
|
'title': 're:^清晨醒脑!T-ara根本停不下来! [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
|
||||||
'description': 'md5:9e525642c25a0a24302869937cf69d17',
|
'description': 'md5:c93d6692dde6fe33809a46edcbecca44',
|
||||||
'thumbnail': 're:^https?://.*\.jpg$',
|
'thumbnail': 're:^https?://.*\.jpg$',
|
||||||
'uploader': '7师傅',
|
'uploader': '7师傅',
|
||||||
'uploader_id': '431925',
|
'uploader_id': '431925',
|
||||||
@@ -22,22 +27,52 @@ class DouyuTVIE(InfoExtractor):
|
|||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
}
|
}
|
||||||
}
|
}, {
|
||||||
|
'url': 'http://www.douyutv.com/85982',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '85982',
|
||||||
|
'display_id': '85982',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': 're:^小漠从零单排记!——CSOL2躲猫猫 [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
|
||||||
|
'description': 'md5:746a2f7a253966a06755a912f0acc0d2',
|
||||||
|
'thumbnail': 're:^https?://.*\.jpg$',
|
||||||
|
'uploader': 'douyu小漠',
|
||||||
|
'uploader_id': '3769985',
|
||||||
|
'is_live': True,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
if video_id.isdigit():
|
||||||
|
room_id = video_id
|
||||||
|
else:
|
||||||
|
page = self._download_webpage(url, video_id)
|
||||||
|
room_id = self._html_search_regex(
|
||||||
|
r'"room_id"\s*:\s*(\d+),', page, 'room id')
|
||||||
|
|
||||||
|
prefix = 'room/%s?aid=android&client_sys=android&time=%d' % (
|
||||||
|
room_id, int(time.time()))
|
||||||
|
|
||||||
|
auth = hashlib.md5((prefix + '1231').encode('ascii')).hexdigest()
|
||||||
config = self._download_json(
|
config = self._download_json(
|
||||||
'http://www.douyutv.com/api/client/room/%s' % video_id, video_id)
|
'http://www.douyutv.com/api/v1/%s&auth=%s' % (prefix, auth),
|
||||||
|
video_id)
|
||||||
|
|
||||||
data = config['data']
|
data = config['data']
|
||||||
|
|
||||||
error_code = config.get('error', 0)
|
error_code = config.get('error', 0)
|
||||||
show_status = data.get('show_status')
|
|
||||||
if error_code is not 0:
|
if error_code is not 0:
|
||||||
raise ExtractorError(
|
error_desc = 'Server reported error %i' % error_code
|
||||||
'Server reported error %i' % error_code, expected=True)
|
if isinstance(data, (compat_str, compat_basestring)):
|
||||||
|
error_desc += ': ' + data
|
||||||
|
raise ExtractorError(error_desc, expected=True)
|
||||||
|
|
||||||
|
show_status = data.get('show_status')
|
||||||
# 1 = live, 2 = offline
|
# 1 = live, 2 = offline
|
||||||
if show_status == '2':
|
if show_status == '2':
|
||||||
raise ExtractorError(
|
raise ExtractorError(
|
||||||
@@ -46,7 +81,7 @@ class DouyuTVIE(InfoExtractor):
|
|||||||
base_url = data['rtmp_url']
|
base_url = data['rtmp_url']
|
||||||
live_path = data['rtmp_live']
|
live_path = data['rtmp_live']
|
||||||
|
|
||||||
title = self._live_title(data['room_name'])
|
title = self._live_title(unescapeHTML(data['room_name']))
|
||||||
description = data.get('show_details')
|
description = data.get('show_details')
|
||||||
thumbnail = data.get('room_src')
|
thumbnail = data.get('room_src')
|
||||||
|
|
||||||
@@ -66,7 +101,8 @@ class DouyuTVIE(InfoExtractor):
|
|||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': room_id,
|
||||||
|
'display_id': video_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': description,
|
'description': description,
|
||||||
'thumbnail': thumbnail,
|
'thumbnail': thumbnail,
|
||||||
|
|||||||
216
youtube_dl/extractor/dramafever.py
Normal file
216
youtube_dl/extractor/dramafever.py
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
# encoding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..compat import (
|
||||||
|
compat_HTTPError,
|
||||||
|
compat_urllib_parse,
|
||||||
|
compat_urllib_request,
|
||||||
|
compat_urlparse,
|
||||||
|
)
|
||||||
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
clean_html,
|
||||||
|
determine_ext,
|
||||||
|
int_or_none,
|
||||||
|
parse_iso8601,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DramaFeverBaseIE(InfoExtractor):
|
||||||
|
_LOGIN_URL = 'https://www.dramafever.com/accounts/login/'
|
||||||
|
_NETRC_MACHINE = 'dramafever'
|
||||||
|
|
||||||
|
_CONSUMER_SECRET = 'DA59dtVXYLxajktV'
|
||||||
|
|
||||||
|
_consumer_secret = None
|
||||||
|
|
||||||
|
def _get_consumer_secret(self):
|
||||||
|
mainjs = self._download_webpage(
|
||||||
|
'http://www.dramafever.com/static/51afe95/df2014/scripts/main.js',
|
||||||
|
None, 'Downloading main.js', fatal=False)
|
||||||
|
if not mainjs:
|
||||||
|
return self._CONSUMER_SECRET
|
||||||
|
return self._search_regex(
|
||||||
|
r"var\s+cs\s*=\s*'([^']+)'", mainjs,
|
||||||
|
'consumer secret', default=self._CONSUMER_SECRET)
|
||||||
|
|
||||||
|
def _real_initialize(self):
|
||||||
|
self._login()
|
||||||
|
self._consumer_secret = self._get_consumer_secret()
|
||||||
|
|
||||||
|
def _login(self):
|
||||||
|
(username, password) = self._get_login_info()
|
||||||
|
if username is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
login_form = {
|
||||||
|
'username': username,
|
||||||
|
'password': password,
|
||||||
|
}
|
||||||
|
|
||||||
|
request = compat_urllib_request.Request(
|
||||||
|
self._LOGIN_URL, compat_urllib_parse.urlencode(login_form).encode('utf-8'))
|
||||||
|
response = self._download_webpage(
|
||||||
|
request, None, 'Logging in as %s' % username)
|
||||||
|
|
||||||
|
if all(logout_pattern not in response
|
||||||
|
for logout_pattern in ['href="/accounts/logout/"', '>Log out<']):
|
||||||
|
error = self._html_search_regex(
|
||||||
|
r'(?s)class="hidden-xs prompt"[^>]*>(.+?)<',
|
||||||
|
response, 'error message', default=None)
|
||||||
|
if error:
|
||||||
|
raise ExtractorError('Unable to login: %s' % error, expected=True)
|
||||||
|
raise ExtractorError('Unable to log in')
|
||||||
|
|
||||||
|
|
||||||
|
class DramaFeverIE(DramaFeverBaseIE):
|
||||||
|
IE_NAME = 'dramafever'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?dramafever\.com/drama/(?P<id>[0-9]+/[0-9]+)(?:/|$)'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'http://www.dramafever.com/drama/4512/1/Cooking_with_Shin/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '4512.1',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': 'Cooking with Shin 4512.1',
|
||||||
|
'description': 'md5:a8eec7942e1664a6896fcd5e1287bfd0',
|
||||||
|
'thumbnail': 're:^https?://.*\.jpg',
|
||||||
|
'timestamp': 1404336058,
|
||||||
|
'upload_date': '20140702',
|
||||||
|
'duration': 343,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url).replace('/', '.')
|
||||||
|
|
||||||
|
try:
|
||||||
|
feed = self._download_json(
|
||||||
|
'http://www.dramafever.com/amp/episode/feed.json?guid=%s' % video_id,
|
||||||
|
video_id, 'Downloading episode JSON')['channel']['item']
|
||||||
|
except ExtractorError as e:
|
||||||
|
if isinstance(e.cause, compat_HTTPError):
|
||||||
|
raise ExtractorError(
|
||||||
|
'Currently unavailable in your country.', expected=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
media_group = feed.get('media-group', {})
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
for media_content in media_group['media-content']:
|
||||||
|
src = media_content.get('@attributes', {}).get('url')
|
||||||
|
if not src:
|
||||||
|
continue
|
||||||
|
ext = determine_ext(src)
|
||||||
|
if ext == 'f4m':
|
||||||
|
formats.extend(self._extract_f4m_formats(
|
||||||
|
src, video_id, f4m_id='hds'))
|
||||||
|
elif ext == 'm3u8':
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
src, video_id, 'mp4', m3u8_id='hls'))
|
||||||
|
else:
|
||||||
|
formats.append({
|
||||||
|
'url': src,
|
||||||
|
})
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
title = media_group.get('media-title')
|
||||||
|
description = media_group.get('media-description')
|
||||||
|
duration = int_or_none(media_group['media-content'][0].get('@attributes', {}).get('duration'))
|
||||||
|
thumbnail = self._proto_relative_url(
|
||||||
|
media_group.get('media-thumbnail', {}).get('@attributes', {}).get('url'))
|
||||||
|
timestamp = parse_iso8601(feed.get('pubDate'), ' ')
|
||||||
|
|
||||||
|
subtitles = {}
|
||||||
|
for media_subtitle in media_group.get('media-subTitle', []):
|
||||||
|
lang = media_subtitle.get('@attributes', {}).get('lang')
|
||||||
|
href = media_subtitle.get('@attributes', {}).get('href')
|
||||||
|
if not lang or not href:
|
||||||
|
continue
|
||||||
|
subtitles[lang] = [{
|
||||||
|
'ext': 'ttml',
|
||||||
|
'url': href,
|
||||||
|
}]
|
||||||
|
|
||||||
|
series_id, episode_number = video_id.split('.')
|
||||||
|
episode_info = self._download_json(
|
||||||
|
# We only need a single episode info, so restricting page size to one episode
|
||||||
|
# and dealing with page number as with episode number
|
||||||
|
r'http://www.dramafever.com/api/4/episode/series/?cs=%s&series_id=%s&page_number=%s&page_size=1'
|
||||||
|
% (self._consumer_secret, series_id, episode_number),
|
||||||
|
video_id, 'Downloading episode info JSON', fatal=False)
|
||||||
|
if episode_info:
|
||||||
|
value = episode_info.get('value')
|
||||||
|
if value:
|
||||||
|
subfile = value[0].get('subfile') or value[0].get('new_subfile')
|
||||||
|
if subfile and subfile != 'http://www.dramafever.com/st/':
|
||||||
|
subtitles.setdefault('English', []).append({
|
||||||
|
'ext': 'srt',
|
||||||
|
'url': subfile,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'duration': duration,
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DramaFeverSeriesIE(DramaFeverBaseIE):
|
||||||
|
IE_NAME = 'dramafever:series'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?dramafever\.com/drama/(?P<id>[0-9]+)(?:/(?:(?!\d+(?:/|$)).+)?)?$'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://www.dramafever.com/drama/4512/Cooking_with_Shin/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '4512',
|
||||||
|
'title': 'Cooking with Shin',
|
||||||
|
'description': 'md5:84a3f26e3cdc3fb7f500211b3593b5c1',
|
||||||
|
},
|
||||||
|
'playlist_count': 4,
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.dramafever.com/drama/124/IRIS/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '124',
|
||||||
|
'title': 'IRIS',
|
||||||
|
'description': 'md5:b3a30e587cf20c59bd1c01ec0ee1b862',
|
||||||
|
},
|
||||||
|
'playlist_count': 20,
|
||||||
|
}]
|
||||||
|
|
||||||
|
_PAGE_SIZE = 60 # max is 60 (see http://api.drama9.com/#get--api-4-episode-series-)
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
series_id = self._match_id(url)
|
||||||
|
|
||||||
|
series = self._download_json(
|
||||||
|
'http://www.dramafever.com/api/4/series/query/?cs=%s&series_id=%s'
|
||||||
|
% (self._consumer_secret, series_id),
|
||||||
|
series_id, 'Downloading series JSON')['series'][series_id]
|
||||||
|
|
||||||
|
title = clean_html(series['name'])
|
||||||
|
description = clean_html(series.get('description') or series.get('description_short'))
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
for page_num in itertools.count(1):
|
||||||
|
episodes = self._download_json(
|
||||||
|
'http://www.dramafever.com/api/4/episode/series/?cs=%s&series_id=%s&page_size=%d&page_number=%d'
|
||||||
|
% (self._consumer_secret, series_id, self._PAGE_SIZE, page_num),
|
||||||
|
series_id, 'Downloading episodes JSON page #%d' % page_num)
|
||||||
|
for episode in episodes.get('value', []):
|
||||||
|
episode_url = episode.get('episode_url')
|
||||||
|
if not episode_url:
|
||||||
|
continue
|
||||||
|
entries.append(self.url_result(
|
||||||
|
compat_urlparse.urljoin(url, episode_url),
|
||||||
|
'DramaFever', episode.get('guid')))
|
||||||
|
if page_num == episodes['num_pages']:
|
||||||
|
break
|
||||||
|
|
||||||
|
return self.playlist_result(entries, series_id, title, description)
|
||||||
@@ -15,7 +15,6 @@ class DRBonanzaIE(InfoExtractor):
|
|||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.dr.dk/bonanza/serie/portraetter/Talkshowet.htm?assetId=65517',
|
'url': 'http://www.dr.dk/bonanza/serie/portraetter/Talkshowet.htm?assetId=65517',
|
||||||
'md5': 'fe330252ddea607635cf2eb2c99a0af3',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '65517',
|
'id': '65517',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
@@ -26,6 +25,9 @@ class DRBonanzaIE(InfoExtractor):
|
|||||||
'upload_date': '20110120',
|
'upload_date': '20110120',
|
||||||
'duration': 3664,
|
'duration': 3664,
|
||||||
},
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True, # requires rtmp
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.dr.dk/bonanza/radio/serie/sport/fodbold.htm?assetId=59410',
|
'url': 'http://www.dr.dk/bonanza/radio/serie/sport/fodbold.htm?assetId=59410',
|
||||||
'md5': '6dfe039417e76795fb783c52da3de11d',
|
'md5': '6dfe039417e76795fb783c52da3de11d',
|
||||||
@@ -93,6 +95,11 @@ class DRBonanzaIE(InfoExtractor):
|
|||||||
'format_id': file['Type'].replace('Video', ''),
|
'format_id': file['Type'].replace('Video', ''),
|
||||||
'preference': preferencemap.get(file['Type'], -10),
|
'preference': preferencemap.get(file['Type'], -10),
|
||||||
})
|
})
|
||||||
|
if format['url'].startswith('rtmp'):
|
||||||
|
rtmp_url = format['url']
|
||||||
|
format['rtmp_live'] = True # --resume does not work
|
||||||
|
if '/bonanza/' in rtmp_url:
|
||||||
|
format['play_path'] = rtmp_url.split('/bonanza/')[1]
|
||||||
formats.append(format)
|
formats.append(format)
|
||||||
elif file['Type'] == "Thumb":
|
elif file['Type'] == "Thumb":
|
||||||
thumbnail = file['Location']
|
thumbnail = file['Location']
|
||||||
@@ -111,9 +118,6 @@ class DRBonanzaIE(InfoExtractor):
|
|||||||
description = '%s\n%s\n%s\n' % (
|
description = '%s\n%s\n%s\n' % (
|
||||||
info['Description'], info['Actors'], info['Colophon'])
|
info['Description'], info['Actors'], info['Colophon'])
|
||||||
|
|
||||||
for f in formats:
|
|
||||||
f['url'] = f['url'].replace('rtmp://vod-bonanza.gss.dr.dk/bonanza/', 'http://vodfiles.dr.dk/')
|
|
||||||
f['url'] = f['url'].replace('mp4:bonanza', 'bonanza')
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
display_id = re.sub(r'[^\w\d-]', '', re.sub(r' ', '-', title.lower())) + '-' + asset_id
|
display_id = re.sub(r'[^\w\d-]', '', re.sub(r' ', '-', title.lower())) + '-' + asset_id
|
||||||
|
|||||||
@@ -3,24 +3,33 @@ from __future__ import unicode_literals
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import unified_strdate
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
unified_strdate,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DreiSatIE(InfoExtractor):
|
class DreiSatIE(InfoExtractor):
|
||||||
IE_NAME = '3sat'
|
IE_NAME = '3sat'
|
||||||
_VALID_URL = r'(?:http://)?(?:www\.)?3sat\.de/mediathek/(?:index\.php)?\?(?:(?:mode|display)=[^&]+&)*obj=(?P<id>[0-9]+)$'
|
_VALID_URL = r'(?:http://)?(?:www\.)?3sat\.de/mediathek/(?:index\.php|mediathek\.php)?\?(?:(?:mode|display)=[^&]+&)*obj=(?P<id>[0-9]+)$'
|
||||||
_TEST = {
|
_TESTS = [
|
||||||
'url': 'http://www.3sat.de/mediathek/index.php?obj=36983',
|
{
|
||||||
'md5': '9dcfe344732808dbfcc901537973c922',
|
'url': 'http://www.3sat.de/mediathek/index.php?mode=play&obj=45918',
|
||||||
'info_dict': {
|
'md5': 'be37228896d30a88f315b638900a026e',
|
||||||
'id': '36983',
|
'info_dict': {
|
||||||
'ext': 'mp4',
|
'id': '45918',
|
||||||
'title': 'Kaffeeland Schweiz',
|
'ext': 'mp4',
|
||||||
'description': 'md5:cc4424b18b75ae9948b13929a0814033',
|
'title': 'Waidmannsheil',
|
||||||
'uploader': '3sat',
|
'description': 'md5:cce00ca1d70e21425e72c86a98a56817',
|
||||||
'upload_date': '20130622'
|
'uploader': '3sat',
|
||||||
}
|
'upload_date': '20140913'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'http://www.3sat.de/mediathek/mediathek.php?mode=play&obj=51066',
|
||||||
|
'only_matching': True,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
@@ -28,6 +37,15 @@ class DreiSatIE(InfoExtractor):
|
|||||||
details_url = 'http://www.3sat.de/mediathek/xmlservice/web/beitragsDetails?ak=web&id=%s' % video_id
|
details_url = 'http://www.3sat.de/mediathek/xmlservice/web/beitragsDetails?ak=web&id=%s' % video_id
|
||||||
details_doc = self._download_xml(details_url, video_id, 'Downloading video details')
|
details_doc = self._download_xml(details_url, video_id, 'Downloading video details')
|
||||||
|
|
||||||
|
status_code = details_doc.find('./status/statuscode')
|
||||||
|
if status_code is not None and status_code.text != 'ok':
|
||||||
|
code = status_code.text
|
||||||
|
if code == 'notVisibleAnymore':
|
||||||
|
message = 'Video %s is not available' % video_id
|
||||||
|
else:
|
||||||
|
message = '%s returned error: %s' % (self.IE_NAME, code)
|
||||||
|
raise ExtractorError(message, expected=True)
|
||||||
|
|
||||||
thumbnail_els = details_doc.findall('.//teaserimage')
|
thumbnail_els = details_doc.findall('.//teaserimage')
|
||||||
thumbnails = [{
|
thumbnails = [{
|
||||||
'width': int(te.attrib['key'].partition('x')[0]),
|
'width': int(te.attrib['key'].partition('x')[0]),
|
||||||
|
|||||||
@@ -36,25 +36,24 @@ class DrTuberIE(InfoExtractor):
|
|||||||
r'<source src="([^"]+)"', webpage, 'video URL')
|
r'<source src="([^"]+)"', webpage, 'video URL')
|
||||||
|
|
||||||
title = self._html_search_regex(
|
title = self._html_search_regex(
|
||||||
[r'class="hd_title" style="[^"]+">([^<]+)</h1>', r'<title>([^<]+) - \d+'],
|
[r'<p[^>]+class="title_substrate">([^<]+)</p>', r'<title>([^<]+) - \d+'],
|
||||||
webpage, 'title')
|
webpage, 'title')
|
||||||
|
|
||||||
thumbnail = self._html_search_regex(
|
thumbnail = self._html_search_regex(
|
||||||
r'poster="([^"]+)"',
|
r'poster="([^"]+)"',
|
||||||
webpage, 'thumbnail', fatal=False)
|
webpage, 'thumbnail', fatal=False)
|
||||||
|
|
||||||
like_count = str_to_int(self._html_search_regex(
|
def extract_count(id_, name):
|
||||||
r'<span id="rate_likes">\s*<img[^>]+>\s*<span>([\d,\.]+)</span>',
|
return str_to_int(self._html_search_regex(
|
||||||
webpage, 'like count', fatal=False))
|
r'<span[^>]+(?:class|id)="%s"[^>]*>([\d,\.]+)</span>' % id_,
|
||||||
dislike_count = str_to_int(self._html_search_regex(
|
webpage, '%s count' % name, fatal=False))
|
||||||
r'<span id="rate_dislikes">\s*<img[^>]+>\s*<span>([\d,\.]+)</span>',
|
|
||||||
webpage, 'like count', fatal=False))
|
like_count = extract_count('rate_likes', 'like')
|
||||||
comment_count = str_to_int(self._html_search_regex(
|
dislike_count = extract_count('rate_dislikes', 'dislike')
|
||||||
r'<span class="comments_count">([\d,\.]+)</span>',
|
comment_count = extract_count('comments_count', 'comment')
|
||||||
webpage, 'comment count', fatal=False))
|
|
||||||
|
|
||||||
cats_str = self._search_regex(
|
cats_str = self._search_regex(
|
||||||
r'<span>Categories:</span><div>(.+?)</div>', webpage, 'categories', fatal=False)
|
r'<div[^>]+class="categories_list">(.+?)</div>', webpage, 'categories', fatal=False)
|
||||||
categories = [] if not cats_str else re.findall(r'<a title="([^"]+)"', cats_str)
|
categories = [] if not cats_str else re.findall(r'<a title="([^"]+)"', cats_str)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,23 +1,27 @@
|
|||||||
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from .common import InfoExtractor, ExtractorError
|
from .common import InfoExtractor
|
||||||
from ..utils import parse_iso8601
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
parse_iso8601,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DRTVIE(InfoExtractor):
|
class DRTVIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?dr\.dk/tv/se/(?:[^/]+/)*(?P<id>[\da-z-]+)(?:[/#?]|$)'
|
_VALID_URL = r'https?://(?:www\.)?dr\.dk/tv/se/(?:[^/]+/)*(?P<id>[\da-z-]+)(?:[/#?]|$)'
|
||||||
|
|
||||||
_TEST = {
|
_TEST = {
|
||||||
'url': 'http://www.dr.dk/tv/se/partiets-mand/partiets-mand-7-8',
|
'url': 'https://www.dr.dk/tv/se/boern/ultra/panisk-paske/panisk-paske-5',
|
||||||
'md5': '4a7e1dd65cdb2643500a3f753c942f25',
|
'md5': 'dc515a9ab50577fa14cc4e4b0265168f',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'partiets-mand-7-8',
|
'id': 'panisk-paske-5',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Partiets mand (7:8)',
|
'title': 'Panisk Påske (5)',
|
||||||
'description': 'md5:a684b90a8f9336cd4aab94b7647d7862',
|
'description': 'md5:ca14173c5ab24cd26b0fcc074dff391c',
|
||||||
'timestamp': 1403047940,
|
'timestamp': 1426984612,
|
||||||
'upload_date': '20140617',
|
'upload_date': '20150322',
|
||||||
'duration': 1299.040,
|
'duration': 1455,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,6 +30,10 @@ class DRTVIE(InfoExtractor):
|
|||||||
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
if '>Programmet er ikke længere tilgængeligt' in webpage:
|
||||||
|
raise ExtractorError(
|
||||||
|
'Video %s is not available' % video_id, expected=True)
|
||||||
|
|
||||||
video_id = self._search_regex(
|
video_id = self._search_regex(
|
||||||
r'data-(?:material-identifier|episode-slug)="([^"]+)"',
|
r'data-(?:material-identifier|episode-slug)="([^"]+)"',
|
||||||
webpage, 'video id')
|
webpage, 'video id')
|
||||||
@@ -55,19 +63,31 @@ class DRTVIE(InfoExtractor):
|
|||||||
restricted_to_denmark = asset['RestrictedToDenmark']
|
restricted_to_denmark = asset['RestrictedToDenmark']
|
||||||
spoken_subtitles = asset['Target'] == 'SpokenSubtitles'
|
spoken_subtitles = asset['Target'] == 'SpokenSubtitles'
|
||||||
for link in asset['Links']:
|
for link in asset['Links']:
|
||||||
target = link['Target']
|
|
||||||
uri = link['Uri']
|
uri = link['Uri']
|
||||||
|
target = link['Target']
|
||||||
format_id = target
|
format_id = target
|
||||||
preference = -1 if target == 'HDS' else -2
|
preference = None
|
||||||
if spoken_subtitles:
|
if spoken_subtitles:
|
||||||
preference -= 2
|
preference = -1
|
||||||
format_id += '-spoken-subtitles'
|
format_id += '-spoken-subtitles'
|
||||||
formats.append({
|
if target == 'HDS':
|
||||||
'url': uri + '?hdcore=3.3.0&plugin=aasp-3.3.0.99.43' if target == 'HDS' else uri,
|
formats.extend(self._extract_f4m_formats(
|
||||||
'format_id': format_id,
|
uri + '?hdcore=3.3.0&plugin=aasp-3.3.0.99.43',
|
||||||
'ext': link['FileFormat'],
|
video_id, preference, f4m_id=format_id))
|
||||||
'preference': preference,
|
elif target == 'HLS':
|
||||||
})
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
uri, video_id, 'mp4', preference=preference,
|
||||||
|
m3u8_id=format_id))
|
||||||
|
else:
|
||||||
|
bitrate = link.get('Bitrate')
|
||||||
|
if bitrate:
|
||||||
|
format_id += '-%s' % bitrate
|
||||||
|
formats.append({
|
||||||
|
'url': uri,
|
||||||
|
'format_id': format_id,
|
||||||
|
'tbr': bitrate,
|
||||||
|
'ext': link.get('FileFormat'),
|
||||||
|
})
|
||||||
subtitles_list = asset.get('SubtitlesList')
|
subtitles_list = asset.get('SubtitlesList')
|
||||||
if isinstance(subtitles_list, list):
|
if isinstance(subtitles_list, list):
|
||||||
LANGS = {
|
LANGS = {
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ class DumpIE(InfoExtractor):
|
|||||||
video_url = self._search_regex(
|
video_url = self._search_regex(
|
||||||
r's1.addVariable\("file",\s*"([^"]+)"', webpage, 'video URL')
|
r's1.addVariable\("file",\s*"([^"]+)"', webpage, 'video URL')
|
||||||
|
|
||||||
thumb = self._og_search_thumbnail(webpage)
|
title = self._og_search_title(webpage)
|
||||||
title = self._search_regex(r'<b>([^"]+)</b>', webpage, 'title')
|
thumbnail = self._og_search_thumbnail(webpage)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'url': video_url,
|
'url': video_url,
|
||||||
'thumbnail': thumb,
|
'thumbnail': thumbnail,
|
||||||
}
|
}
|
||||||
|
|||||||
64
youtube_dl/extractor/dumpert.py
Normal file
64
youtube_dl/extractor/dumpert.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import base64
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..compat import compat_urllib_request
|
||||||
|
from ..utils import qualities
|
||||||
|
|
||||||
|
|
||||||
|
class DumpertIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?dumpert\.nl/(?:mediabase|embed)/(?P<id>[0-9]+/[0-9a-zA-Z]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://www.dumpert.nl/mediabase/6646981/951bc60f/',
|
||||||
|
'md5': '1b9318d7d5054e7dcb9dc7654f21d643',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '6646981/951bc60f',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Ik heb nieuws voor je',
|
||||||
|
'description': 'Niet schrikken hoor',
|
||||||
|
'thumbnail': 're:^https?://.*\.jpg$',
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.dumpert.nl/embed/6675421/dc440fe7/',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
url = 'https://www.dumpert.nl/mediabase/' + video_id
|
||||||
|
req = compat_urllib_request.Request(url)
|
||||||
|
req.add_header('Cookie', 'nsfw=1; cpc=10')
|
||||||
|
webpage = self._download_webpage(req, video_id)
|
||||||
|
|
||||||
|
files_base64 = self._search_regex(
|
||||||
|
r'data-files="([^"]+)"', webpage, 'data files')
|
||||||
|
|
||||||
|
files = self._parse_json(
|
||||||
|
base64.b64decode(files_base64.encode('utf-8')).decode('utf-8'),
|
||||||
|
video_id)
|
||||||
|
|
||||||
|
quality = qualities(['flv', 'mobile', 'tablet', '720p'])
|
||||||
|
|
||||||
|
formats = [{
|
||||||
|
'url': video_url,
|
||||||
|
'format_id': format_id,
|
||||||
|
'quality': quality(format_id),
|
||||||
|
} for format_id, video_url in files.items() if format_id != 'still']
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
title = self._html_search_meta(
|
||||||
|
'title', webpage) or self._og_search_title(webpage)
|
||||||
|
description = self._html_search_meta(
|
||||||
|
'description', webpage) or self._og_search_description(webpage)
|
||||||
|
thumbnail = files.get('still') or self._og_search_thumbnail(webpage)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
|
'formats': formats
|
||||||
|
}
|
||||||
@@ -45,6 +45,7 @@ class EaglePlatformIE(InfoExtractor):
|
|||||||
'duration': 216,
|
'duration': 216,
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
},
|
},
|
||||||
|
'skip': 'Georestricted',
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _handle_error(self, response):
|
def _handle_error(self, response):
|
||||||
@@ -78,7 +79,7 @@ class EaglePlatformIE(InfoExtractor):
|
|||||||
age_limit = 0 if age_restriction == 'allow_all' else 18
|
age_limit = 0 if age_restriction == 'allow_all' else 18
|
||||||
|
|
||||||
m3u8_data = self._download_json(
|
m3u8_data = self._download_json(
|
||||||
media['sources']['secure_m3u8']['auto'],
|
self._proto_relative_url(media['sources']['secure_m3u8']['auto'], 'http:'),
|
||||||
video_id, 'Downloading m3u8 JSON')
|
video_id, 'Downloading m3u8 JSON')
|
||||||
|
|
||||||
formats = self._extract_m3u8_formats(
|
formats = self._extract_m3u8_formats(
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from ..compat import (
|
|
||||||
compat_urllib_parse,
|
|
||||||
)
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
from ..compat import compat_urllib_parse_unquote
|
||||||
|
|
||||||
|
|
||||||
class EHowIE(InfoExtractor):
|
class EHowIE(InfoExtractor):
|
||||||
@@ -26,7 +24,7 @@ class EHowIE(InfoExtractor):
|
|||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
video_url = self._search_regex(
|
video_url = self._search_regex(
|
||||||
r'(?:file|source)=(http[^\'"&]*)', webpage, 'video URL')
|
r'(?:file|source)=(http[^\'"&]*)', webpage, 'video URL')
|
||||||
final_url = compat_urllib_parse.unquote(video_url)
|
final_url = compat_urllib_parse_unquote(video_url)
|
||||||
uploader = self._html_search_meta('uploader', webpage)
|
uploader = self._html_search_meta('uploader', webpage)
|
||||||
title = self._og_search_title(webpage).replace(' | eHow', '')
|
title = self._og_search_title(webpage).replace(' | eHow', '')
|
||||||
|
|
||||||
|
|||||||
@@ -6,56 +6,42 @@ import json
|
|||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
parse_iso8601,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class EllenTVIE(InfoExtractor):
|
class EllenTVIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?(?:ellentv|ellentube)\.com/videos/(?P<id>[a-z0-9_-]+)'
|
_VALID_URL = r'https?://(?:www\.)?(?:ellentv|ellentube)\.com/videos/(?P<id>[a-z0-9_-]+)'
|
||||||
_TESTS = [{
|
_TEST = {
|
||||||
'url': 'http://www.ellentv.com/videos/0-7jqrsr18/',
|
'url': 'http://www.ellentv.com/videos/0-ipq1gsai/',
|
||||||
'md5': 'e4af06f3bf0d5f471921a18db5764642',
|
'md5': '8e3c576bf2e9bfff4d76565f56f94c9c',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '0-7jqrsr18',
|
'id': '0_ipq1gsai',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'What\'s Wrong with These Photos? A Whole Lot',
|
'title': 'Fast Fingers of Fate',
|
||||||
'description': 'md5:35f152dc66b587cf13e6d2cf4fa467f6',
|
'description': 'md5:587e79fbbd0d73b148bc596d99ce48e6',
|
||||||
'timestamp': 1406876400,
|
'timestamp': 1428035648,
|
||||||
'upload_date': '20140801',
|
'upload_date': '20150403',
|
||||||
|
'uploader_id': 'batchUser',
|
||||||
}
|
}
|
||||||
}, {
|
}
|
||||||
'url': 'http://ellentube.com/videos/0-dvzmabd5/',
|
|
||||||
'md5': '98238118eaa2bbdf6ad7f708e3e4f4eb',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '0-dvzmabd5',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': '1 year old twin sister makes her brother laugh',
|
|
||||||
'description': '1 year old twin sister makes her brother laugh',
|
|
||||||
'timestamp': 1419542075,
|
|
||||||
'upload_date': '20141225',
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
|
|
||||||
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(
|
||||||
video_url = self._html_search_meta('VideoURL', webpage, 'url')
|
'http://widgets.ellentube.com/videos/%s' % video_id,
|
||||||
title = self._og_search_title(webpage, default=None) or self._search_regex(
|
video_id)
|
||||||
r'pageName\s*=\s*"([^"]+)"', webpage, 'title')
|
|
||||||
description = self._html_search_meta(
|
|
||||||
'description', webpage, 'description') or self._og_search_description(webpage)
|
|
||||||
timestamp = parse_iso8601(self._search_regex(
|
|
||||||
r'<span class="publish-date"><time datetime="([^"]+)">',
|
|
||||||
webpage, 'timestamp'))
|
|
||||||
|
|
||||||
return {
|
partner_id = self._search_regex(
|
||||||
'id': video_id,
|
r"var\s+partnerId\s*=\s*'([^']+)", webpage, 'partner id')
|
||||||
'url': video_url,
|
|
||||||
'title': title,
|
kaltura_id = self._search_regex(
|
||||||
'description': description,
|
[r'id="kaltura_player_([^"]+)"',
|
||||||
'timestamp': timestamp,
|
r"_wb_entry_id\s*:\s*'([^']+)",
|
||||||
}
|
r'data-kaltura-entry-id="([^"]+)'],
|
||||||
|
webpage, 'kaltura id')
|
||||||
|
|
||||||
|
return self.url_result('kaltura:%s:%s' % (partner_id, kaltura_id), 'Kaltura')
|
||||||
|
|
||||||
|
|
||||||
class EllenTVClipsIE(InfoExtractor):
|
class EllenTVClipsIE(InfoExtractor):
|
||||||
@@ -67,7 +53,7 @@ class EllenTVClipsIE(InfoExtractor):
|
|||||||
'id': 'meryl-streep-vanessa-hudgens',
|
'id': 'meryl-streep-vanessa-hudgens',
|
||||||
'title': 'Meryl Streep, Vanessa Hudgens',
|
'title': 'Meryl Streep, Vanessa Hudgens',
|
||||||
},
|
},
|
||||||
'playlist_mincount': 9,
|
'playlist_mincount': 7,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@@ -91,4 +77,8 @@ class EllenTVClipsIE(InfoExtractor):
|
|||||||
raise ExtractorError('Failed to download JSON', cause=ve)
|
raise ExtractorError('Failed to download JSON', cause=ve)
|
||||||
|
|
||||||
def _extract_entries(self, playlist):
|
def _extract_entries(self, playlist):
|
||||||
return [self.url_result(item['url'], 'EllenTV') for item in playlist]
|
return [
|
||||||
|
self.url_result(
|
||||||
|
'kaltura:%s:%s' % (item['kaltura_partner_id'], item['kaltura_entry_id']),
|
||||||
|
'Kaltura')
|
||||||
|
for item in playlist]
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from .tnaflix import TNAFlixIE
|
|
||||||
|
|
||||||
|
|
||||||
class EMPFlixIE(TNAFlixIE):
|
|
||||||
_VALID_URL = r'^https?://www\.empflix\.com/videos/(?P<display_id>[0-9a-zA-Z-]+)-(?P<id>[0-9]+)\.html'
|
|
||||||
|
|
||||||
_TITLE_REGEX = r'name="title" value="(?P<title>[^"]*)"'
|
|
||||||
_DESCRIPTION_REGEX = r'name="description" value="([^"]*)"'
|
|
||||||
_CONFIG_REGEX = r'flashvars\.config\s*=\s*escape\("([^"]+)"'
|
|
||||||
|
|
||||||
_TEST = {
|
|
||||||
'url': 'http://www.empflix.com/videos/Amateur-Finger-Fuck-33051.html',
|
|
||||||
'md5': 'b1bc15b6412d33902d6e5952035fcabc',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '33051',
|
|
||||||
'display_id': 'Amateur-Finger-Fuck',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Amateur Finger Fuck',
|
|
||||||
'description': 'Amateur solo finger fucking.',
|
|
||||||
'thumbnail': 're:https?://.*\.jpg$',
|
|
||||||
'age_limit': 18,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,10 @@ import re
|
|||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import compat_urllib_parse
|
from ..compat import compat_urllib_parse
|
||||||
from ..utils import ExtractorError
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
unescapeHTML
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class EroProfileIE(InfoExtractor):
|
class EroProfileIE(InfoExtractor):
|
||||||
@@ -68,15 +71,14 @@ class EroProfileIE(InfoExtractor):
|
|||||||
|
|
||||||
m = re.search(r'You must be logged in to view this video\.', webpage)
|
m = re.search(r'You must be logged in to view this video\.', webpage)
|
||||||
if m:
|
if m:
|
||||||
raise ExtractorError(
|
self.raise_login_required('This video requires login')
|
||||||
'This video requires login. Please specify a username and password and try again.', expected=True)
|
|
||||||
|
|
||||||
video_id = self._search_regex(
|
video_id = self._search_regex(
|
||||||
[r"glbUpdViews\s*\('\d*','(\d+)'", r'p/report/video/(\d+)'],
|
[r"glbUpdViews\s*\('\d*','(\d+)'", r'p/report/video/(\d+)'],
|
||||||
webpage, 'video id', default=None)
|
webpage, 'video id', default=None)
|
||||||
|
|
||||||
video_url = self._search_regex(
|
video_url = unescapeHTML(self._search_regex(
|
||||||
r'<source src="([^"]+)', webpage, 'video url')
|
r'<source src="([^"]+)', webpage, 'video url'))
|
||||||
title = self._html_search_regex(
|
title = self._html_search_regex(
|
||||||
r'Title:</th><td>([^<]+)</td>', webpage, 'title')
|
r'Title:</th><td>([^<]+)</td>', webpage, 'title')
|
||||||
thumbnail = self._search_regex(
|
thumbnail = self._search_regex(
|
||||||
|
|||||||
@@ -1,128 +1,107 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import (
|
from ..compat import compat_urllib_request
|
||||||
compat_urllib_parse,
|
|
||||||
compat_urllib_request,
|
|
||||||
)
|
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
determine_ext,
|
||||||
js_to_json,
|
clean_html,
|
||||||
parse_duration,
|
int_or_none,
|
||||||
|
float_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _decrypt_config(key, string):
|
||||||
|
a = ''
|
||||||
|
i = ''
|
||||||
|
r = ''
|
||||||
|
|
||||||
|
while len(a) < (len(string) / 2):
|
||||||
|
a += key
|
||||||
|
|
||||||
|
a = a[0:int(len(string) / 2)]
|
||||||
|
|
||||||
|
t = 0
|
||||||
|
while t < len(string):
|
||||||
|
i += chr(int(string[t] + string[t + 1], 16))
|
||||||
|
t += 2
|
||||||
|
|
||||||
|
icko = [s for s in i]
|
||||||
|
|
||||||
|
for t, c in enumerate(a):
|
||||||
|
r += chr(ord(c) ^ ord(icko[t]))
|
||||||
|
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
class EscapistIE(InfoExtractor):
|
class EscapistIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://?(www\.)?escapistmagazine\.com/videos/view/[^/?#]+/(?P<id>[0-9]+)-[^/?#]*(?:$|[?#])'
|
_VALID_URL = r'https?://?(?:www\.)?escapistmagazine\.com/videos/view/[^/?#]+/(?P<id>[0-9]+)-[^/?#]*(?:$|[?#])'
|
||||||
_USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko'
|
_TESTS = [{
|
||||||
_TEST = {
|
|
||||||
'url': 'http://www.escapistmagazine.com/videos/view/the-escapist-presents/6618-Breaking-Down-Baldurs-Gate',
|
'url': 'http://www.escapistmagazine.com/videos/view/the-escapist-presents/6618-Breaking-Down-Baldurs-Gate',
|
||||||
'md5': 'ab3a706c681efca53f0a35f1415cf0d1',
|
'md5': 'ab3a706c681efca53f0a35f1415cf0d1',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '6618',
|
'id': '6618',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'description': "Baldur's Gate: Original, Modded or Enhanced Edition? I'll break down what you can expect from the new Baldur's Gate: Enhanced Edition.",
|
'description': "Baldur's Gate: Original, Modded or Enhanced Edition? I'll break down what you can expect from the new Baldur's Gate: Enhanced Edition.",
|
||||||
'uploader_id': 'the-escapist-presents',
|
|
||||||
'uploader': 'The Escapist Presents',
|
|
||||||
'title': "Breaking Down Baldur's Gate",
|
'title': "Breaking Down Baldur's Gate",
|
||||||
'thumbnail': 're:^https?://.*\.jpg$',
|
'thumbnail': 're:^https?://.*\.jpg$',
|
||||||
'duration': 264,
|
'duration': 264,
|
||||||
|
'uploader': 'The Escapist',
|
||||||
}
|
}
|
||||||
}
|
}, {
|
||||||
|
'url': 'http://www.escapistmagazine.com/videos/view/zero-punctuation/10044-Evolve-One-vs-Multiplayer',
|
||||||
|
'md5': '9e8c437b0dbb0387d3bd3255ca77f6bf',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '10044',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'description': 'This week, Zero Punctuation reviews Evolve.',
|
||||||
|
'title': 'Evolve - One vs Multiplayer',
|
||||||
|
'thumbnail': 're:^https?://.*\.jpg$',
|
||||||
|
'duration': 304,
|
||||||
|
'uploader': 'The Escapist',
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
webpage_req = compat_urllib_request.Request(url)
|
webpage = self._download_webpage(url, video_id)
|
||||||
webpage_req.add_header('User-Agent', self._USER_AGENT)
|
|
||||||
webpage = self._download_webpage(webpage_req, video_id)
|
|
||||||
|
|
||||||
uploader_id = self._html_search_regex(
|
ims_video = self._parse_json(
|
||||||
r"<h1\s+class='headline'>\s*<a\s+href='/videos/view/(.*?)'",
|
self._search_regex(
|
||||||
webpage, 'uploader ID', fatal=False)
|
r'imsVideo\.play\(({.+?})\);', webpage, 'imsVideo'),
|
||||||
uploader = self._html_search_regex(
|
video_id)
|
||||||
r"<h1\s+class='headline'>(.*?)</a>",
|
video_id = ims_video['videoID']
|
||||||
webpage, 'uploader', fatal=False)
|
key = ims_video['hash']
|
||||||
description = self._html_search_meta('description', webpage)
|
|
||||||
duration = parse_duration(self._html_search_meta('duration', webpage))
|
|
||||||
|
|
||||||
raw_title = self._html_search_meta('title', webpage, fatal=True)
|
config_req = compat_urllib_request.Request(
|
||||||
title = raw_title.partition(' : ')[2]
|
'http://www.escapistmagazine.com/videos/'
|
||||||
|
'vidconfig.php?videoID=%s&hash=%s' % (video_id, key))
|
||||||
|
config_req.add_header('Referer', url)
|
||||||
|
config = self._download_webpage(config_req, video_id, 'Downloading video config')
|
||||||
|
|
||||||
config_url = compat_urllib_parse.unquote(self._html_search_regex(
|
data = json.loads(_decrypt_config(key, config))
|
||||||
r'''(?x)
|
|
||||||
(?:
|
|
||||||
<param\s+name="flashvars".*?\s+value="config=|
|
|
||||||
flashvars="config=
|
|
||||||
)
|
|
||||||
(https?://[^"&]+)
|
|
||||||
''',
|
|
||||||
webpage, 'config URL'))
|
|
||||||
|
|
||||||
formats = []
|
video_data = data['videoData']
|
||||||
ad_formats = []
|
|
||||||
|
|
||||||
def _add_format(name, cfg_url, quality):
|
title = clean_html(video_data['title'])
|
||||||
cfg_req = compat_urllib_request.Request(cfg_url)
|
duration = float_or_none(video_data.get('duration'), 1000)
|
||||||
cfg_req.add_header('User-Agent', self._USER_AGENT)
|
uploader = video_data.get('publisher')
|
||||||
config = self._download_json(
|
|
||||||
cfg_req, video_id,
|
|
||||||
'Downloading ' + name + ' configuration',
|
|
||||||
'Unable to download ' + name + ' configuration',
|
|
||||||
transform_source=js_to_json)
|
|
||||||
|
|
||||||
playlist = config['playlist']
|
formats = [{
|
||||||
for p in playlist:
|
'url': video['src'],
|
||||||
if p.get('eventCategory') == 'Video':
|
'format_id': '%s-%sp' % (determine_ext(video['src']), video['res']),
|
||||||
ar = formats
|
'height': int_or_none(video.get('res')),
|
||||||
elif p.get('eventCategory') == 'Video Postroll':
|
} for video in data['files']['videos']]
|
||||||
ar = ad_formats
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
|
|
||||||
ar.append({
|
|
||||||
'url': p['url'],
|
|
||||||
'format_id': name,
|
|
||||||
'quality': quality,
|
|
||||||
'http_headers': {
|
|
||||||
'User-Agent': self._USER_AGENT,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
_add_format('normal', config_url, quality=0)
|
|
||||||
hq_url = (config_url +
|
|
||||||
('&hq=1' if '?' in config_url else config_url + '?hq=1'))
|
|
||||||
try:
|
|
||||||
_add_format('hq', hq_url, quality=1)
|
|
||||||
except ExtractorError:
|
|
||||||
pass # That's fine, we'll just use normal quality
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
if '/escapist/sales-marketing/' in formats[-1]['url']:
|
return {
|
||||||
raise ExtractorError('This IP address has been blocked by The Escapist', expected=True)
|
|
||||||
|
|
||||||
res = {
|
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'uploader': uploader,
|
|
||||||
'uploader_id': uploader_id,
|
|
||||||
'title': title,
|
'title': title,
|
||||||
'thumbnail': self._og_search_thumbnail(webpage),
|
'thumbnail': self._og_search_thumbnail(webpage),
|
||||||
'description': description,
|
'description': self._og_search_description(webpage),
|
||||||
'duration': duration,
|
'duration': duration,
|
||||||
|
'uploader': uploader,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self._downloader.params.get('include_ads') and ad_formats:
|
|
||||||
self._sort_formats(ad_formats)
|
|
||||||
ad_res = {
|
|
||||||
'id': '%s-ad' % video_id,
|
|
||||||
'title': '%s (Postroll)' % title,
|
|
||||||
'formats': ad_formats,
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
'_type': 'playlist',
|
|
||||||
'entries': [res, ad_res],
|
|
||||||
'title': title,
|
|
||||||
'id': video_id,
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
|
|||||||
55
youtube_dl/extractor/espn.py
Normal file
55
youtube_dl/extractor/espn.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
|
||||||
|
|
||||||
|
class ESPNIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://espn\.go\.com/(?:[^/]+/)*(?P<id>[^/]+)'
|
||||||
|
_WORKING = False
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://espn.go.com/video/clip?id=10365079',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'FkYWtmazr6Ed8xmvILvKLWjd4QvYZpzG',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'dm_140128_30for30Shorts___JudgingJewellv2',
|
||||||
|
'description': '',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# m3u8 download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://espn.go.com/video/iframe/twitter/?cms=espn&id=10365079',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://espn.go.com/nba/recap?gameId=400793786',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://espn.go.com/blog/golden-state-warriors/post/_/id/593/how-warriors-rapidly-regained-a-winning-edge',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://espn.go.com/sports/endurance/story/_/id/12893522/dzhokhar-tsarnaev-sentenced-role-boston-marathon-bombings',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://espn.go.com/nba/playoffs/2015/story/_/id/12887571/john-wall-washington-wizards-no-swelling-left-hand-wrist-game-5-return',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
video_id = self._search_regex(
|
||||||
|
r'class="video-play-button"[^>]+data-id="(\d+)',
|
||||||
|
webpage, 'video id')
|
||||||
|
|
||||||
|
player = self._download_webpage(
|
||||||
|
'https://espn.go.com/video/iframe/twitter/?id=%s' % video_id, video_id)
|
||||||
|
|
||||||
|
pcode = self._search_regex(
|
||||||
|
r'["\']pcode=([^"\']+)["\']', player, 'pcode')
|
||||||
|
|
||||||
|
return self.url_result(
|
||||||
|
'ooyalaexternal:espn:%s:%s' % (video_id, pcode),
|
||||||
|
'OoyalaExternal')
|
||||||
74
youtube_dl/extractor/esri.py
Normal file
74
youtube_dl/extractor/esri.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..compat import compat_urlparse
|
||||||
|
from ..utils import (
|
||||||
|
int_or_none,
|
||||||
|
parse_filesize,
|
||||||
|
unified_strdate,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EsriVideoIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://video\.esri\.com/watch/(?P<id>[0-9]+)'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'https://video.esri.com/watch/1124/arcgis-online-_dash_-developing-applications',
|
||||||
|
'md5': 'd4aaf1408b221f1b38227a9bbaeb95bc',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1124',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'ArcGIS Online - Developing Applications',
|
||||||
|
'description': 'Jeremy Bartley demonstrates how to develop applications with ArcGIS Online.',
|
||||||
|
'thumbnail': 're:^https?://.*\.jpg$',
|
||||||
|
'duration': 185,
|
||||||
|
'upload_date': '20120419',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
for width, height, content in re.findall(
|
||||||
|
r'(?s)<li><strong>(\d+)x(\d+):</strong>(.+?)</li>', webpage):
|
||||||
|
for video_url, ext, filesize in re.findall(
|
||||||
|
r'<a[^>]+href="([^"]+)">([^<]+) \(([^<]+)\)</a>', content):
|
||||||
|
formats.append({
|
||||||
|
'url': compat_urlparse.urljoin(url, video_url),
|
||||||
|
'ext': ext.lower(),
|
||||||
|
'format_id': '%s-%s' % (ext.lower(), height),
|
||||||
|
'width': int(width),
|
||||||
|
'height': int(height),
|
||||||
|
'filesize_approx': parse_filesize(filesize),
|
||||||
|
})
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
title = self._html_search_meta('title', webpage, 'title')
|
||||||
|
description = self._html_search_meta(
|
||||||
|
'description', webpage, 'description', fatal=False)
|
||||||
|
|
||||||
|
thumbnail = self._html_search_meta('thumbnail', webpage, 'thumbnail', fatal=False)
|
||||||
|
if thumbnail:
|
||||||
|
thumbnail = re.sub(r'_[st]\.jpg$', '_x.jpg', thumbnail)
|
||||||
|
|
||||||
|
duration = int_or_none(self._search_regex(
|
||||||
|
[r'var\s+videoSeconds\s*=\s*(\d+)', r"'duration'\s*:\s*(\d+)"],
|
||||||
|
webpage, 'duration', fatal=False))
|
||||||
|
|
||||||
|
upload_date = unified_strdate(self._html_search_meta(
|
||||||
|
'last-modified', webpage, 'upload date', fatal=None))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
|
'duration': duration,
|
||||||
|
'upload_date': upload_date,
|
||||||
|
'formats': formats
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ from ..compat import (
|
|||||||
compat_http_client,
|
compat_http_client,
|
||||||
compat_str,
|
compat_str,
|
||||||
compat_urllib_error,
|
compat_urllib_error,
|
||||||
compat_urllib_parse,
|
compat_urllib_parse_unquote,
|
||||||
compat_urllib_request,
|
compat_urllib_request,
|
||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
@@ -17,6 +17,8 @@ from ..utils import (
|
|||||||
int_or_none,
|
int_or_none,
|
||||||
limit_length,
|
limit_length,
|
||||||
urlencode_postdata,
|
urlencode_postdata,
|
||||||
|
get_element_by_id,
|
||||||
|
clean_html,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -24,8 +26,12 @@ class FacebookIE(InfoExtractor):
|
|||||||
_VALID_URL = r'''(?x)
|
_VALID_URL = r'''(?x)
|
||||||
https?://(?:\w+\.)?facebook\.com/
|
https?://(?:\w+\.)?facebook\.com/
|
||||||
(?:[^#]*?\#!/)?
|
(?:[^#]*?\#!/)?
|
||||||
(?:video/video\.php|photo\.php|video\.php|video/embed)\?(?:.*?)
|
(?:
|
||||||
(?:v|video_id)=(?P<id>[0-9]+)
|
(?:video/video\.php|photo\.php|video\.php|video/embed)\?(?:.*?)
|
||||||
|
(?:v|video_id)=|
|
||||||
|
[^/]+/videos/(?:[^/]+/)?
|
||||||
|
)
|
||||||
|
(?P<id>[0-9]+)
|
||||||
(?:.*)'''
|
(?:.*)'''
|
||||||
_LOGIN_URL = 'https://www.facebook.com/login.php?next=http%3A%2F%2Ffacebook.com%2Fhome.php&login_attempt=1'
|
_LOGIN_URL = 'https://www.facebook.com/login.php?next=http%3A%2F%2Ffacebook.com%2Fhome.php&login_attempt=1'
|
||||||
_CHECKPOINT_URL = 'https://www.facebook.com/checkpoint/?next=http%3A%2F%2Ffacebook.com%2Fhome.php&_fb_noscript=1'
|
_CHECKPOINT_URL = 'https://www.facebook.com/checkpoint/?next=http%3A%2F%2Ffacebook.com%2Fhome.php&_fb_noscript=1'
|
||||||
@@ -38,6 +44,7 @@ class FacebookIE(InfoExtractor):
|
|||||||
'id': '637842556329505',
|
'id': '637842556329505',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 're:Did you know Kei Nishikori is the first Asian man to ever reach a Grand Slam',
|
'title': 're:Did you know Kei Nishikori is the first Asian man to ever reach a Grand Slam',
|
||||||
|
'uploader': 'Tennis on Facebook',
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
'note': 'Video without discernible title',
|
'note': 'Video without discernible title',
|
||||||
@@ -46,10 +53,20 @@ class FacebookIE(InfoExtractor):
|
|||||||
'id': '274175099429670',
|
'id': '274175099429670',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Facebook video #274175099429670',
|
'title': 'Facebook video #274175099429670',
|
||||||
}
|
'uploader': 'Asif Nawab Butt',
|
||||||
|
},
|
||||||
|
'expected_warnings': [
|
||||||
|
'title'
|
||||||
|
]
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.facebook.com/video.php?v=10204634152394104',
|
'url': 'https://www.facebook.com/video.php?v=10204634152394104',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.facebook.com/amogood/videos/1618742068337349/?fref=nf',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.facebook.com/ChristyClarkForBC/videos/vb.22819070941/10153870694020942/?type=2&theater',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _login(self):
|
def _login(self):
|
||||||
@@ -123,7 +140,7 @@ class FacebookIE(InfoExtractor):
|
|||||||
else:
|
else:
|
||||||
raise ExtractorError('Cannot parse data')
|
raise ExtractorError('Cannot parse data')
|
||||||
data = dict(json.loads(m.group(1)))
|
data = dict(json.loads(m.group(1)))
|
||||||
params_raw = compat_urllib_parse.unquote(data['params'])
|
params_raw = compat_urllib_parse_unquote(data['params'])
|
||||||
params = json.loads(params_raw)
|
params = json.loads(params_raw)
|
||||||
video_data = params['video_data'][0]
|
video_data = params['video_data'][0]
|
||||||
|
|
||||||
@@ -139,15 +156,16 @@ class FacebookIE(InfoExtractor):
|
|||||||
raise ExtractorError('Cannot find video formats')
|
raise ExtractorError('Cannot find video formats')
|
||||||
|
|
||||||
video_title = self._html_search_regex(
|
video_title = self._html_search_regex(
|
||||||
r'<h2 class="uiHeaderTitle">([^<]*)</h2>', webpage, 'title',
|
r'<h2\s+[^>]*class="uiHeaderTitle"[^>]*>([^<]*)</h2>', webpage, 'title',
|
||||||
fatal=False)
|
default=None)
|
||||||
if not video_title:
|
if not video_title:
|
||||||
video_title = self._html_search_regex(
|
video_title = self._html_search_regex(
|
||||||
r'(?s)<span class="fbPhotosPhotoCaption".*?id="fbPhotoPageCaption"><span class="hasCaption">(.*?)</span>',
|
r'(?s)<span class="fbPhotosPhotoCaption".*?id="fbPhotoPageCaption"><span class="hasCaption">(.*?)</span>',
|
||||||
webpage, 'alternative title', default=None)
|
webpage, 'alternative title', fatal=False)
|
||||||
video_title = limit_length(video_title, 80)
|
video_title = limit_length(video_title, 80)
|
||||||
if not video_title:
|
if not video_title:
|
||||||
video_title = 'Facebook video #%s' % video_id
|
video_title = 'Facebook video #%s' % video_id
|
||||||
|
uploader = clean_html(get_element_by_id('fbPhotoPageAuthorName', webpage))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
@@ -155,4 +173,5 @@ class FacebookIE(InfoExtractor):
|
|||||||
'formats': formats,
|
'formats': formats,
|
||||||
'duration': int_or_none(video_data.get('video_duration')),
|
'duration': int_or_none(video_data.get('video_duration')),
|
||||||
'thumbnail': video_data.get('thumbnail_src'),
|
'thumbnail': video_data.get('thumbnail_src'),
|
||||||
|
'uploader': uploader,
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user