Compare commits
691 Commits
2012.11.29
...
2013.06.21
Author | SHA1 | Date | |
---|---|---|---|
|
759d525301 | ||
|
fcfa188548 | ||
|
f4c8bbcfc2 | ||
|
038a3a1a61 | ||
|
587c68b2cd | ||
|
377fdf5dde | ||
|
5c67601931 | ||
|
68f54207a3 | ||
|
bb47437686 | ||
|
213b715893 | ||
|
449d5c910c | ||
|
0251f9c9c0 | ||
|
8bc7c3d858 | ||
|
af44c94862 | ||
|
36ed7177f0 | ||
|
32aa88bcae | ||
|
31513ea6b9 | ||
|
88cebbd7b8 | ||
|
fb8f7280bc | ||
|
f380401bbd | ||
|
9abc6c8b31 | ||
|
8cd252f115 | ||
|
53f72b11e5 | ||
|
ee55fcbe12 | ||
|
78d3442b12 | ||
|
979a9dd4c4 | ||
|
d5979c5d55 | ||
|
8027175600 | ||
|
3054ff0cbe | ||
|
cd453d38bb | ||
|
f5a290eed9 | ||
|
ecb3e676a5 | ||
|
8b59a98610 | ||
|
8409501206 | ||
|
be95cac157 | ||
|
476203d025 | ||
|
468e2e926b | ||
|
ac3e9394e7 | ||
|
868d62a509 | ||
|
157b864a01 | ||
|
951b9dfd94 | ||
|
1142d31164 | ||
|
9131bde941 | ||
|
1132c10dc2 | ||
|
c978a96c02 | ||
|
71e458d437 | ||
|
57bde0d9c7 | ||
|
50b4d25980 | ||
|
eda60e8251 | ||
|
c794cbbb19 | ||
|
4a76d1dbe5 | ||
|
418f734a58 | ||
|
dc1c355b72 | ||
|
1b2b22ed9f | ||
|
f2cd958c0a | ||
|
57adeaea87 | ||
|
8f3f1aef05 | ||
|
51d2453c7a | ||
|
45014296be | ||
|
afef36c950 | ||
|
b31756c18e | ||
|
f008688520 | ||
|
5b68ea215b | ||
|
b1d568f0bc | ||
|
17bd1b2f41 | ||
|
5b0d3cc0cd | ||
|
d4f76f1674 | ||
|
340fa21198 | ||
|
de5d66d431 | ||
|
7bdb17d4d5 | ||
|
419c64b107 | ||
|
99a5ae3f8e | ||
|
c7563c528b | ||
|
e30e9318da | ||
|
5c51028d38 | ||
|
c1d58e1c67 | ||
|
02030ff7fe | ||
|
f45c185fa9 | ||
|
1bd96c3a60 | ||
|
929f85d851 | ||
|
98d4a4e6bc | ||
|
fb2f83360c | ||
|
3c5e7729e1 | ||
|
5a853e1423 | ||
|
2f58b12dad | ||
|
59f4fd4dc6 | ||
|
5738240ee8 | ||
|
86fd453ea8 | ||
|
c83411b9ee | ||
|
057c9938a1 | ||
|
9259966132 | ||
|
b08980412e | ||
|
532a1e0429 | ||
|
2a36c352a0 | ||
|
1a2adf3f49 | ||
|
43b62accbb | ||
|
be74864ace | ||
|
0ae456f08a | ||
|
0f75d25991 | ||
|
67129e4a15 | ||
|
dfb9323cf9 | ||
|
7f5bd09baf | ||
|
02d5eb935f | ||
|
94ca71b7cc | ||
|
b338f1b154 | ||
|
486f0c9476 | ||
|
d96680f58d | ||
|
f8602d3242 | ||
|
0c021ad171 | ||
|
086d7b4500 | ||
|
891629c84a | ||
|
ea6d901e51 | ||
|
4539dd30e6 | ||
|
c43e57242e | ||
|
db8fd71ca9 | ||
|
f4f316881d | ||
|
0e16f09474 | ||
|
09dd418f53 | ||
|
decd1d1737 | ||
|
180e689f7e | ||
|
7da5556ac2 | ||
|
f23a03a89b | ||
|
84e4682f0e | ||
|
1f99511210 | ||
|
0d94f2474c | ||
|
480b6c1e8b | ||
|
95464f14d1 | ||
|
c34407d16c | ||
|
5e34d2ebbf | ||
|
815dd2ffa8 | ||
|
ecd5fb49c5 | ||
|
b86174e7a3 | ||
|
2e2038dc35 | ||
|
46bfb42258 | ||
|
feecf22511 | ||
|
4c4f15eb78 | ||
|
104ccdb8b4 | ||
|
6ccff79594 | ||
|
aed523ecc1 | ||
|
d496a75d0a | ||
|
5c01dd1e73 | ||
|
11d9224e3b | ||
|
34c29ba1d7 | ||
|
6cd657f9f2 | ||
|
4ae9e55822 | ||
|
8749b71273 | ||
|
dbc50fdf82 | ||
|
b1d2ef9255 | ||
|
5fb16555af | ||
|
ba7c775a04 | ||
|
fe348844d9 | ||
|
767e00277f | ||
|
6ce533a220 | ||
|
08b2ac745a | ||
|
46a127eecb | ||
|
fc63faf070 | ||
|
9665577802 | ||
|
434aca5b14 | ||
|
e31852aba9 | ||
|
37254abc36 | ||
|
a11ea50319 | ||
|
81df121dd3 | ||
|
50f6412eb8 | ||
|
bf50b0383e | ||
|
bd55852517 | ||
|
4c9f7a9988 | ||
|
aba8df23ed | ||
|
3820df0106 | ||
|
e74c504f91 | ||
|
fa70605db2 | ||
|
0d173446ff | ||
|
320e26a0af | ||
|
a3d689cfb3 | ||
|
59cc5d9380 | ||
|
28535652ab | ||
|
7b670a4483 | ||
|
69fc019f26 | ||
|
613bf66939 | ||
|
9edb0916f4 | ||
|
f4b659f782 | ||
|
c70446c7df | ||
|
c76cb6d548 | ||
|
71f37e90ef | ||
|
75b5c590a8 | ||
|
4469666780 | ||
|
c15e024141 | ||
|
8cb94542f4 | ||
|
c681a03918 | ||
|
30f2999962 | ||
|
74e3452b9e | ||
|
9e1cf0c200 | ||
|
e11eb11906 | ||
|
c04bca6f60 | ||
|
b0936ef423 | ||
|
41a6eb949a | ||
|
f17ce13a92 | ||
|
8c416ad29a | ||
|
c72938240e | ||
|
e905b6f80e | ||
|
6de8f1afb7 | ||
|
9341212642 | ||
|
f7a9721e16 | ||
|
089e843b0f | ||
|
c8056d866a | ||
|
49da66e459 | ||
|
fb6c319904 | ||
|
5a8d13199c | ||
|
dce9027045 | ||
|
feba604e92 | ||
|
d22f65413a | ||
|
0599ef8c08 | ||
|
bfdf469295 | ||
|
32c96387c1 | ||
|
c8c5443bb5 | ||
|
a60b854d90 | ||
|
b8ad4f02a2 | ||
|
d281274bf2 | ||
|
b625bc2c31 | ||
|
f4381ab88a | ||
|
744435f2a4 | ||
|
855703e55e | ||
|
927c8c4924 | ||
|
0ba994e9e3 | ||
|
af9ad45cd4 | ||
|
e0fee250c3 | ||
|
72ca05016d | ||
|
844d1f9fa1 | ||
|
213c31ae16 | ||
|
04f3d551a0 | ||
|
e8600d69fd | ||
|
b03d65c237 | ||
|
8743974189 | ||
|
dc36bc9434 | ||
|
bce878a7c1 | ||
|
532d797824 | ||
|
146c12a2da | ||
|
d39919c03e | ||
|
df2dedeefb | ||
|
adb029ed81 | ||
|
43ff1a347d | ||
|
14294236bf | ||
|
c2b293ba30 | ||
|
37cd9f522f | ||
|
f33154cd39 | ||
|
bafeed9f5d | ||
|
ef767f9fd5 | ||
|
bc97f6d60c | ||
|
90a99c1b5e | ||
|
f375d4b7de | ||
|
fa41fbd318 | ||
|
6a205c8876 | ||
|
0fb3756409 | ||
|
fbbdf475b1 | ||
|
c238be3e3a | ||
|
1bf2801e6a | ||
|
c9c8402093 | ||
|
6060788083 | ||
|
e3700fc9e4 | ||
|
b693216d8d | ||
|
46b9d8295d | ||
|
7decf8951c | ||
|
1f46c15262 | ||
|
0cd358676c | ||
|
43113d92cc | ||
|
7eab8dc750 | ||
|
44e939514e | ||
|
95506f1235 | ||
|
a91556fd74 | ||
|
1447f728b5 | ||
|
d2c690828a | ||
|
cfa90f4adc | ||
|
898280a056 | ||
|
59b4a2f0e4 | ||
|
1ee9778405 | ||
|
db74c11d2b | ||
|
5011cded16 | ||
|
f10b2a9c14 | ||
|
5cb3c0b319 | ||
|
b9fc428494 | ||
|
c0ba104674 | ||
|
2a4093eaf3 | ||
|
9e62bc4439 | ||
|
553d097442 | ||
|
ae608b8076 | ||
|
c397187061 | ||
|
e32b06e977 | ||
|
8c42c506cd | ||
|
8cc83b8dbe | ||
|
51af426d89 | ||
|
08ec0af7c6 | ||
|
3b221c5406 | ||
|
3d3423574d | ||
|
e5edd51de4 | ||
|
64c78d50cc | ||
|
b3bcca0844 | ||
|
61e40c88a9 | ||
|
40634747f7 | ||
|
c2e21f2f0d | ||
|
47dcd621c0 | ||
|
a0d6fe7b92 | ||
|
c9fa1cbab6 | ||
|
8a38a194fb | ||
|
6ac7f082c4 | ||
|
f6e6da9525 | ||
|
597cc8a455 | ||
|
3370abd509 | ||
|
631f73978c | ||
|
e5f30ade10 | ||
|
6622d22c79 | ||
|
4e1582f372 | ||
|
967897fd22 | ||
|
f918ec7ea2 | ||
|
a2ae43a55f | ||
|
7ae153ee9c | ||
|
f7b567ff84 | ||
|
f2e237adc8 | ||
|
2e5457be1d | ||
|
7f9d41a55e | ||
|
8207626bbe | ||
|
df8db1aa21 | ||
|
691db5ba02 | ||
|
acb8752f80 | ||
|
679790eee1 | ||
|
6bf48bd866 | ||
|
790d4fcbe1 | ||
|
89de9eb125 | ||
|
6324fd1d74 | ||
|
9e07cf2955 | ||
|
f03b88b3fb | ||
|
97d0365f49 | ||
|
12887875a2 | ||
|
450e709972 | ||
|
9befce2b8c | ||
|
cb99797798 | ||
|
f82b28146a | ||
|
4dc72b830c | ||
|
ea05129ebd | ||
|
35d217133f | ||
|
d1b7a24354 | ||
|
c85538dba1 | ||
|
60bd48b175 | ||
|
4be0aa3539 | ||
|
f636c34481 | ||
|
3bf79c752e | ||
|
cdb130b09a | ||
|
2e5d60b7db | ||
|
8271226a55 | ||
|
1013186a17 | ||
|
7c038b3c32 | ||
|
c8cd8e5f55 | ||
|
471cf47796 | ||
|
d8f64574a4 | ||
|
e711babbd1 | ||
|
a72b0f2b6f | ||
|
434eb6f26b | ||
|
197080b10b | ||
|
7796e8c2cb | ||
|
6d4363368a | ||
|
414638cd50 | ||
|
2a9983b78f | ||
|
b17c974a88 | ||
|
5717d91ab7 | ||
|
79eb0287ab | ||
|
58994225bc | ||
|
59d4c2fe1b | ||
|
3a468f2d8b | ||
|
1ad5d872b9 | ||
|
355fc8e944 | ||
|
380a29dbf7 | ||
|
1528d6642d | ||
|
7311fef854 | ||
|
906417c7c5 | ||
|
6aabe82035 | ||
|
f0877a445e | ||
|
da06e2daf8 | ||
|
d3f5f9f6b9 | ||
|
bfc6ea7935 | ||
|
8edc2cf8ca | ||
|
fb778e66df | ||
|
3a9918d37f | ||
|
ccb0cae134 | ||
|
085c8b75a6 | ||
|
dbf2ba3d61 | ||
|
b47bbac393 | ||
|
229cac754a | ||
|
0e33684194 | ||
|
9e982f9e4e | ||
|
c7a725cfad | ||
|
450a30cae8 | ||
|
9cd5e4fce8 | ||
|
edba5137b8 | ||
|
233a22960a | ||
|
3b024e17af | ||
|
a32b573ccb | ||
|
ec71c13ab8 | ||
|
f0bad2b026 | ||
|
25580f3251 | ||
|
da4de959df | ||
|
d0d51a8afa | ||
|
c67598c3e1 | ||
|
811d253bc2 | ||
|
c3a1642ead | ||
|
ccf65f9dee | ||
|
b954070d70 | ||
|
30e9f4496b | ||
|
271d3fbdaa | ||
|
6df40dcbe0 | ||
|
97f194c1fb | ||
|
4da769ccca | ||
|
253d96f2e2 | ||
|
bbc3e2753a | ||
|
67353612ba | ||
|
bffbd5f038 | ||
|
d8bbf2018e | ||
|
187f491ad2 | ||
|
335959e778 | ||
|
3b83bf8f6a | ||
|
51719893bf | ||
|
1841f65e64 | ||
|
bb28998920 | ||
|
fbc5f99db9 | ||
|
ca0a0bbeec | ||
|
6119f78cb9 | ||
|
539679c7f9 | ||
|
b642cd44c1 | ||
|
fffec3b9d9 | ||
|
3446dfb7cb | ||
|
db16276b7c | ||
|
629fcdd135 | ||
|
64ce2aada8 | ||
|
565f751967 | ||
|
6017964580 | ||
|
1d16b0c3fe | ||
|
7851b37993 | ||
|
d81edc573e | ||
|
ef0c8d5f9f | ||
|
db30f02b50 | ||
|
4ba7262467 | ||
|
67d0c25eab | ||
|
09f9552b40 | ||
|
142d38f776 | ||
|
6dd3471900 | ||
|
280d67896a | ||
|
510e6f6dc1 | ||
|
712e86b999 | ||
|
74fdba620d | ||
|
dc1c479a6f | ||
|
119d536e07 | ||
|
fa1bf9c653 | ||
|
814eed0ea1 | ||
|
0aa3068e9e | ||
|
db2d6124b1 | ||
|
039dc61bd2 | ||
|
4b879984ea | ||
|
55e286ba55 | ||
|
9450bfa26e | ||
|
18be482a6f | ||
|
ca6710ee41 | ||
|
9314810243 | ||
|
7717ae19fa | ||
|
32635ec685 | ||
|
caec7618a1 | ||
|
7e7ab2815c | ||
|
d7744f2219 | ||
|
7161829de5 | ||
|
991ba7fae3 | ||
|
a7539296ce | ||
|
258d5850c9 | ||
|
20759b340a | ||
|
8e5f761870 | ||
|
26714799c9 | ||
|
5e9d042d8f | ||
|
9cf98a2bcc | ||
|
f5ebb61495 | ||
|
431d88dd31 | ||
|
876f1a86af | ||
|
01951dda7a | ||
|
6e3dba168b | ||
|
d851e895d5 | ||
|
b962b76f43 | ||
|
26cf040827 | ||
|
8e241d1a1a | ||
|
3a648b209c | ||
|
c80f0a417a | ||
|
4fcca4bb18 | ||
|
511eda8eda | ||
|
5f9551719c | ||
|
d830b7c297 | ||
|
1c256f7047 | ||
|
a34dd63beb | ||
|
4aeae91f86 | ||
|
c073e35b1e | ||
|
5c892b0ba9 | ||
|
6985325e01 | ||
|
911ee27e83 | ||
|
2069acc6a4 | ||
|
278986ea0f | ||
|
6535e9511f | ||
|
60c7520a51 | ||
|
deb594a9a0 | ||
|
e314ba675b | ||
|
0214ce7c75 | ||
|
95fedbf86b | ||
|
b7769a05ec | ||
|
067f6a3536 | ||
|
8cad53e84c | ||
|
d5ed35b664 | ||
|
f427df17ab | ||
|
4e38899e97 | ||
|
cb6ff87fbb | ||
|
0deac3a2d8 | ||
|
92e3e18a1d | ||
|
3bb6165927 | ||
|
d0d4f277da | ||
|
99b0a1292b | ||
|
dc23886a77 | ||
|
b7298b6e2a | ||
|
3e6c3f52a9 | ||
|
0c0074328b | ||
|
f0648fc18c | ||
|
a7c0f8602e | ||
|
21a9c6aaac | ||
|
162e3c5261 | ||
|
6b3aef80ce | ||
|
77c4beab8a | ||
|
1a2c3c0f3e | ||
|
0eaf520d77 | ||
|
056d857571 | ||
|
69a3883199 | ||
|
0dcfb234ed | ||
|
43e8fafd49 | ||
|
314d506b96 | ||
|
af42895612 | ||
|
bfa6389b74 | ||
|
9b14f51a3e | ||
|
f4bfd65ff2 | ||
|
3cc687d486 | ||
|
cdb3076445 | ||
|
8a2f13c304 | ||
|
77bd7968ea | ||
|
993693aa79 | ||
|
ce4be3a91d | ||
|
937021133f | ||
|
f7b111b7d1 | ||
|
80d3177e5c | ||
|
5e5ddcfbcf | ||
|
5910e210f4 | ||
|
b375c8b946 | ||
|
88f6c78b02 | ||
|
4096b60948 | ||
|
2ab1c5ed1a | ||
|
0b40544f29 | ||
|
187da2c093 | ||
|
9a2cf56d51 | ||
|
0be41ec241 | ||
|
f1171f7c2d | ||
|
28ca6b5afa | ||
|
bec102a843 | ||
|
8f6f40d991 | ||
|
e2a8ff24a9 | ||
|
8588a86f9e | ||
|
5cb9c3129b | ||
|
4cc3d07426 | ||
|
5d01a64719 | ||
|
a276e06080 | ||
|
fd5ff02042 | ||
|
2b5b2cb84c | ||
|
ca6849e65d | ||
|
1535ac2ae9 | ||
|
a4680a590f | ||
|
fedb6816cd | ||
|
f6152b4b64 | ||
|
4b618047ce | ||
|
2c6945be30 | ||
|
9a6f4429a0 | ||
|
4c21c56bfe | ||
|
2a298b72eb | ||
|
55c0539872 | ||
|
9789a05c20 | ||
|
d050de77f9 | ||
|
95eb771dcd | ||
|
4fb1acc212 | ||
|
d3d3199870 | ||
|
1ca63e3ae3 | ||
|
59ce201915 | ||
|
8d5d3a5d00 | ||
|
37c8fd4842 | ||
|
3c6ffbaedb | ||
|
c7287a3caf | ||
|
5a304a7637 | ||
|
4c1d273e88 | ||
|
a9d2f7e894 | ||
|
682407f2d5 | ||
|
bdff345529 | ||
|
23109d6a9c | ||
|
4bb028f48e | ||
|
fec89790b1 | ||
|
a5741a3f5e | ||
|
863baa16ec | ||
|
c7214f9a6f | ||
|
8fd3afd56c | ||
|
f9b2f2b955 | ||
|
633b4a5ff6 | ||
|
b4cd069d5e | ||
|
0f8d03f81c | ||
|
077174f4ed | ||
|
e387eb5aba | ||
|
4083bf81a0 | ||
|
796173d08b | ||
|
e575b6821e | ||
|
d78be7e331 | ||
|
15c8d83358 | ||
|
e91d2338d8 | ||
|
4b235346d6 | ||
|
ad348291bb | ||
|
2f1765c4ea | ||
|
3c5b63d2d6 | ||
|
cc51a7d4e0 | ||
|
8af4ed7b4f | ||
|
8192ebe1f8 | ||
|
20ba04267c | ||
|
743b28ce11 | ||
|
caaa47d372 | ||
|
10f100ac8a | ||
|
8176041605 | ||
|
87bec4c715 | ||
|
190e8e27d8 | ||
|
4efe62a016 | ||
|
c64de2c980 | ||
|
6ad98fb3fd | ||
|
b08e09c370 | ||
|
cdab8aa389 | ||
|
3cd69a54b2 | ||
|
627dcfff39 | ||
|
df5cff3751 | ||
|
79ae0a06d5 | ||
|
2d2fa229ec | ||
|
5a59fd6392 | ||
|
0eb0faa26f | ||
|
32761d863c | ||
|
799c076384 | ||
|
f1cb5bcad2 | ||
|
9e8056d5a7 | ||
|
c6f3620859 | ||
|
59ae15a507 | ||
|
40b35b4aa6 | ||
|
be0f77d075 | ||
|
0f00efed4c | ||
|
e6137fd61d | ||
|
8cd10ac4ef | ||
|
64a57846d3 | ||
|
72f976701a | ||
|
5bd9cc7a6a | ||
|
f660c89d51 | ||
|
73dce4b2e4 | ||
|
9f37a95941 | ||
|
a130bc6d02 | ||
|
348d0a7a18 | ||
|
03f9daab34 | ||
|
a8156c1d2e | ||
|
3e669f369f | ||
|
da779b4924 | ||
|
89fb51dd2d | ||
|
01ba00ca42 | ||
|
e08bee320e | ||
|
96731798db | ||
|
c116339ddb | ||
|
e643e2c6b7 | ||
|
c63cc10ffa | ||
|
dae7c920f6 | ||
|
f462df021a | ||
|
1a84d8675b | ||
|
18ea0cefc3 | ||
|
c806f804d8 | ||
|
03c5b0fbd4 | ||
|
95649b3936 | ||
|
3aeb78ea4e | ||
|
dd109dee8e | ||
|
b514df2034 | ||
|
0969bdd305 | ||
|
1a9c655e3b | ||
|
7e4674830e | ||
|
9ce5d9ee75 | ||
|
b49e75ff9a | ||
|
abe7a3ac2a | ||
|
717b1f72ed | ||
|
5f7ad21633 | ||
|
089d47f8d5 | ||
|
fdef722fa1 | ||
|
110d4f4c91 | ||
|
bae611f216 |
14
.gitignore
vendored
14
.gitignore
vendored
@@ -1,6 +1,20 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
*.pyo
|
*.pyo
|
||||||
*~
|
*~
|
||||||
|
*.DS_Store
|
||||||
wine-py2exe/
|
wine-py2exe/
|
||||||
py2exe.log
|
py2exe.log
|
||||||
*.kate-swp
|
*.kate-swp
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
MANIFEST
|
||||||
|
README.txt
|
||||||
|
youtube-dl.1
|
||||||
|
youtube-dl.bash-completion
|
||||||
|
youtube-dl
|
||||||
|
youtube-dl.exe
|
||||||
|
youtube-dl.tar.gz
|
||||||
|
.coverage
|
||||||
|
cover/
|
||||||
|
updates_key.pem
|
||||||
|
*.egg-info
|
16
.travis.yml
16
.travis.yml
@@ -1,9 +1,15 @@
|
|||||||
language: python
|
language: python
|
||||||
#specify the python version
|
|
||||||
python:
|
python:
|
||||||
- "2.6"
|
- "2.6"
|
||||||
- "2.7"
|
- "2.7"
|
||||||
#command to install the setup
|
- "3.3"
|
||||||
install:
|
script: nosetests test --verbose
|
||||||
# command to run tests
|
notifications:
|
||||||
script: nosetests test --nocapture
|
email:
|
||||||
|
- filippo.valsorda@gmail.com
|
||||||
|
- phihag@phihag.de
|
||||||
|
- jaime.marquinez.ferrandiz+travis@gmail.com
|
||||||
|
# irc:
|
||||||
|
# channels:
|
||||||
|
# - "irc.freenode.org#youtube-dl"
|
||||||
|
# skip_join: true
|
||||||
|
14
CHANGELOG
Normal file
14
CHANGELOG
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
2013.01.02 Codename: GIULIA
|
||||||
|
|
||||||
|
* Add support for ComedyCentral clips <nto>
|
||||||
|
* Corrected Vimeo description fetching <Nick Daniels>
|
||||||
|
* Added the --no-post-overwrites argument <Barbu Paul - Gheorghe>
|
||||||
|
* --verbose offers more environment info
|
||||||
|
* New info_dict field: uploader_id
|
||||||
|
* New updates system, with signature checking
|
||||||
|
* New IEs: NBA, JustinTV, FunnyOrDie, TweetReel, Steam, Ustream
|
||||||
|
* Fixed IEs: BlipTv
|
||||||
|
* Fixed for Python 3 IEs: Xvideo, Youku, XNXX, Dailymotion, Vimeo, InfoQ
|
||||||
|
* Simplified IEs and test code
|
||||||
|
* Various (Python 3 and other) fixes
|
||||||
|
* Revamped and expanded tests
|
@@ -1 +1 @@
|
|||||||
2012.11.29
|
2012.12.99
|
||||||
|
24
LICENSE
Normal file
24
LICENSE
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
This is free and unencumbered software released into the public domain.
|
||||||
|
|
||||||
|
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||||
|
distribute this software, either in source code form or as a compiled
|
||||||
|
binary, for any purpose, commercial or non-commercial, and by any
|
||||||
|
means.
|
||||||
|
|
||||||
|
In jurisdictions that recognize copyright laws, the author or authors
|
||||||
|
of this software dedicate any and all copyright interest in the
|
||||||
|
software to the public domain. We make this dedication for the benefit
|
||||||
|
of the public at large and to the detriment of our heirs and
|
||||||
|
successors. We intend this dedication to be an overt act of
|
||||||
|
relinquishment in perpetuity of all present and future rights to this
|
||||||
|
software under copyright law.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||||
|
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
For more information, please refer to <http://unlicense.org/>
|
5
MANIFEST.in
Normal file
5
MANIFEST.in
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
include README.md
|
||||||
|
include test/*.py
|
||||||
|
include test/*.json
|
||||||
|
include youtube-dl.bash-completion
|
||||||
|
include youtube-dl.1
|
79
Makefile
79
Makefile
@@ -1,13 +1,26 @@
|
|||||||
all: youtube-dl README.md youtube-dl.1 youtube-dl.bash-completion LATEST_VERSION
|
all: youtube-dl README.md README.txt youtube-dl.1 youtube-dl.bash-completion
|
||||||
# TODO: re-add youtube-dl.exe, and make sure it's 1. safe and 2. doesn't need sudo
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f youtube-dl youtube-dl.exe youtube-dl.1 LATEST_VERSION
|
rm -rf youtube-dl.1 youtube-dl.bash-completion README.txt MANIFEST build/ dist/ .coverage cover/ youtube-dl.tar.gz
|
||||||
|
|
||||||
|
cleanall: clean
|
||||||
|
rm -f youtube-dl youtube-dl.exe
|
||||||
|
|
||||||
PREFIX=/usr/local
|
PREFIX=/usr/local
|
||||||
BINDIR=$(PREFIX)/bin
|
BINDIR=$(PREFIX)/bin
|
||||||
MANDIR=$(PREFIX)/man
|
MANDIR=$(PREFIX)/man
|
||||||
SYSCONFDIR=/etc
|
PYTHON=/usr/bin/env python
|
||||||
|
|
||||||
|
# set SYSCONFDIR to /etc if PREFIX=/usr or PREFIX=/usr/local
|
||||||
|
ifeq ($(PREFIX),/usr)
|
||||||
|
SYSCONFDIR=/etc
|
||||||
|
else
|
||||||
|
ifeq ($(PREFIX),/usr/local)
|
||||||
|
SYSCONFDIR=/etc
|
||||||
|
else
|
||||||
|
SYSCONFDIR=$(PREFIX)/etc
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
install: youtube-dl youtube-dl.1 youtube-dl.bash-completion
|
install: youtube-dl youtube-dl.1 youtube-dl.bash-completion
|
||||||
install -d $(DESTDIR)$(BINDIR)
|
install -d $(DESTDIR)$(BINDIR)
|
||||||
@@ -18,40 +31,48 @@ install: youtube-dl youtube-dl.1 youtube-dl.bash-completion
|
|||||||
install -m 644 youtube-dl.bash-completion $(DESTDIR)$(SYSCONFDIR)/bash_completion.d/youtube-dl
|
install -m 644 youtube-dl.bash-completion $(DESTDIR)$(SYSCONFDIR)/bash_completion.d/youtube-dl
|
||||||
|
|
||||||
test:
|
test:
|
||||||
nosetests2 --nocapture test
|
#nosetests --with-coverage --cover-package=youtube_dl --cover-html --verbose --processes 4 test
|
||||||
|
nosetests --verbose test
|
||||||
|
|
||||||
.PHONY: all clean install test README.md youtube-dl.bash-completion
|
tar: youtube-dl.tar.gz
|
||||||
# TODO un-phony README.md and youtube-dl.bash_completion by reading from .in files and generating from them
|
|
||||||
|
.PHONY: all clean install test tar bash-completion pypi-files
|
||||||
|
|
||||||
|
pypi-files: youtube-dl.bash-completion README.txt youtube-dl.1
|
||||||
|
|
||||||
youtube-dl: youtube_dl/*.py
|
youtube-dl: youtube_dl/*.py
|
||||||
zip --quiet --junk-paths youtube-dl youtube_dl/*.py
|
zip --quiet youtube-dl youtube_dl/*.py
|
||||||
echo '#!/usr/bin/env python' > youtube-dl
|
zip --quiet --junk-paths youtube-dl youtube_dl/__main__.py
|
||||||
|
echo '#!$(PYTHON)' > youtube-dl
|
||||||
cat youtube-dl.zip >> youtube-dl
|
cat youtube-dl.zip >> youtube-dl
|
||||||
rm youtube-dl.zip
|
rm youtube-dl.zip
|
||||||
chmod a+x youtube-dl
|
chmod a+x youtube-dl
|
||||||
|
|
||||||
youtube-dl.exe: youtube_dl/*.py
|
|
||||||
bash devscripts/wine-py2exe.sh build_exe.py
|
|
||||||
|
|
||||||
README.md: youtube_dl/*.py
|
README.md: youtube_dl/*.py
|
||||||
@options=$$(COLUMNS=80 python -m youtube_dl --help | sed -e '1,/.*General Options.*/ d' -e 's/^\W\{2\}\(\w\)/## \1/') && \
|
COLUMNS=80 python -m youtube_dl --help | python devscripts/make_readme.py
|
||||||
header=$$(sed -e '/.*# OPTIONS/,$$ d' README.md) && \
|
|
||||||
footer=$$(sed -e '1,/.*# CONFIGURATION/ d' README.md) && \
|
README.txt: README.md
|
||||||
echo "$${header}" > README.md && \
|
pandoc -f markdown -t plain README.md -o README.txt
|
||||||
echo >> README.md && \
|
|
||||||
echo '# OPTIONS' >> README.md && \
|
|
||||||
echo "$${options}" >> README.md&& \
|
|
||||||
echo >> README.md && \
|
|
||||||
echo '# CONFIGURATION' >> README.md && \
|
|
||||||
echo "$${footer}" >> README.md
|
|
||||||
|
|
||||||
youtube-dl.1: README.md
|
youtube-dl.1: README.md
|
||||||
pandoc -s -w man README.md -o youtube-dl.1
|
pandoc -s -f markdown -t man README.md -o youtube-dl.1
|
||||||
|
|
||||||
youtube-dl.bash-completion: README.md
|
youtube-dl.bash-completion: youtube_dl/*.py devscripts/bash-completion.in
|
||||||
@options=`egrep -o '(--[a-z-]+) ' README.md | sort -u | xargs echo` && \
|
python devscripts/bash-completion.py
|
||||||
content=`sed "s/opts=\"[^\"]*\"/opts=\"$${options}\"/g" youtube-dl.bash-completion` && \
|
|
||||||
echo "$${content}" > youtube-dl.bash-completion
|
|
||||||
|
|
||||||
LATEST_VERSION: youtube_dl/__init__.py
|
bash-completion: youtube-dl.bash-completion
|
||||||
python -m youtube_dl --version > LATEST_VERSION
|
|
||||||
|
youtube-dl.tar.gz: youtube-dl README.md README.txt youtube-dl.1 youtube-dl.bash-completion
|
||||||
|
@tar -czf youtube-dl.tar.gz --transform "s|^|youtube-dl/|" --owner 0 --group 0 \
|
||||||
|
--exclude '*.DS_Store' \
|
||||||
|
--exclude '*.kate-swp' \
|
||||||
|
--exclude '*.pyc' \
|
||||||
|
--exclude '*.pyo' \
|
||||||
|
--exclude '*~' \
|
||||||
|
--exclude '__pycache' \
|
||||||
|
--exclude '.git' \
|
||||||
|
-- \
|
||||||
|
bin devscripts test youtube_dl \
|
||||||
|
CHANGELOG LICENSE README.md README.txt \
|
||||||
|
Makefile MANIFEST.in youtube-dl.1 youtube-dl.bash-completion setup.py \
|
||||||
|
youtube-dl
|
||||||
|
127
README.md
127
README.md
@@ -9,8 +9,8 @@ youtube-dl
|
|||||||
# 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
|
||||||
YouTube.com and a few more sites. It requires the Python interpreter, version
|
YouTube.com and a few more sites. It requires the Python interpreter, version
|
||||||
2.x (x being at least 6), and it is not platform specific. It should work in
|
2.6, 2.7, or 3.3+, and it is not platform specific. It should work on
|
||||||
your Unix box, in Windows or in Mac OS X. It is released to the public domain,
|
your Unix box, on Windows or on Mac OS X. It is released to the public domain,
|
||||||
which means you can modify it, redistribute it or use it however you like.
|
which means you can modify it, redistribute it or use it however you like.
|
||||||
|
|
||||||
# OPTIONS
|
# OPTIONS
|
||||||
@@ -18,17 +18,21 @@ which means you can modify it, redistribute it or use it however you like.
|
|||||||
--version print program version and exit
|
--version print program version and exit
|
||||||
-U, --update update this program to latest version
|
-U, --update update this program to latest version
|
||||||
-i, --ignore-errors continue on download errors
|
-i, --ignore-errors continue on download errors
|
||||||
-r, --rate-limit LIMIT download rate limit (e.g. 50k or 44.6m)
|
-r, --rate-limit LIMIT maximum download rate (e.g. 50k or 44.6m)
|
||||||
-R, --retries RETRIES number of retries (default is 10)
|
-R, --retries RETRIES number of retries (default is 10)
|
||||||
--buffer-size SIZE size of download buffer (e.g. 1024 or 16k) (default
|
--buffer-size SIZE size of download buffer (e.g. 1024 or 16k)
|
||||||
is 1024)
|
(default is 1024)
|
||||||
--no-resize-buffer do not automatically adjust the buffer size. By
|
--no-resize-buffer do not automatically adjust the buffer size. By
|
||||||
default, the buffer size is automatically resized
|
default, the buffer size is automatically resized
|
||||||
from an initial value of SIZE.
|
from an initial value of SIZE.
|
||||||
--dump-user-agent display the current browser identification
|
--dump-user-agent display the current browser identification
|
||||||
--user-agent UA specify a custom user agent
|
--user-agent UA specify a custom user agent
|
||||||
|
--referer REF specify a custom referer, use if the video access
|
||||||
|
is restricted to one domain
|
||||||
--list-extractors List all supported extractors and the URLs they
|
--list-extractors List all supported extractors and the URLs they
|
||||||
would handle
|
would handle
|
||||||
|
--proxy URL Use the specified HTTP/HTTPS proxy
|
||||||
|
--no-check-certificate Suppress HTTPS certificate validation.
|
||||||
|
|
||||||
## 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)
|
||||||
@@ -38,20 +42,36 @@ which means you can modify it, redistribute it or use it however you like.
|
|||||||
--reject-title REGEX skip download for matching titles (regex or
|
--reject-title REGEX skip download for matching titles (regex or
|
||||||
caseless sub-string)
|
caseless sub-string)
|
||||||
--max-downloads NUMBER Abort after downloading NUMBER files
|
--max-downloads NUMBER Abort after downloading NUMBER files
|
||||||
|
--min-filesize SIZE Do not download any videos smaller than 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
|
||||||
|
--datebefore DATE download only videos uploaded before this date
|
||||||
|
--dateafter DATE download only videos uploaded after this date
|
||||||
|
|
||||||
## Filesystem Options:
|
## Filesystem Options:
|
||||||
-t, --title use title in file name
|
-t, --title use title in file name (default)
|
||||||
--id use video ID in file name
|
--id use only video ID in file name
|
||||||
-l, --literal [deprecated] alias of --title
|
-l, --literal [deprecated] alias of --title
|
||||||
-A, --auto-number number downloaded files starting from 00000
|
-A, --auto-number number downloaded files starting from 00000
|
||||||
-o, --output TEMPLATE output filename template. Use %(title)s to get the
|
-o, --output TEMPLATE output filename template. Use %(title)s to get
|
||||||
title, %(uploader)s for the uploader name,
|
the title, %(uploader)s for the uploader name,
|
||||||
%(autonumber)s to get an automatically incremented
|
%(uploader_id)s for the uploader nickname if
|
||||||
number, %(ext)s for the filename extension,
|
different, %(autonumber)s to get an automatically
|
||||||
%(upload_date)s for the upload date (YYYYMMDD),
|
incremented number, %(ext)s for the filename
|
||||||
%(extractor)s for the provider (youtube, metacafe,
|
extension, %(upload_date)s for the upload date
|
||||||
etc), %(id)s for the video id and %% for a literal
|
(YYYYMMDD), %(extractor)s for the provider
|
||||||
percent. Use - to output to stdout.
|
(youtube, metacafe, etc), %(id)s for the video id
|
||||||
|
, %(playlist)s for the playlist the video is in,
|
||||||
|
%(playlist_index)s for the position in the
|
||||||
|
playlist and %% for a literal percent. Use - to
|
||||||
|
output to stdout. Can also be used to download to
|
||||||
|
a different directory, for example with -o '/my/d
|
||||||
|
ownloads/%(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
|
||||||
|
--autonumber option is given
|
||||||
--restrict-filenames Restrict filenames to only ASCII characters, and
|
--restrict-filenames Restrict filenames to only ASCII characters, and
|
||||||
avoid "&" and spaces in filenames
|
avoid "&" and spaces in filenames
|
||||||
-a, --batch-file FILE file containing URLs to download ('-' for stdin)
|
-a, --batch-file FILE file containing URLs to download ('-' for stdin)
|
||||||
@@ -61,37 +81,50 @@ which means you can modify it, redistribute it or use it however you like.
|
|||||||
from beginning)
|
from beginning)
|
||||||
--cookies FILE file to read cookies from and dump cookie jar in
|
--cookies FILE file to read cookies from and dump cookie jar in
|
||||||
--no-part do not use .part files
|
--no-part do not use .part files
|
||||||
--no-mtime do not use the Last-modified header to set the file
|
--no-mtime do not use the Last-modified header to set the
|
||||||
modification time
|
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-thumbnail write thumbnail image to disk
|
||||||
|
|
||||||
## Verbosity / Simulation Options:
|
## Verbosity / Simulation Options:
|
||||||
-q, --quiet activates quiet mode
|
-q, --quiet activates quiet mode
|
||||||
-s, --simulate do not download the video and do not write anything
|
-s, --simulate do not download the video and do not write
|
||||||
to disk
|
anything to disk
|
||||||
--skip-download do not download the video
|
--skip-download do not download the video
|
||||||
-g, --get-url simulate, quiet but print URL
|
-g, --get-url simulate, quiet but print URL
|
||||||
-e, --get-title simulate, quiet but print title
|
-e, --get-title simulate, quiet but print title
|
||||||
|
--get-id simulate, quiet but print id
|
||||||
--get-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-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
|
||||||
|
--newline output progress bar as new lines
|
||||||
--no-progress do not print progress bar
|
--no-progress do not print progress bar
|
||||||
--console-title display progress in console titlebar
|
--console-title display progress in console titlebar
|
||||||
-v, --verbose print various debugging information
|
-v, --verbose print various debugging information
|
||||||
|
--dump-intermediate-pages print downloaded pages to debug problems(very
|
||||||
|
verbose)
|
||||||
|
|
||||||
## Video Format Options:
|
## Video Format Options:
|
||||||
-f, --format FORMAT video format code
|
-f, --format FORMAT video format code, specifiy the order of
|
||||||
|
preference using slashes: "-f 22/17/18"
|
||||||
--all-formats download all available video formats
|
--all-formats download all available video formats
|
||||||
--prefer-free-formats prefer free video formats unless a specific one is
|
--prefer-free-formats prefer free video formats unless a specific one
|
||||||
requested
|
is requested
|
||||||
--max-quality FORMAT highest quality format to download
|
--max-quality FORMAT highest quality format to download
|
||||||
-F, --list-formats list all available formats (currently youtube only)
|
-F, --list-formats list all available formats (currently youtube
|
||||||
--write-srt write video closed captions to a .srt file
|
only)
|
||||||
|
--write-sub write subtitle file (currently youtube only)
|
||||||
|
--only-sub [deprecated] alias of --skip-download
|
||||||
|
--all-subs downloads all the available subtitles of the
|
||||||
|
video (currently youtube only)
|
||||||
|
--list-subs lists all available subtitles for the video
|
||||||
(currently youtube only)
|
(currently youtube only)
|
||||||
--srt-lang LANG language of the closed captions to download
|
--sub-format LANG subtitle format [srt/sbv] (default=srt)
|
||||||
(optional) use IETF language tags like 'en'
|
(currently youtube only)
|
||||||
|
--sub-lang LANG language of the subtitles to download (optional)
|
||||||
|
use IETF language tags like 'en'
|
||||||
|
|
||||||
## Authentication Options:
|
## Authentication Options:
|
||||||
-u, --username USERNAME account username
|
-u, --username USERNAME account username
|
||||||
@@ -101,17 +134,21 @@ which means you can modify it, redistribute it or use it however you like.
|
|||||||
## Post-processing Options:
|
## Post-processing Options:
|
||||||
-x, --extract-audio convert video files to audio-only files (requires
|
-x, --extract-audio convert video files to audio-only files (requires
|
||||||
ffmpeg or avconv and ffprobe or avprobe)
|
ffmpeg or avconv and ffprobe or avprobe)
|
||||||
--audio-format FORMAT "best", "aac", "vorbis", "mp3", "m4a", or "wav";
|
--audio-format FORMAT "best", "aac", "vorbis", "mp3", "m4a", "opus", or
|
||||||
best by default
|
"wav"; best by default
|
||||||
--audio-quality QUALITY ffmpeg/avconv audio quality specification, insert a
|
--audio-quality QUALITY ffmpeg/avconv audio quality specification, insert
|
||||||
value between 0 (better) and 9 (worse) for VBR or a
|
a value between 0 (better) and 9 (worse) for VBR
|
||||||
specific bitrate like 128K (default 5)
|
or a specific bitrate like 128K (default 5)
|
||||||
|
--recode-video FORMAT Encode the video to another format if necessary
|
||||||
|
(currently supported: mp4|flv|ogg|webm)
|
||||||
-k, --keep-video keeps the video file on disk after the post-
|
-k, --keep-video keeps the video file on disk after the post-
|
||||||
processing; the video is erased by default
|
processing; the video is erased by default
|
||||||
|
--no-post-overwrites do not overwrite post-processed files; the post-
|
||||||
|
processed files are overwritten by default
|
||||||
|
|
||||||
# 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 `~/.local/config/youtube-dl.conf`.
|
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.conf`.
|
||||||
|
|
||||||
# OUTPUT TEMPLATE
|
# OUTPUT TEMPLATE
|
||||||
|
|
||||||
@@ -125,6 +162,8 @@ The `-o` option allows users to indicate a template for the output file names. T
|
|||||||
- `ext`: The sequence will be replaced by the appropriate extension (like flv or mp4).
|
- `ext`: The sequence will be replaced by the appropriate extension (like flv or mp4).
|
||||||
- `epoch`: The sequence will be replaced by the Unix epoch when creating the file.
|
- `epoch`: The sequence will be replaced by the Unix epoch when creating the file.
|
||||||
- `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_index`: The index of the video in the playlist, a five-digit number.
|
||||||
|
|
||||||
The current default template is `%(id)s.%(ext)s`, but that will be switchted to `%(title)s-%(id)s.%(ext)s` (which can be requested with `-t` at the moment).
|
The current default template is `%(id)s.%(ext)s`, but that will be switchted to `%(title)s-%(id)s.%(ext)s` (which can be requested with `-t` at the moment).
|
||||||
|
|
||||||
@@ -135,6 +174,19 @@ In some cases, you don't want special characters such as 中, spaces, or &, such
|
|||||||
$ youtube-dl --get-filename -o "%(title)s.%(ext)s" BaW_jenozKc --restrict-filenames
|
$ youtube-dl --get-filename -o "%(title)s.%(ext)s" BaW_jenozKc --restrict-filenames
|
||||||
youtube-dl_test_video_.mp4 # A simple file name
|
youtube-dl_test_video_.mp4 # A simple file name
|
||||||
|
|
||||||
|
# VIDEO SELECTION
|
||||||
|
|
||||||
|
Videos can be filtered by their upload date using the options `--date`, `--datebefore` or `--dateafter`, they accept dates in two formats:
|
||||||
|
|
||||||
|
- Absolute dates: Dates in the format `YYYYMMDD`.
|
||||||
|
- Relative dates: Dates in the format `(now|today)[+-][0-9](day|week|month|year)(s)?`
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
$ youtube-dl --dateafter now-6months #will only download the videos uploaded in the last 6 months
|
||||||
|
$ youtube-dl --date 19700101 #will only download the videos uploaded in January 1, 1970
|
||||||
|
$ youtube-dl --dateafter 20000101 --datebefore 20100101 #will only download the videos uploaded between 2000 and 2010
|
||||||
|
|
||||||
# FAQ
|
# FAQ
|
||||||
|
|
||||||
### Can you please put the -b option back?
|
### Can you please put the -b option back?
|
||||||
@@ -170,14 +222,6 @@ The error
|
|||||||
|
|
||||||
means you're using an outdated version of Python. Please update to Python 2.6 or 2.7.
|
means you're using an outdated version of Python. Please update to Python 2.6 or 2.7.
|
||||||
|
|
||||||
To run youtube-dl under Python 2.5, you'll have to manually check it out like this:
|
|
||||||
|
|
||||||
git clone git://github.com/rg3/youtube-dl.git
|
|
||||||
cd youtube-dl
|
|
||||||
python -m youtube_dl --help
|
|
||||||
|
|
||||||
Please note that Python 2.5 is not supported anymore.
|
|
||||||
|
|
||||||
### What is this binary file? Where has the code gone?
|
### What is this binary file? Where has the code gone?
|
||||||
|
|
||||||
Since June 2012 (#342) youtube-dl is packed as an executable zipfile, simply unzip it (might need renaming to `youtube-dl.zip` first on some systems) or clone the git repository, as laid out above. If you modify the code, you can run it by executing the `__main__.py` file. To recompile the executable, run `make youtube-dl`.
|
Since June 2012 (#342) youtube-dl is packed as an executable zipfile, simply unzip it (might need renaming to `youtube-dl.zip` first on some systems) or clone the git repository, as laid out above. If you modify the code, you can run it by executing the `__main__.py` file. To recompile the executable, run `make youtube-dl`.
|
||||||
@@ -199,6 +243,9 @@ Bugs and suggestions should be reported at: <https://github.com/rg3/youtube-dl/i
|
|||||||
Please include:
|
Please include:
|
||||||
|
|
||||||
* Your exact command line, like `youtube-dl -t "http://www.youtube.com/watch?v=uHlDtZ6Oc3s&feature=channel_video_title"`. A common mistake is not to escape the `&`. Putting URLs in quotes should solve this problem.
|
* Your exact command line, like `youtube-dl -t "http://www.youtube.com/watch?v=uHlDtZ6Oc3s&feature=channel_video_title"`. A common mistake is not to escape the `&`. Putting URLs in quotes should solve this problem.
|
||||||
|
* If possible re-run the command with `--verbose`, and include the full output, it is really helpful to us.
|
||||||
* The output of `youtube-dl --version`
|
* The output of `youtube-dl --version`
|
||||||
* The output of `python --version`
|
* The output of `python --version`
|
||||||
* The name and version of your Operating System ("Ubuntu 11.04 x64" or "Windows 7 x64" is usually enough).
|
* The name and version of your Operating System ("Ubuntu 11.04 x64" or "Windows 7 x64" is usually enough).
|
||||||
|
|
||||||
|
For discussions, join us in the irc channel #youtube-dl on freenode.
|
||||||
|
6
bin/youtube-dl
Executable file
6
bin/youtube-dl
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import youtube_dl
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
youtube_dl.main()
|
48
build_exe.py
48
build_exe.py
@@ -1,48 +0,0 @@
|
|||||||
from distutils.core import setup
|
|
||||||
import py2exe
|
|
||||||
import sys, os
|
|
||||||
|
|
||||||
"""This will create an exe that needs Microsoft Visual C++ 2008 Redistributable Package"""
|
|
||||||
|
|
||||||
# If run without args, build executables
|
|
||||||
if len(sys.argv) == 1:
|
|
||||||
sys.argv.append("py2exe")
|
|
||||||
|
|
||||||
# os.chdir(os.path.dirname(os.path.abspath(sys.argv[0]))) # conflict with wine-py2exe.sh
|
|
||||||
sys.path.append('./youtube_dl')
|
|
||||||
|
|
||||||
options = {
|
|
||||||
"bundle_files": 1,
|
|
||||||
"compressed": 1,
|
|
||||||
"optimize": 2,
|
|
||||||
"dist_dir": '.',
|
|
||||||
"dll_excludes": ['w9xpopen.exe']
|
|
||||||
}
|
|
||||||
|
|
||||||
console = [{
|
|
||||||
"script":"./youtube_dl/__main__.py",
|
|
||||||
"dest_base": "youtube-dl",
|
|
||||||
}]
|
|
||||||
|
|
||||||
init_file = open('./youtube_dl/__init__.py')
|
|
||||||
for line in init_file.readlines():
|
|
||||||
if line.startswith('__version__'):
|
|
||||||
version = line[11:].strip(" ='\n")
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
version = ''
|
|
||||||
|
|
||||||
setup(name='youtube-dl',
|
|
||||||
version=version,
|
|
||||||
description='Small command-line program to download videos from YouTube.com and other video sites',
|
|
||||||
url='https://github.com/rg3/youtube-dl',
|
|
||||||
packages=['youtube_dl'],
|
|
||||||
|
|
||||||
console = console,
|
|
||||||
options = {"py2exe": options},
|
|
||||||
zipfile = None,
|
|
||||||
)
|
|
||||||
|
|
||||||
import shutil
|
|
||||||
shutil.rmtree("build")
|
|
||||||
|
|
14
devscripts/bash-completion.in
Normal file
14
devscripts/bash-completion.in
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
__youtube-dl()
|
||||||
|
{
|
||||||
|
local cur prev opts
|
||||||
|
COMPREPLY=()
|
||||||
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||||
|
opts="{{flags}}"
|
||||||
|
|
||||||
|
if [[ ${cur} == * ]] ; then
|
||||||
|
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
complete -F __youtube-dl youtube-dl
|
26
devscripts/bash-completion.py
Executable file
26
devscripts/bash-completion.py
Executable file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
import os
|
||||||
|
from os.path import dirname as dirn
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.append(dirn(dirn((os.path.abspath(__file__)))))
|
||||||
|
import youtube_dl
|
||||||
|
|
||||||
|
BASH_COMPLETION_FILE = "youtube-dl.bash-completion"
|
||||||
|
BASH_COMPLETION_TEMPLATE = "devscripts/bash-completion.in"
|
||||||
|
|
||||||
|
def build_completion(opt_parser):
|
||||||
|
opts_flag = []
|
||||||
|
for group in opt_parser.option_groups:
|
||||||
|
for option in group.option_list:
|
||||||
|
#for every long flag
|
||||||
|
opts_flag.append(option.get_opt_string())
|
||||||
|
with open(BASH_COMPLETION_TEMPLATE) as f:
|
||||||
|
template = f.read()
|
||||||
|
with open(BASH_COMPLETION_FILE, "w") as f:
|
||||||
|
#just using the special char
|
||||||
|
filled_template = template.replace("{{flags}}", " ".join(opts_flag))
|
||||||
|
f.write(filled_template)
|
||||||
|
|
||||||
|
parser = youtube_dl.parseOpts()[0]
|
||||||
|
build_completion(parser)
|
33
devscripts/gh-pages/add-version.py
Executable file
33
devscripts/gh-pages/add-version.py
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import hashlib
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
if len(sys.argv) <= 1:
|
||||||
|
print('Specify the version number as parameter')
|
||||||
|
sys.exit()
|
||||||
|
version = sys.argv[1]
|
||||||
|
|
||||||
|
with open('update/LATEST_VERSION', 'w') as f:
|
||||||
|
f.write(version)
|
||||||
|
|
||||||
|
versions_info = json.load(open('update/versions.json'))
|
||||||
|
if 'signature' in versions_info:
|
||||||
|
del versions_info['signature']
|
||||||
|
|
||||||
|
new_version = {}
|
||||||
|
|
||||||
|
filenames = {'bin': 'youtube-dl', 'exe': 'youtube-dl.exe', 'tar': 'youtube-dl-%s.tar.gz' % version}
|
||||||
|
for key, filename in filenames.items():
|
||||||
|
print('Downloading and checksumming %s...' %filename)
|
||||||
|
url = 'http://youtube-dl.org/downloads/%s/%s' % (version, filename)
|
||||||
|
data = urllib.request.urlopen(url).read()
|
||||||
|
sha256sum = hashlib.sha256(data).hexdigest()
|
||||||
|
new_version[key] = (url, sha256sum)
|
||||||
|
|
||||||
|
versions_info['versions'][version] = new_version
|
||||||
|
versions_info['latest'] = version
|
||||||
|
|
||||||
|
json.dump(versions_info, open('update/versions.json', 'w'), indent=4, sort_keys=True)
|
32
devscripts/gh-pages/generate-download.py
Executable file
32
devscripts/gh-pages/generate-download.py
Executable file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import hashlib
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import urllib.request
|
||||||
|
import json
|
||||||
|
|
||||||
|
versions_info = json.load(open('update/versions.json'))
|
||||||
|
version = versions_info['latest']
|
||||||
|
URL = versions_info['versions'][version]['bin'][0]
|
||||||
|
|
||||||
|
data = urllib.request.urlopen(URL).read()
|
||||||
|
|
||||||
|
# Read template page
|
||||||
|
with open('download.html.in', 'r', encoding='utf-8') as tmplf:
|
||||||
|
template = tmplf.read()
|
||||||
|
|
||||||
|
md5sum = hashlib.md5(data).hexdigest()
|
||||||
|
sha1sum = hashlib.sha1(data).hexdigest()
|
||||||
|
sha256sum = hashlib.sha256(data).hexdigest()
|
||||||
|
template = template.replace('@PROGRAM_VERSION@', version)
|
||||||
|
template = template.replace('@PROGRAM_URL@', URL)
|
||||||
|
template = template.replace('@PROGRAM_MD5SUM@', md5sum)
|
||||||
|
template = template.replace('@PROGRAM_SHA1SUM@', sha1sum)
|
||||||
|
template = template.replace('@PROGRAM_SHA256SUM@', sha256sum)
|
||||||
|
template = template.replace('@EXE_URL@', versions_info['versions'][version]['exe'][0])
|
||||||
|
template = template.replace('@EXE_SHA256SUM@', versions_info['versions'][version]['exe'][1])
|
||||||
|
template = template.replace('@TAR_URL@', versions_info['versions'][version]['tar'][0])
|
||||||
|
template = template.replace('@TAR_SHA256SUM@', versions_info['versions'][version]['tar'][1])
|
||||||
|
with open('download.html', 'w', encoding='utf-8') as dlf:
|
||||||
|
dlf.write(template)
|
32
devscripts/gh-pages/sign-versions.py
Executable file
32
devscripts/gh-pages/sign-versions.py
Executable file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import rsa
|
||||||
|
import json
|
||||||
|
from binascii import hexlify
|
||||||
|
|
||||||
|
try:
|
||||||
|
input = raw_input
|
||||||
|
except NameError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
versions_info = json.load(open('update/versions.json'))
|
||||||
|
if 'signature' in versions_info:
|
||||||
|
del versions_info['signature']
|
||||||
|
|
||||||
|
print('Enter the PKCS1 private key, followed by a blank line:')
|
||||||
|
privkey = b''
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
line = input()
|
||||||
|
except EOFError:
|
||||||
|
break
|
||||||
|
if line == '':
|
||||||
|
break
|
||||||
|
privkey += line.encode('ascii') + b'\n'
|
||||||
|
privkey = rsa.PrivateKey.load_pkcs1(privkey)
|
||||||
|
|
||||||
|
signature = hexlify(rsa.pkcs1.sign(json.dumps(versions_info, sort_keys=True).encode('utf-8'), privkey, 'SHA-256')).decode()
|
||||||
|
print('signature: ' + signature)
|
||||||
|
|
||||||
|
versions_info['signature'] = signature
|
||||||
|
json.dump(versions_info, open('update/versions.json', 'w'), indent=4, sort_keys=True)
|
21
devscripts/gh-pages/update-copyright.py
Executable file
21
devscripts/gh-pages/update-copyright.py
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# coding: utf-8
|
||||||
|
|
||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import glob
|
||||||
|
import io # For Python 2 compatibilty
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
year = str(datetime.datetime.now().year)
|
||||||
|
for fn in glob.glob('*.html*'):
|
||||||
|
with io.open(fn, encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
newc = re.sub(u'(?P<copyright>Copyright © 2006-)(?P<year>[0-9]{4})', u'Copyright © 2006-' + year, content)
|
||||||
|
if content != newc:
|
||||||
|
tmpFn = fn + '.part'
|
||||||
|
with io.open(tmpFn, 'wt', encoding='utf-8') as outf:
|
||||||
|
outf.write(newc)
|
||||||
|
os.rename(tmpFn, fn)
|
57
devscripts/gh-pages/update-feed.py
Executable file
57
devscripts/gh-pages/update-feed.py
Executable file
@@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
atom_template=textwrap.dedent("""\
|
||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<atom:feed xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
|
<atom:title>youtube-dl releases</atom:title>
|
||||||
|
<atom:id>youtube-dl-updates-feed</atom:id>
|
||||||
|
<atom:updated>@TIMESTAMP@</atom:updated>
|
||||||
|
@ENTRIES@
|
||||||
|
</atom:feed>""")
|
||||||
|
|
||||||
|
entry_template=textwrap.dedent("""
|
||||||
|
<atom:entry>
|
||||||
|
<atom:id>youtube-dl-@VERSION@</atom:id>
|
||||||
|
<atom:title>New version @VERSION@</atom:title>
|
||||||
|
<atom:link href="http://rg3.github.io/youtube-dl" />
|
||||||
|
<atom:content type="xhtml">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
Downloads available at <a href="http://youtube-dl.org/downloads/@VERSION@/">http://youtube-dl.org/downloads/@VERSION@/</a>
|
||||||
|
</div>
|
||||||
|
</atom:content>
|
||||||
|
<atom:author>
|
||||||
|
<atom:name>The youtube-dl maintainers</atom:name>
|
||||||
|
</atom:author>
|
||||||
|
<atom:updated>@TIMESTAMP@</atom:updated>
|
||||||
|
</atom:entry>
|
||||||
|
""")
|
||||||
|
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
now_iso = now.isoformat()
|
||||||
|
|
||||||
|
atom_template = atom_template.replace('@TIMESTAMP@',now_iso)
|
||||||
|
|
||||||
|
entries=[]
|
||||||
|
|
||||||
|
versions_info = json.load(open('update/versions.json'))
|
||||||
|
versions = list(versions_info['versions'].keys())
|
||||||
|
versions.sort()
|
||||||
|
|
||||||
|
for v in versions:
|
||||||
|
entry = entry_template.replace('@TIMESTAMP@',v.replace('.','-'))
|
||||||
|
entry = entry.replace('@VERSION@',v)
|
||||||
|
entries.append(entry)
|
||||||
|
|
||||||
|
entries_str = textwrap.indent(''.join(entries), '\t')
|
||||||
|
atom_template = atom_template.replace('@ENTRIES@', entries_str)
|
||||||
|
|
||||||
|
with open('update/releases.atom','w',encoding='utf-8') as atom_file:
|
||||||
|
atom_file.write(atom_template)
|
||||||
|
|
||||||
|
|
20
devscripts/make_readme.py
Executable file
20
devscripts/make_readme.py
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
import sys
|
||||||
|
import re
|
||||||
|
|
||||||
|
README_FILE = 'README.md'
|
||||||
|
helptext = sys.stdin.read()
|
||||||
|
|
||||||
|
with open(README_FILE) as f:
|
||||||
|
oldreadme = f.read()
|
||||||
|
|
||||||
|
header = oldreadme[:oldreadme.index('# OPTIONS')]
|
||||||
|
footer = oldreadme[oldreadme.index('# CONFIGURATION'):]
|
||||||
|
|
||||||
|
options = helptext[helptext.index(' General Options:')+19:]
|
||||||
|
options = re.sub(r'^ (\w.+)$', r'## \1', options, flags=re.M)
|
||||||
|
options = '# OPTIONS\n' + options + '\n'
|
||||||
|
|
||||||
|
with open(README_FILE, 'w') as f:
|
||||||
|
f.write(header)
|
||||||
|
f.write(options)
|
||||||
|
f.write(footer)
|
@@ -1,11 +1,92 @@
|
|||||||
#!/bin/sh
|
#!/bin/bash
|
||||||
|
|
||||||
|
# IMPORTANT: the following assumptions are made
|
||||||
|
# * the GH repo is on the origin remote
|
||||||
|
# * the gh-pages branch is named so locally
|
||||||
|
# * the git config user.signingkey is properly set
|
||||||
|
|
||||||
|
# You will need
|
||||||
|
# pip install coverage nose rsa
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
# release notes
|
||||||
|
# make hash on local files
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
if [ -z "$1" ]; then echo "ERROR: specify version number like this: $0 1994.09.06"; exit 1; fi
|
if [ -z "$1" ]; then echo "ERROR: specify version number like this: $0 1994.09.06"; exit 1; fi
|
||||||
version="$1"
|
version="$1"
|
||||||
if [ ! -z "`git tag | grep "$version"`" ]; then echo 'ERROR: version already present'; exit 1; fi
|
if [ ! -z "`git tag | grep "$version"`" ]; then echo 'ERROR: version already present'; exit 1; fi
|
||||||
if [ ! -z "`git status --porcelain`" ]; then echo 'ERROR: the working directory is not clean; commit or stash changes'; exit 1; fi
|
if [ ! -z "`git status --porcelain | grep -v CHANGELOG`" ]; then echo 'ERROR: the working directory is not clean; commit or stash changes'; exit 1; fi
|
||||||
sed -i "s/__version__ = '.*'/__version__ = '$version'/" youtube_dl/__init__.py
|
if [ ! -f "updates_key.pem" ]; then echo 'ERROR: updates_key.pem missing'; exit 1; fi
|
||||||
make all
|
|
||||||
git add -A
|
/bin/echo -e "\n### First of all, testing..."
|
||||||
|
make cleanall
|
||||||
|
nosetests --verbose --with-coverage --cover-package=youtube_dl --cover-html test --stop || exit 1
|
||||||
|
|
||||||
|
/bin/echo -e "\n### Changing version in version.py..."
|
||||||
|
sed -i "s/__version__ = '.*'/__version__ = '$version'/" youtube_dl/version.py
|
||||||
|
|
||||||
|
/bin/echo -e "\n### Committing CHANGELOG README.md and youtube_dl/version.py..."
|
||||||
|
make README.md
|
||||||
|
git add CHANGELOG README.md youtube_dl/version.py
|
||||||
git commit -m "release $version"
|
git commit -m "release $version"
|
||||||
git tag -m "Release $version" "$version"
|
|
||||||
|
/bin/echo -e "\n### Now tagging, signing and pushing..."
|
||||||
|
git tag -s -m "Release $version" "$version"
|
||||||
|
git show "$version"
|
||||||
|
read -p "Is it good, can I push? (y/n) " -n 1
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1; fi
|
||||||
|
echo
|
||||||
|
MASTER=$(git rev-parse --abbrev-ref HEAD)
|
||||||
|
git push origin $MASTER:master
|
||||||
|
git push origin "$version"
|
||||||
|
|
||||||
|
/bin/echo -e "\n### OK, now it is time to build the binaries..."
|
||||||
|
REV=$(git rev-parse HEAD)
|
||||||
|
make youtube-dl youtube-dl.tar.gz
|
||||||
|
wget "http://jeromelaheurte.net:8142/download/rg3/youtube-dl/youtube-dl.exe?rev=$REV" -O youtube-dl.exe || \
|
||||||
|
wget "http://jeromelaheurte.net:8142/build/rg3/youtube-dl/youtube-dl.exe?rev=$REV" -O youtube-dl.exe
|
||||||
|
mkdir -p "build/$version"
|
||||||
|
mv youtube-dl youtube-dl.exe "build/$version"
|
||||||
|
mv youtube-dl.tar.gz "build/$version/youtube-dl-$version.tar.gz"
|
||||||
|
RELEASE_FILES="youtube-dl youtube-dl.exe youtube-dl-$version.tar.gz"
|
||||||
|
(cd build/$version/ && md5sum $RELEASE_FILES > MD5SUMS)
|
||||||
|
(cd build/$version/ && sha1sum $RELEASE_FILES > SHA1SUMS)
|
||||||
|
(cd build/$version/ && sha256sum $RELEASE_FILES > SHA2-256SUMS)
|
||||||
|
(cd build/$version/ && sha512sum $RELEASE_FILES > SHA2-512SUMS)
|
||||||
|
git checkout HEAD -- youtube-dl youtube-dl.exe
|
||||||
|
|
||||||
|
/bin/echo -e "\n### Signing and uploading the new binaries to youtube-dl.org..."
|
||||||
|
for f in $RELEASE_FILES; do gpg --detach-sig "build/$version/$f"; done
|
||||||
|
scp -r "build/$version" ytdl@youtube-dl.org:html/downloads/
|
||||||
|
|
||||||
|
/bin/echo -e "\n### Now switching to gh-pages..."
|
||||||
|
git clone --branch gh-pages --single-branch . build/gh-pages
|
||||||
|
ROOT=$(pwd)
|
||||||
|
(
|
||||||
|
set -e
|
||||||
|
ORIGIN_URL=$(git config --get remote.origin.url)
|
||||||
|
cd build/gh-pages
|
||||||
|
"$ROOT/devscripts/gh-pages/add-version.py" $version
|
||||||
|
"$ROOT/devscripts/gh-pages/update-feed.py"
|
||||||
|
"$ROOT/devscripts/gh-pages/sign-versions.py" < "$ROOT/updates_key.pem"
|
||||||
|
"$ROOT/devscripts/gh-pages/generate-download.py"
|
||||||
|
"$ROOT/devscripts/gh-pages/update-copyright.py"
|
||||||
|
git add *.html *.html.in update
|
||||||
|
git commit -m "release $version"
|
||||||
|
git show HEAD
|
||||||
|
read -p "Is it good, can I push? (y/n) " -n 1
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1; fi
|
||||||
|
echo
|
||||||
|
git push "$ROOT" gh-pages
|
||||||
|
git push "$ORIGIN_URL" gh-pages
|
||||||
|
)
|
||||||
|
rm -rf build
|
||||||
|
|
||||||
|
make pypi-files
|
||||||
|
echo "Uploading to PyPi ..."
|
||||||
|
python setup.py sdist upload
|
||||||
|
make clean
|
||||||
|
|
||||||
|
/bin/echo -e "\n### DONE!"
|
||||||
|
40
devscripts/transition_helper.py
Normal file
40
devscripts/transition_helper.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import sys, os
|
||||||
|
|
||||||
|
try:
|
||||||
|
import urllib.request as compat_urllib_request
|
||||||
|
except ImportError: # Python 2
|
||||||
|
import urllib2 as compat_urllib_request
|
||||||
|
|
||||||
|
sys.stderr.write(u'Hi! We changed distribution method and now youtube-dl needs to update itself one more time.\n')
|
||||||
|
sys.stderr.write(u'This will only happen once. Simply press enter to go on. Sorry for the trouble!\n')
|
||||||
|
sys.stderr.write(u'The new location of the binaries is https://github.com/rg3/youtube-dl/downloads, not the git repository.\n\n')
|
||||||
|
|
||||||
|
try:
|
||||||
|
raw_input()
|
||||||
|
except NameError: # Python 3
|
||||||
|
input()
|
||||||
|
|
||||||
|
filename = sys.argv[0]
|
||||||
|
|
||||||
|
API_URL = "https://api.github.com/repos/rg3/youtube-dl/downloads"
|
||||||
|
BIN_URL = "https://github.com/downloads/rg3/youtube-dl/youtube-dl"
|
||||||
|
|
||||||
|
if not os.access(filename, os.W_OK):
|
||||||
|
sys.exit('ERROR: no write permissions on %s' % filename)
|
||||||
|
|
||||||
|
try:
|
||||||
|
urlh = compat_urllib_request.urlopen(BIN_URL)
|
||||||
|
newcontent = urlh.read()
|
||||||
|
urlh.close()
|
||||||
|
except (IOError, OSError) as err:
|
||||||
|
sys.exit('ERROR: unable to download latest version')
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(filename, 'wb') as outf:
|
||||||
|
outf.write(newcontent)
|
||||||
|
except (IOError, OSError) as err:
|
||||||
|
sys.exit('ERROR: unable to overwrite current version')
|
||||||
|
|
||||||
|
sys.stderr.write(u'Done! Now you can run youtube-dl.\n')
|
12
devscripts/transition_helper_exe/setup.py
Normal file
12
devscripts/transition_helper_exe/setup.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from distutils.core import setup
|
||||||
|
import py2exe
|
||||||
|
|
||||||
|
py2exe_options = {
|
||||||
|
"bundle_files": 1,
|
||||||
|
"compressed": 1,
|
||||||
|
"optimize": 2,
|
||||||
|
"dist_dir": '.',
|
||||||
|
"dll_excludes": ['w9xpopen.exe']
|
||||||
|
}
|
||||||
|
|
||||||
|
setup(console=['youtube-dl.py'], options={ "py2exe": py2exe_options }, zipfile=None)
|
102
devscripts/transition_helper_exe/youtube-dl.py
Normal file
102
devscripts/transition_helper_exe/youtube-dl.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import sys, os
|
||||||
|
import urllib2
|
||||||
|
import json, hashlib
|
||||||
|
|
||||||
|
def rsa_verify(message, signature, key):
|
||||||
|
from struct import pack
|
||||||
|
from hashlib import sha256
|
||||||
|
from sys import version_info
|
||||||
|
def b(x):
|
||||||
|
if version_info[0] == 2: return x
|
||||||
|
else: return x.encode('latin1')
|
||||||
|
assert(type(message) == type(b('')))
|
||||||
|
block_size = 0
|
||||||
|
n = key[0]
|
||||||
|
while n:
|
||||||
|
block_size += 1
|
||||||
|
n >>= 8
|
||||||
|
signature = pow(int(signature, 16), key[1], key[0])
|
||||||
|
raw_bytes = []
|
||||||
|
while signature:
|
||||||
|
raw_bytes.insert(0, pack("B", signature & 0xFF))
|
||||||
|
signature >>= 8
|
||||||
|
signature = (block_size - len(raw_bytes)) * b('\x00') + b('').join(raw_bytes)
|
||||||
|
if signature[0:2] != b('\x00\x01'): return False
|
||||||
|
signature = signature[2:]
|
||||||
|
if not b('\x00') in signature: return False
|
||||||
|
signature = signature[signature.index(b('\x00'))+1:]
|
||||||
|
if not signature.startswith(b('\x30\x31\x30\x0D\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20')): return False
|
||||||
|
signature = signature[19:]
|
||||||
|
if signature != sha256(message).digest(): return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
sys.stderr.write(u'Hi! We changed distribution method and now youtube-dl needs to update itself one more time.\n')
|
||||||
|
sys.stderr.write(u'This will only happen once. Simply press enter to go on. Sorry for the trouble!\n')
|
||||||
|
sys.stderr.write(u'From now on, get the binaries from http://rg3.github.com/youtube-dl/download.html, not from the git repository.\n\n')
|
||||||
|
|
||||||
|
raw_input()
|
||||||
|
|
||||||
|
filename = sys.argv[0]
|
||||||
|
|
||||||
|
UPDATE_URL = "http://rg3.github.io/youtube-dl/update/"
|
||||||
|
VERSION_URL = UPDATE_URL + 'LATEST_VERSION'
|
||||||
|
JSON_URL = UPDATE_URL + 'versions.json'
|
||||||
|
UPDATES_RSA_KEY = (0x9d60ee4d8f805312fdb15a62f87b95bd66177b91df176765d13514a0f1754bcd2057295c5b6f1d35daa6742c3ffc9a82d3e118861c207995a8031e151d863c9927e304576bc80692bc8e094896fcf11b66f3e29e04e3a71e9a11558558acea1840aec37fc396fb6b65dc81a1c4144e03bd1c011de62e3f1357b327d08426fe93, 65537)
|
||||||
|
|
||||||
|
if not os.access(filename, os.W_OK):
|
||||||
|
sys.exit('ERROR: no write permissions on %s' % filename)
|
||||||
|
|
||||||
|
exe = os.path.abspath(filename)
|
||||||
|
directory = os.path.dirname(exe)
|
||||||
|
if not os.access(directory, os.W_OK):
|
||||||
|
sys.exit('ERROR: no write permissions on %s' % directory)
|
||||||
|
|
||||||
|
try:
|
||||||
|
versions_info = urllib2.urlopen(JSON_URL).read().decode('utf-8')
|
||||||
|
versions_info = json.loads(versions_info)
|
||||||
|
except:
|
||||||
|
sys.exit(u'ERROR: can\'t obtain versions info. Please try again later.')
|
||||||
|
if not 'signature' in versions_info:
|
||||||
|
sys.exit(u'ERROR: the versions file is not signed or corrupted. Aborting.')
|
||||||
|
signature = versions_info['signature']
|
||||||
|
del versions_info['signature']
|
||||||
|
if not rsa_verify(json.dumps(versions_info, sort_keys=True), signature, UPDATES_RSA_KEY):
|
||||||
|
sys.exit(u'ERROR: the versions file signature is invalid. Aborting.')
|
||||||
|
|
||||||
|
version = versions_info['versions'][versions_info['latest']]
|
||||||
|
|
||||||
|
try:
|
||||||
|
urlh = urllib2.urlopen(version['exe'][0])
|
||||||
|
newcontent = urlh.read()
|
||||||
|
urlh.close()
|
||||||
|
except (IOError, OSError) as err:
|
||||||
|
sys.exit('ERROR: unable to download latest version')
|
||||||
|
|
||||||
|
newcontent_hash = hashlib.sha256(newcontent).hexdigest()
|
||||||
|
if newcontent_hash != version['exe'][1]:
|
||||||
|
sys.exit(u'ERROR: the downloaded file hash does not match. Aborting.')
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(exe + '.new', 'wb') as outf:
|
||||||
|
outf.write(newcontent)
|
||||||
|
except (IOError, OSError) as err:
|
||||||
|
sys.exit(u'ERROR: unable to write the new version')
|
||||||
|
|
||||||
|
try:
|
||||||
|
bat = os.path.join(directory, 'youtube-dl-updater.bat')
|
||||||
|
b = open(bat, 'w')
|
||||||
|
b.write("""
|
||||||
|
echo Updating youtube-dl...
|
||||||
|
ping 127.0.0.1 -n 5 -w 1000 > NUL
|
||||||
|
move /Y "%s.new" "%s"
|
||||||
|
del "%s"
|
||||||
|
\n""" %(exe, exe, bat))
|
||||||
|
b.close()
|
||||||
|
|
||||||
|
os.startfile(bat)
|
||||||
|
except (IOError, OSError) as err:
|
||||||
|
sys.exit('ERROR: unable to overwrite current version')
|
||||||
|
|
||||||
|
sys.stderr.write(u'Done! Now you can run youtube-dl.\n')
|
78
setup.py
Normal file
78
setup.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
import pkg_resources
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
from setuptools import setup
|
||||||
|
except ImportError:
|
||||||
|
from distutils.core import setup
|
||||||
|
|
||||||
|
try:
|
||||||
|
import py2exe
|
||||||
|
"""This will create an exe that needs Microsoft Visual C++ 2008 Redistributable Package"""
|
||||||
|
except ImportError:
|
||||||
|
if len(sys.argv) >= 2 and sys.argv[1] == 'py2exe':
|
||||||
|
print("Cannot import py2exe", file=sys.stderr)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
py2exe_options = {
|
||||||
|
"bundle_files": 1,
|
||||||
|
"compressed": 1,
|
||||||
|
"optimize": 2,
|
||||||
|
"dist_dir": '.',
|
||||||
|
"dll_excludes": ['w9xpopen.exe']
|
||||||
|
}
|
||||||
|
py2exe_console = [{
|
||||||
|
"script": "./youtube_dl/__main__.py",
|
||||||
|
"dest_base": "youtube-dl",
|
||||||
|
}]
|
||||||
|
py2exe_params = {
|
||||||
|
'console': py2exe_console,
|
||||||
|
'options': { "py2exe": py2exe_options },
|
||||||
|
'zipfile': None
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(sys.argv) >= 2 and sys.argv[1] == 'py2exe':
|
||||||
|
params = py2exe_params
|
||||||
|
else:
|
||||||
|
params = {
|
||||||
|
'scripts': ['bin/youtube-dl'],
|
||||||
|
'data_files': [('etc/bash_completion.d', ['youtube-dl.bash-completion']), # Installing system-wide would require sudo...
|
||||||
|
('share/doc/youtube_dl', ['README.txt']),
|
||||||
|
('share/man/man1/', ['youtube-dl.1'])]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the version from youtube_dl/version.py without importing the package
|
||||||
|
exec(compile(open('youtube_dl/version.py').read(), 'youtube_dl/version.py', 'exec'))
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name = 'youtube_dl',
|
||||||
|
version = __version__,
|
||||||
|
description = 'YouTube video downloader',
|
||||||
|
long_description = 'Small command-line program to download videos from YouTube.com and other video sites.',
|
||||||
|
url = 'https://github.com/rg3/youtube-dl',
|
||||||
|
author = 'Ricardo Garcia',
|
||||||
|
maintainer = 'Philipp Hagemeister',
|
||||||
|
maintainer_email = 'phihag@phihag.de',
|
||||||
|
packages = ['youtube_dl'],
|
||||||
|
|
||||||
|
# Provokes warning on most systems (why?!)
|
||||||
|
#test_suite = 'nose.collector',
|
||||||
|
#test_requires = ['nosetest'],
|
||||||
|
|
||||||
|
classifiers = [
|
||||||
|
"Topic :: Multimedia :: Video",
|
||||||
|
"Development Status :: 5 - Production/Stable",
|
||||||
|
"Environment :: Console",
|
||||||
|
"License :: Public Domain",
|
||||||
|
"Programming Language :: Python :: 2.6",
|
||||||
|
"Programming Language :: Python :: 2.7",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.3"
|
||||||
|
],
|
||||||
|
|
||||||
|
**params
|
||||||
|
)
|
@@ -1 +1,44 @@
|
|||||||
{"username": null, "listformats": null, "skip_download": false, "usenetrc": false, "max_downloads": null, "noprogress": false, "forcethumbnail": false, "forceformat": false, "format_limit": null, "ratelimit": null, "nooverwrites": false, "forceurl": false, "writeinfojson": false, "simulate": false, "playliststart": 1, "continuedl": true, "password": null, "prefer_free_formats": false, "nopart": false, "retries": 10, "updatetime": true, "consoletitle": false, "verbose": true, "forcefilename": false, "ignoreerrors": false, "logtostderr": false, "format": null, "subtitleslang": null, "quiet": false, "outtmpl": "%(id)s.%(ext)s", "rejecttitle": null, "playlistend": -1, "writedescription": false, "forcetitle": false, "forcedescription": false, "writesubtitles": false, "matchtitle": null}
|
{
|
||||||
|
"consoletitle": false,
|
||||||
|
"continuedl": true,
|
||||||
|
"forcedescription": false,
|
||||||
|
"forcefilename": false,
|
||||||
|
"forceformat": false,
|
||||||
|
"forcethumbnail": false,
|
||||||
|
"forcetitle": false,
|
||||||
|
"forceurl": false,
|
||||||
|
"format": null,
|
||||||
|
"format_limit": null,
|
||||||
|
"ignoreerrors": false,
|
||||||
|
"listformats": null,
|
||||||
|
"logtostderr": false,
|
||||||
|
"matchtitle": null,
|
||||||
|
"max_downloads": null,
|
||||||
|
"nooverwrites": false,
|
||||||
|
"nopart": false,
|
||||||
|
"noprogress": false,
|
||||||
|
"outtmpl": "%(id)s.%(ext)s",
|
||||||
|
"password": null,
|
||||||
|
"playlistend": -1,
|
||||||
|
"playliststart": 1,
|
||||||
|
"prefer_free_formats": false,
|
||||||
|
"quiet": false,
|
||||||
|
"ratelimit": null,
|
||||||
|
"rejecttitle": null,
|
||||||
|
"retries": 10,
|
||||||
|
"simulate": false,
|
||||||
|
"skip_download": false,
|
||||||
|
"subtitleslang": null,
|
||||||
|
"subtitlesformat": "srt",
|
||||||
|
"test": true,
|
||||||
|
"updatetime": true,
|
||||||
|
"usenetrc": false,
|
||||||
|
"username": null,
|
||||||
|
"verbose": true,
|
||||||
|
"writedescription": false,
|
||||||
|
"writeinfojson": true,
|
||||||
|
"writesubtitles": false,
|
||||||
|
"onlysubtitles": false,
|
||||||
|
"allsubtitles": false,
|
||||||
|
"listssubtitles": false
|
||||||
|
}
|
||||||
|
54
test/test_all_urls.py
Normal file
54
test/test_all_urls.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
# Allow direct execution
|
||||||
|
import os
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from youtube_dl.InfoExtractors import YoutubeIE, YoutubePlaylistIE, YoutubeChannelIE, JustinTVIE
|
||||||
|
|
||||||
|
class TestAllURLsMatching(unittest.TestCase):
|
||||||
|
def test_youtube_playlist_matching(self):
|
||||||
|
self.assertTrue(YoutubePlaylistIE.suitable(u'ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8'))
|
||||||
|
self.assertTrue(YoutubePlaylistIE.suitable(u'UUBABnxM4Ar9ten8Mdjj1j0Q')) #585
|
||||||
|
self.assertTrue(YoutubePlaylistIE.suitable(u'PL63F0C78739B09958'))
|
||||||
|
self.assertTrue(YoutubePlaylistIE.suitable(u'https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q'))
|
||||||
|
self.assertTrue(YoutubePlaylistIE.suitable(u'https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8'))
|
||||||
|
self.assertTrue(YoutubePlaylistIE.suitable(u'https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC'))
|
||||||
|
self.assertTrue(YoutubePlaylistIE.suitable(u'https://www.youtube.com/watch?v=AV6J6_AeFEQ&playnext=1&list=PL4023E734DA416012')) #668
|
||||||
|
self.assertFalse(YoutubePlaylistIE.suitable(u'PLtS2H6bU1M'))
|
||||||
|
|
||||||
|
def test_youtube_matching(self):
|
||||||
|
self.assertTrue(YoutubeIE.suitable(u'PLtS2H6bU1M'))
|
||||||
|
self.assertFalse(YoutubeIE.suitable(u'https://www.youtube.com/watch?v=AV6J6_AeFEQ&playnext=1&list=PL4023E734DA416012')) #668
|
||||||
|
|
||||||
|
def test_youtube_channel_matching(self):
|
||||||
|
self.assertTrue(YoutubeChannelIE.suitable('https://www.youtube.com/channel/HCtnHdj3df7iM'))
|
||||||
|
self.assertTrue(YoutubeChannelIE.suitable('https://www.youtube.com/channel/HCtnHdj3df7iM?feature=gb_ch_rec'))
|
||||||
|
self.assertTrue(YoutubeChannelIE.suitable('https://www.youtube.com/channel/HCtnHdj3df7iM/videos'))
|
||||||
|
|
||||||
|
def test_justin_tv_channelid_matching(self):
|
||||||
|
self.assertTrue(JustinTVIE.suitable(u"justin.tv/vanillatv"))
|
||||||
|
self.assertTrue(JustinTVIE.suitable(u"twitch.tv/vanillatv"))
|
||||||
|
self.assertTrue(JustinTVIE.suitable(u"www.justin.tv/vanillatv"))
|
||||||
|
self.assertTrue(JustinTVIE.suitable(u"www.twitch.tv/vanillatv"))
|
||||||
|
self.assertTrue(JustinTVIE.suitable(u"http://www.justin.tv/vanillatv"))
|
||||||
|
self.assertTrue(JustinTVIE.suitable(u"http://www.twitch.tv/vanillatv"))
|
||||||
|
self.assertTrue(JustinTVIE.suitable(u"http://www.justin.tv/vanillatv/"))
|
||||||
|
self.assertTrue(JustinTVIE.suitable(u"http://www.twitch.tv/vanillatv/"))
|
||||||
|
|
||||||
|
def test_justintv_videoid_matching(self):
|
||||||
|
self.assertTrue(JustinTVIE.suitable(u"http://www.twitch.tv/vanillatv/b/328087483"))
|
||||||
|
|
||||||
|
def test_justin_tv_chapterid_matching(self):
|
||||||
|
self.assertTrue(JustinTVIE.suitable(u"http://www.twitch.tv/tsm_theoddone/c/2349361"))
|
||||||
|
|
||||||
|
def test_youtube_extract(self):
|
||||||
|
self.assertEqual(YoutubeIE()._extract_id('http://www.youtube.com/watch?&v=BaW_jenozKc'), 'BaW_jenozKc')
|
||||||
|
self.assertEqual(YoutubeIE()._extract_id('https://www.youtube.com/watch?&v=BaW_jenozKc'), 'BaW_jenozKc')
|
||||||
|
self.assertEqual(YoutubeIE()._extract_id('https://www.youtube.com/watch?feature=player_embedded&v=BaW_jenozKc'), 'BaW_jenozKc')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
@@ -1,198 +1,161 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python
|
||||||
import unittest
|
|
||||||
|
import errno
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import io
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
import unittest
|
||||||
|
import sys
|
||||||
|
import socket
|
||||||
|
import binascii
|
||||||
|
|
||||||
from youtube_dl.FileDownloader import FileDownloader
|
# Allow direct execution
|
||||||
from youtube_dl.InfoExtractors import YoutubeIE, DailymotionIE
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
from youtube_dl.InfoExtractors import MetacafeIE, BlipTVIE
|
|
||||||
from youtube_dl.InfoExtractors import XVideosIE, VimeoIE
|
import youtube_dl.FileDownloader
|
||||||
from youtube_dl.InfoExtractors import SoundcloudIE, StanfordOpenClassroomIE
|
import youtube_dl.InfoExtractors
|
||||||
from youtube_dl.InfoExtractors import CollegeHumorIE, XNXXIE
|
from youtube_dl.utils import *
|
||||||
|
|
||||||
|
DEF_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'tests.json')
|
||||||
|
PARAMETERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "parameters.json")
|
||||||
|
|
||||||
|
RETRIES = 3
|
||||||
|
|
||||||
|
# General configuration (from __init__, not very elegant...)
|
||||||
|
jar = compat_cookiejar.CookieJar()
|
||||||
|
cookie_processor = compat_urllib_request.HTTPCookieProcessor(jar)
|
||||||
|
proxy_handler = compat_urllib_request.ProxyHandler()
|
||||||
|
opener = compat_urllib_request.build_opener(proxy_handler, cookie_processor, YoutubeDLHandler())
|
||||||
|
compat_urllib_request.install_opener(opener)
|
||||||
|
socket.setdefaulttimeout(10)
|
||||||
|
|
||||||
|
def _try_rm(filename):
|
||||||
|
""" Remove a file if it exists """
|
||||||
|
try:
|
||||||
|
os.remove(filename)
|
||||||
|
except OSError as ose:
|
||||||
|
if ose.errno != errno.ENOENT:
|
||||||
|
raise
|
||||||
|
|
||||||
|
md5 = lambda s: hashlib.md5(s.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
|
class FileDownloader(youtube_dl.FileDownloader):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.to_stderr = self.to_screen
|
||||||
|
self.processed_info_dicts = []
|
||||||
|
return youtube_dl.FileDownloader.__init__(self, *args, **kwargs)
|
||||||
|
def report_warning(self, message):
|
||||||
|
# Don't accept warnings during tests
|
||||||
|
raise ExtractorError(message)
|
||||||
|
def process_info(self, info_dict):
|
||||||
|
self.processed_info_dicts.append(info_dict)
|
||||||
|
return youtube_dl.FileDownloader.process_info(self, info_dict)
|
||||||
|
|
||||||
|
def _file_md5(fn):
|
||||||
|
with open(fn, 'rb') as f:
|
||||||
|
return hashlib.md5(f.read()).hexdigest()
|
||||||
|
|
||||||
|
with io.open(DEF_FILE, encoding='utf-8') as deff:
|
||||||
|
defs = json.load(deff)
|
||||||
|
with io.open(PARAMETERS_FILE, encoding='utf-8') as pf:
|
||||||
|
parameters = json.load(pf)
|
||||||
|
|
||||||
|
|
||||||
class DownloadTest(unittest.TestCase):
|
class TestDownload(unittest.TestCase):
|
||||||
PARAMETERS_FILE = "test/parameters.json"
|
maxDiff = None
|
||||||
#calculated with md5sum:
|
def setUp(self):
|
||||||
#md5sum (GNU coreutils) 8.19
|
self.parameters = parameters
|
||||||
|
self.defs = defs
|
||||||
|
|
||||||
YOUTUBE_SIZE = 1993883
|
### Dynamically generate tests
|
||||||
YOUTUBE_URL = "http://www.youtube.com/watch?v=BaW_jenozKc"
|
def generator(test_case):
|
||||||
YOUTUBE_FILE = "BaW_jenozKc.mp4"
|
|
||||||
|
|
||||||
DAILYMOTION_MD5 = "d363a50e9eb4f22ce90d08d15695bb47"
|
def test_template(self):
|
||||||
DAILYMOTION_URL = "http://www.dailymotion.com/video/x33vw9_tutoriel-de-youtubeur-dl-des-video_tech"
|
ie = youtube_dl.InfoExtractors.get_info_extractor(test_case['name'])
|
||||||
DAILYMOTION_FILE = "x33vw9.mp4"
|
if not ie._WORKING:
|
||||||
|
print('Skipping: IE marked as not _WORKING')
|
||||||
METACAFE_SIZE = 5754305
|
|
||||||
METACAFE_URL = "http://www.metacafe.com/watch/yt-_aUehQsCQtM/the_electric_company_short_i_pbs_kids_go/"
|
|
||||||
METACAFE_FILE = "_aUehQsCQtM.flv"
|
|
||||||
|
|
||||||
BLIP_MD5 = "93c24d2f4e0782af13b8a7606ea97ba7"
|
|
||||||
BLIP_URL = "http://blip.tv/cbr/cbr-exclusive-gotham-city-imposters-bats-vs-jokerz-short-3-5796352"
|
|
||||||
BLIP_FILE = "5779306.m4v"
|
|
||||||
|
|
||||||
XVIDEO_MD5 = "1ab4dedc01f771cb2a65e91caa801aaf"
|
|
||||||
XVIDEO_URL = "http://www.xvideos.com/video939581/funny_porns_by_s_-1"
|
|
||||||
XVIDEO_FILE = "939581.flv"
|
|
||||||
|
|
||||||
VIMEO_MD5 = "1ab4dedc01f771cb2a65e91caa801aaf"
|
|
||||||
VIMEO_URL = "http://vimeo.com/14160053"
|
|
||||||
VIMEO_FILE = ""
|
|
||||||
|
|
||||||
VIMEO2_MD5 = ""
|
|
||||||
VIMEO2_URL = "http://player.vimeo.com/video/47019590"
|
|
||||||
VIMEO2_FILE = ""
|
|
||||||
|
|
||||||
SOUNDCLOUD_MD5 = "ce3775768ebb6432fa8495d446a078ed"
|
|
||||||
SOUNDCLOUD_URL = "http://soundcloud.com/ethmusic/lostin-powers-she-so-heavy"
|
|
||||||
SOUNDCLOUD_FILE = "n6FLbx6ZzMiu.mp3"
|
|
||||||
|
|
||||||
STANDFORD_MD5 = "22c8206291368c4e2c9c1a307f0ea0f4"
|
|
||||||
STANDFORD_URL = "http://openclassroom.stanford.edu/MainFolder/VideoPage.php?course=PracticalUnix&video=intro-environment&speed=100"
|
|
||||||
STANDFORD_FILE = "PracticalUnix_intro-environment.mp4"
|
|
||||||
|
|
||||||
COLLEGEHUMOR_MD5 = ""
|
|
||||||
COLLEGEHUMOR_URL = "http://www.collegehumor.com/video/6830834/mitt-romney-style-gangnam-style-parody"
|
|
||||||
COLLEGEHUMOR_FILE = ""
|
|
||||||
|
|
||||||
XNXX_MD5 = "5f0469c8d1dfd1bc38c8e6deb5e0a21d"
|
|
||||||
XNXX_URL = "http://video.xnxx.com/video1135332/lida_naked_funny_actress_5_"
|
|
||||||
XNXX_FILE = "1135332.flv"
|
|
||||||
|
|
||||||
def test_youtube(self):
|
|
||||||
#let's download a file from youtube
|
|
||||||
with open(DownloadTest.PARAMETERS_FILE) as f:
|
|
||||||
fd = FileDownloader(json.load(f))
|
|
||||||
fd.add_info_extractor(YoutubeIE())
|
|
||||||
fd.download([DownloadTest.YOUTUBE_URL])
|
|
||||||
self.assertTrue(os.path.exists(DownloadTest.YOUTUBE_FILE))
|
|
||||||
self.assertEqual(os.path.getsize(DownloadTest.YOUTUBE_FILE), DownloadTest.YOUTUBE_SIZE)
|
|
||||||
|
|
||||||
def test_dailymotion(self):
|
|
||||||
with open(DownloadTest.PARAMETERS_FILE) as f:
|
|
||||||
fd = FileDownloader(json.load(f))
|
|
||||||
fd.add_info_extractor(DailymotionIE())
|
|
||||||
fd.download([DownloadTest.DAILYMOTION_URL])
|
|
||||||
self.assertTrue(os.path.exists(DownloadTest.DAILYMOTION_FILE))
|
|
||||||
md5_down_file = md5_for_file(DownloadTest.DAILYMOTION_FILE)
|
|
||||||
self.assertEqual(md5_down_file, DownloadTest.DAILYMOTION_MD5)
|
|
||||||
|
|
||||||
def test_metacafe(self):
|
|
||||||
#this emulate a skip,to be 2.6 compatible
|
|
||||||
with open(DownloadTest.PARAMETERS_FILE) as f:
|
|
||||||
fd = FileDownloader(json.load(f))
|
|
||||||
fd.add_info_extractor(MetacafeIE())
|
|
||||||
fd.add_info_extractor(YoutubeIE())
|
|
||||||
fd.download([DownloadTest.METACAFE_URL])
|
|
||||||
self.assertTrue(os.path.exists(DownloadTest.METACAFE_FILE))
|
|
||||||
self.assertEqual(os.path.getsize(DownloadTest.METACAFE_FILE), DownloadTest.METACAFE_SIZE)
|
|
||||||
|
|
||||||
def test_blip(self):
|
|
||||||
with open(DownloadTest.PARAMETERS_FILE) as f:
|
|
||||||
fd = FileDownloader(json.load(f))
|
|
||||||
fd.add_info_extractor(BlipTVIE())
|
|
||||||
fd.download([DownloadTest.BLIP_URL])
|
|
||||||
self.assertTrue(os.path.exists(DownloadTest.BLIP_FILE))
|
|
||||||
md5_down_file = md5_for_file(DownloadTest.BLIP_FILE)
|
|
||||||
self.assertEqual(md5_down_file, DownloadTest.BLIP_MD5)
|
|
||||||
|
|
||||||
def test_xvideo(self):
|
|
||||||
with open(DownloadTest.PARAMETERS_FILE) as f:
|
|
||||||
fd = FileDownloader(json.load(f))
|
|
||||||
fd.add_info_extractor(XVideosIE())
|
|
||||||
fd.download([DownloadTest.XVIDEO_URL])
|
|
||||||
self.assertTrue(os.path.exists(DownloadTest.XVIDEO_FILE))
|
|
||||||
md5_down_file = md5_for_file(DownloadTest.XVIDEO_FILE)
|
|
||||||
self.assertEqual(md5_down_file, DownloadTest.XVIDEO_MD5)
|
|
||||||
|
|
||||||
def test_vimeo(self):
|
|
||||||
#skipped for the moment produce an error
|
|
||||||
return
|
return
|
||||||
with open(DownloadTest.PARAMETERS_FILE) as f:
|
if 'playlist' not in test_case and not test_case['file']:
|
||||||
fd = FileDownloader(json.load(f))
|
print('Skipping: No output file specified')
|
||||||
fd.add_info_extractor(VimeoIE())
|
return
|
||||||
fd.download([DownloadTest.VIMEO_URL])
|
if 'skip' in test_case:
|
||||||
self.assertTrue(os.path.exists(DownloadTest.VIMEO_FILE))
|
print('Skipping: {0}'.format(test_case['skip']))
|
||||||
md5_down_file = md5_for_file(DownloadTest.VIMEO_FILE)
|
|
||||||
self.assertEqual(md5_down_file, DownloadTest.VIMEO_MD5)
|
|
||||||
|
|
||||||
def test_vimeo2(self):
|
|
||||||
#skipped for the moment produce an error
|
|
||||||
return
|
return
|
||||||
with open(DownloadTest.PARAMETERS_FILE) as f:
|
|
||||||
fd = FileDownloader(json.load(f))
|
|
||||||
fd.add_info_extractor(VimeoIE())
|
|
||||||
fd.download([DownloadTest.VIMEO2_URL])
|
|
||||||
self.assertTrue(os.path.exists(DownloadTest.VIMEO2_FILE))
|
|
||||||
md5_down_file = md5_for_file(DownloadTest.VIMEO2_FILE)
|
|
||||||
self.assertEqual(md5_down_file, DownloadTest.VIMEO2_MD5)
|
|
||||||
|
|
||||||
def test_soundcloud(self):
|
params = self.parameters.copy()
|
||||||
with open(DownloadTest.PARAMETERS_FILE) as f:
|
params.update(test_case.get('params', {}))
|
||||||
fd = FileDownloader(json.load(f))
|
|
||||||
fd.add_info_extractor(SoundcloudIE())
|
|
||||||
fd.download([DownloadTest.SOUNDCLOUD_URL])
|
|
||||||
self.assertTrue(os.path.exists(DownloadTest.SOUNDCLOUD_FILE))
|
|
||||||
md5_down_file = md5_for_file(DownloadTest.SOUNDCLOUD_FILE)
|
|
||||||
self.assertEqual(md5_down_file, DownloadTest.SOUNDCLOUD_MD5)
|
|
||||||
|
|
||||||
def test_standford(self):
|
fd = FileDownloader(params)
|
||||||
with open(DownloadTest.PARAMETERS_FILE) as f:
|
for ie in youtube_dl.InfoExtractors.gen_extractors():
|
||||||
fd = FileDownloader(json.load(f))
|
fd.add_info_extractor(ie)
|
||||||
fd.add_info_extractor(StanfordOpenClassroomIE())
|
finished_hook_called = set()
|
||||||
fd.download([DownloadTest.STANDFORD_URL])
|
def _hook(status):
|
||||||
self.assertTrue(os.path.exists(DownloadTest.STANDFORD_FILE))
|
if status['status'] == 'finished':
|
||||||
md5_down_file = md5_for_file(DownloadTest.STANDFORD_FILE)
|
finished_hook_called.add(status['filename'])
|
||||||
self.assertEqual(md5_down_file, DownloadTest.STANDFORD_MD5)
|
fd.add_progress_hook(_hook)
|
||||||
|
|
||||||
def test_collegehumor(self):
|
test_cases = test_case.get('playlist', [test_case])
|
||||||
with open(DownloadTest.PARAMETERS_FILE) as f:
|
for tc in test_cases:
|
||||||
fd = FileDownloader(json.load(f))
|
_try_rm(tc['file'])
|
||||||
fd.add_info_extractor(CollegeHumorIE())
|
_try_rm(tc['file'] + '.part')
|
||||||
fd.download([DownloadTest.COLLEGEHUMOR_URL])
|
_try_rm(tc['file'] + '.info.json')
|
||||||
self.assertTrue(os.path.exists(DownloadTest.COLLEGEHUMOR_FILE))
|
try:
|
||||||
md5_down_file = md5_for_file(DownloadTest.COLLEGEHUMOR_FILE)
|
for retry in range(1, RETRIES + 1):
|
||||||
self.assertEqual(md5_down_file, DownloadTest.COLLEGEHUMOR_MD5)
|
try:
|
||||||
|
fd.download([test_case['url']])
|
||||||
|
except (DownloadError, ExtractorError) as err:
|
||||||
|
if retry == RETRIES: raise
|
||||||
|
|
||||||
def test_xnxx(self):
|
# Check if the exception is not a network related one
|
||||||
with open(DownloadTest.PARAMETERS_FILE) as f:
|
if not err.exc_info[0] in (compat_urllib_error.URLError, socket.timeout, UnavailableVideoError):
|
||||||
fd = FileDownloader(json.load(f))
|
raise
|
||||||
fd.add_info_extractor(XNXXIE())
|
|
||||||
fd.download([DownloadTest.XNXX_URL])
|
|
||||||
self.assertTrue(os.path.exists(DownloadTest.XNXX_FILE))
|
|
||||||
md5_down_file = md5_for_file(DownloadTest.XNXX_FILE)
|
|
||||||
self.assertEqual(md5_down_file, DownloadTest.XNXX_MD5)
|
|
||||||
|
|
||||||
def tearDown(self):
|
print('Retrying: {0} failed tries\n\n##########\n\n'.format(retry))
|
||||||
if os.path.exists(DownloadTest.YOUTUBE_FILE):
|
else:
|
||||||
os.remove(DownloadTest.YOUTUBE_FILE)
|
|
||||||
if os.path.exists(DownloadTest.DAILYMOTION_FILE):
|
|
||||||
os.remove(DownloadTest.DAILYMOTION_FILE)
|
|
||||||
if os.path.exists(DownloadTest.METACAFE_FILE):
|
|
||||||
os.remove(DownloadTest.METACAFE_FILE)
|
|
||||||
if os.path.exists(DownloadTest.BLIP_FILE):
|
|
||||||
os.remove(DownloadTest.BLIP_FILE)
|
|
||||||
if os.path.exists(DownloadTest.XVIDEO_FILE):
|
|
||||||
os.remove(DownloadTest.XVIDEO_FILE)
|
|
||||||
if os.path.exists(DownloadTest.VIMEO_FILE):
|
|
||||||
os.remove(DownloadTest.VIMEO_FILE)
|
|
||||||
if os.path.exists(DownloadTest.SOUNDCLOUD_FILE):
|
|
||||||
os.remove(DownloadTest.SOUNDCLOUD_FILE)
|
|
||||||
if os.path.exists(DownloadTest.STANDFORD_FILE):
|
|
||||||
os.remove(DownloadTest.STANDFORD_FILE)
|
|
||||||
if os.path.exists(DownloadTest.COLLEGEHUMOR_FILE):
|
|
||||||
os.remove(DownloadTest.COLLEGEHUMOR_FILE)
|
|
||||||
if os.path.exists(DownloadTest.XNXX_FILE):
|
|
||||||
os.remove(DownloadTest.XNXX_FILE)
|
|
||||||
|
|
||||||
def md5_for_file(filename, block_size=2**20):
|
|
||||||
with open(filename) as f:
|
|
||||||
md5 = hashlib.md5()
|
|
||||||
while True:
|
|
||||||
data = f.read(block_size)
|
|
||||||
if not data:
|
|
||||||
break
|
break
|
||||||
md5.update(data)
|
|
||||||
return md5.hexdigest()
|
for tc in test_cases:
|
||||||
|
if not test_case.get('params', {}).get('skip_download', False):
|
||||||
|
self.assertTrue(os.path.exists(tc['file']), msg='Missing file ' + tc['file'])
|
||||||
|
self.assertTrue(tc['file'] in finished_hook_called)
|
||||||
|
self.assertTrue(os.path.exists(tc['file'] + '.info.json'))
|
||||||
|
if 'md5' in tc:
|
||||||
|
md5_for_file = _file_md5(tc['file'])
|
||||||
|
self.assertEqual(md5_for_file, tc['md5'])
|
||||||
|
with io.open(tc['file'] + '.info.json', encoding='utf-8') as infof:
|
||||||
|
info_dict = json.load(infof)
|
||||||
|
for (info_field, value) in tc.get('info_dict', {}).items():
|
||||||
|
if isinstance(value, compat_str) and value.startswith('md5:'):
|
||||||
|
self.assertEqual(value, 'md5:' + md5(info_dict.get(info_field)))
|
||||||
|
else:
|
||||||
|
self.assertEqual(value, info_dict.get(info_field))
|
||||||
|
|
||||||
|
# If checkable fields are missing from the test case, print the info_dict
|
||||||
|
test_info_dict = dict((key, value if not isinstance(value, compat_str) or len(value) < 250 else 'md5:' + md5(value))
|
||||||
|
for key, value in info_dict.items()
|
||||||
|
if value and key in ('title', 'description', 'uploader', 'upload_date', 'uploader_id', 'location'))
|
||||||
|
if not all(key in tc.get('info_dict', {}).keys() for key in test_info_dict.keys()):
|
||||||
|
sys.stderr.write(u'\n"info_dict": ' + json.dumps(test_info_dict, ensure_ascii=False, indent=2) + u'\n')
|
||||||
|
|
||||||
|
# Check for the presence of mandatory fields
|
||||||
|
for key in ('id', 'url', 'title', 'ext'):
|
||||||
|
self.assertTrue(key in info_dict.keys() and info_dict[key])
|
||||||
|
finally:
|
||||||
|
for tc in test_cases:
|
||||||
|
_try_rm(tc['file'])
|
||||||
|
_try_rm(tc['file'] + '.part')
|
||||||
|
_try_rm(tc['file'] + '.info.json')
|
||||||
|
|
||||||
|
return test_template
|
||||||
|
|
||||||
|
### And add them to TestDownload
|
||||||
|
for test_case in defs:
|
||||||
|
test_method = generator(test_case)
|
||||||
|
test_method.__name__ = "test_{0}".format(test_case["name"])
|
||||||
|
setattr(TestDownload, test_method.__name__, test_method)
|
||||||
|
del test_method
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
|
26
test/test_execution.py
Normal file
26
test/test_execution.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
rootDir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
try:
|
||||||
|
_DEV_NULL = subprocess.DEVNULL
|
||||||
|
except AttributeError:
|
||||||
|
_DEV_NULL = open(os.devnull, 'wb')
|
||||||
|
|
||||||
|
class TestExecution(unittest.TestCase):
|
||||||
|
def test_import(self):
|
||||||
|
subprocess.check_call([sys.executable, '-c', 'import youtube_dl'], cwd=rootDir)
|
||||||
|
|
||||||
|
def test_module_exec(self):
|
||||||
|
if sys.version_info >= (2,7): # Python 2.6 doesn't support package execution
|
||||||
|
subprocess.check_call([sys.executable, '-m', 'youtube_dl', '--version'], cwd=rootDir, stdout=_DEV_NULL)
|
||||||
|
|
||||||
|
def test_main_exec(self):
|
||||||
|
subprocess.check_call([sys.executable, 'youtube_dl/__main__.py', '--version'], cwd=rootDir, stdout=_DEV_NULL)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
@@ -1,14 +1,26 @@
|
|||||||
# -*- coding: utf-8 -*-
|
#!/usr/bin/env python
|
||||||
|
|
||||||
# Various small unit tests
|
# Various small unit tests
|
||||||
|
|
||||||
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
# Allow direct execution
|
||||||
|
import os
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
#from youtube_dl.utils import htmlentity_transform
|
#from youtube_dl.utils import htmlentity_transform
|
||||||
from youtube_dl.utils import timeconvert
|
from youtube_dl.utils import timeconvert
|
||||||
from youtube_dl.utils import sanitize_filename
|
from youtube_dl.utils import sanitize_filename
|
||||||
from youtube_dl.utils import unescapeHTML
|
from youtube_dl.utils import unescapeHTML
|
||||||
from youtube_dl.utils import orderedSet
|
from youtube_dl.utils import orderedSet
|
||||||
|
from youtube_dl.utils import DateRange
|
||||||
|
from youtube_dl.utils import unified_strdate
|
||||||
|
|
||||||
|
if sys.version_info < (3, 0):
|
||||||
|
_compat_str = lambda b: b.decode('unicode-escape')
|
||||||
|
else:
|
||||||
|
_compat_str = lambda s: s
|
||||||
|
|
||||||
|
|
||||||
class TestUtil(unittest.TestCase):
|
class TestUtil(unittest.TestCase):
|
||||||
@@ -17,63 +29,88 @@ class TestUtil(unittest.TestCase):
|
|||||||
self.assertTrue(timeconvert('bougrg') is None)
|
self.assertTrue(timeconvert('bougrg') is None)
|
||||||
|
|
||||||
def test_sanitize_filename(self):
|
def test_sanitize_filename(self):
|
||||||
self.assertEqual(sanitize_filename(u'abc'), u'abc')
|
self.assertEqual(sanitize_filename('abc'), 'abc')
|
||||||
self.assertEqual(sanitize_filename(u'abc_d-e'), u'abc_d-e')
|
self.assertEqual(sanitize_filename('abc_d-e'), 'abc_d-e')
|
||||||
|
|
||||||
self.assertEqual(sanitize_filename(u'123'), u'123')
|
self.assertEqual(sanitize_filename('123'), '123')
|
||||||
|
|
||||||
self.assertEqual(u'abc_de', sanitize_filename(u'abc/de'))
|
self.assertEqual('abc_de', sanitize_filename('abc/de'))
|
||||||
self.assertFalse(u'/' in sanitize_filename(u'abc/de///'))
|
self.assertFalse('/' in sanitize_filename('abc/de///'))
|
||||||
|
|
||||||
self.assertEqual(u'abc_de', sanitize_filename(u'abc/<>\\*|de'))
|
self.assertEqual('abc_de', sanitize_filename('abc/<>\\*|de'))
|
||||||
self.assertEqual(u'xxx', sanitize_filename(u'xxx/<>\\*|'))
|
self.assertEqual('xxx', sanitize_filename('xxx/<>\\*|'))
|
||||||
self.assertEqual(u'yes no', sanitize_filename(u'yes? no'))
|
self.assertEqual('yes no', sanitize_filename('yes? no'))
|
||||||
self.assertEqual(u'this - that', sanitize_filename(u'this: that'))
|
self.assertEqual('this - that', sanitize_filename('this: that'))
|
||||||
|
|
||||||
self.assertEqual(sanitize_filename(u'AT&T'), u'AT&T')
|
self.assertEqual(sanitize_filename('AT&T'), 'AT&T')
|
||||||
self.assertEqual(sanitize_filename(u'ä'), u'ä')
|
aumlaut = _compat_str('\xe4')
|
||||||
self.assertEqual(sanitize_filename(u'кириллица'), u'кириллица')
|
self.assertEqual(sanitize_filename(aumlaut), aumlaut)
|
||||||
|
tests = _compat_str('\u043a\u0438\u0440\u0438\u043b\u043b\u0438\u0446\u0430')
|
||||||
|
self.assertEqual(sanitize_filename(tests), tests)
|
||||||
|
|
||||||
forbidden = u'"\0\\/'
|
forbidden = '"\0\\/'
|
||||||
for fc in forbidden:
|
for fc in forbidden:
|
||||||
for fbc in forbidden:
|
for fbc in forbidden:
|
||||||
self.assertTrue(fbc not in sanitize_filename(fc))
|
self.assertTrue(fbc not in sanitize_filename(fc))
|
||||||
|
|
||||||
def test_sanitize_filename_restricted(self):
|
def test_sanitize_filename_restricted(self):
|
||||||
self.assertEqual(sanitize_filename(u'abc', restricted=True), u'abc')
|
self.assertEqual(sanitize_filename('abc', restricted=True), 'abc')
|
||||||
self.assertEqual(sanitize_filename(u'abc_d-e', restricted=True), u'abc_d-e')
|
self.assertEqual(sanitize_filename('abc_d-e', restricted=True), 'abc_d-e')
|
||||||
|
|
||||||
self.assertEqual(sanitize_filename(u'123', restricted=True), u'123')
|
self.assertEqual(sanitize_filename('123', restricted=True), '123')
|
||||||
|
|
||||||
self.assertEqual(u'abc_de', sanitize_filename(u'abc/de', restricted=True))
|
self.assertEqual('abc_de', sanitize_filename('abc/de', restricted=True))
|
||||||
self.assertFalse(u'/' in sanitize_filename(u'abc/de///', restricted=True))
|
self.assertFalse('/' in sanitize_filename('abc/de///', restricted=True))
|
||||||
|
|
||||||
self.assertEqual(u'abc_de', sanitize_filename(u'abc/<>\\*|de', restricted=True))
|
self.assertEqual('abc_de', sanitize_filename('abc/<>\\*|de', restricted=True))
|
||||||
self.assertEqual(u'xxx', sanitize_filename(u'xxx/<>\\*|', restricted=True))
|
self.assertEqual('xxx', sanitize_filename('xxx/<>\\*|', restricted=True))
|
||||||
self.assertEqual(u'yes_no', sanitize_filename(u'yes? no', restricted=True))
|
self.assertEqual('yes_no', sanitize_filename('yes? no', restricted=True))
|
||||||
self.assertEqual(u'this_-_that', sanitize_filename(u'this: that', restricted=True))
|
self.assertEqual('this_-_that', sanitize_filename('this: that', restricted=True))
|
||||||
|
|
||||||
self.assertEqual(sanitize_filename(u'aäb中国的c', restricted=True), u'a_b_c')
|
tests = _compat_str('a\xe4b\u4e2d\u56fd\u7684c')
|
||||||
self.assertTrue(sanitize_filename(u'ö', restricted=True) != u'') # No empty filename
|
self.assertEqual(sanitize_filename(tests, restricted=True), 'a_b_c')
|
||||||
|
self.assertTrue(sanitize_filename(_compat_str('\xf6'), restricted=True) != '') # No empty filename
|
||||||
|
|
||||||
forbidden = u'"\0\\/&!: \'\t\n'
|
forbidden = '"\0\\/&!: \'\t\n()[]{}$;`^,#'
|
||||||
for fc in forbidden:
|
for fc in forbidden:
|
||||||
for fbc in forbidden:
|
for fbc in forbidden:
|
||||||
self.assertTrue(fbc not in sanitize_filename(fc, restricted=True))
|
self.assertTrue(fbc not in sanitize_filename(fc, restricted=True))
|
||||||
|
|
||||||
# Handle a common case more neatly
|
# Handle a common case more neatly
|
||||||
self.assertEqual(sanitize_filename(u'大声带 - Song', restricted=True), u'Song')
|
self.assertEqual(sanitize_filename(_compat_str('\u5927\u58f0\u5e26 - Song'), restricted=True), 'Song')
|
||||||
self.assertEqual(sanitize_filename(u'总统: Speech', restricted=True), u'Speech')
|
self.assertEqual(sanitize_filename(_compat_str('\u603b\u7edf: Speech'), restricted=True), 'Speech')
|
||||||
# .. but make sure the file name is never empty
|
# .. but make sure the file name is never empty
|
||||||
self.assertTrue(sanitize_filename(u'-', restricted=True) != u'')
|
self.assertTrue(sanitize_filename('-', restricted=True) != '')
|
||||||
self.assertTrue(sanitize_filename(u':', restricted=True) != u'')
|
self.assertTrue(sanitize_filename(':', restricted=True) != '')
|
||||||
|
|
||||||
|
def test_sanitize_ids(self):
|
||||||
|
self.assertEqual(sanitize_filename('_n_cd26wFpw', is_id=True), '_n_cd26wFpw')
|
||||||
|
self.assertEqual(sanitize_filename('_BD_eEpuzXw', is_id=True), '_BD_eEpuzXw')
|
||||||
|
self.assertEqual(sanitize_filename('N0Y__7-UOdI', is_id=True), 'N0Y__7-UOdI')
|
||||||
|
|
||||||
def test_ordered_set(self):
|
def test_ordered_set(self):
|
||||||
self.assertEqual(orderedSet([1,1,2,3,4,4,5,6,7,3,5]), [1,2,3,4,5,6,7])
|
self.assertEqual(orderedSet([1, 1, 2, 3, 4, 4, 5, 6, 7, 3, 5]), [1, 2, 3, 4, 5, 6, 7])
|
||||||
self.assertEqual(orderedSet([]), [])
|
self.assertEqual(orderedSet([]), [])
|
||||||
self.assertEqual(orderedSet([1]), [1])
|
self.assertEqual(orderedSet([1]), [1])
|
||||||
#keep the list ordered
|
#keep the list ordered
|
||||||
self.assertEqual(orderedSet([135,1,1,1]), [135,1])
|
self.assertEqual(orderedSet([135, 1, 1, 1]), [135, 1])
|
||||||
|
|
||||||
def test_unescape_html(self):
|
def test_unescape_html(self):
|
||||||
self.assertEqual(unescapeHTML(u"%20;"), u"%20;")
|
self.assertEqual(unescapeHTML(_compat_str('%20;')), _compat_str('%20;'))
|
||||||
|
|
||||||
|
def test_daterange(self):
|
||||||
|
_20century = DateRange("19000101","20000101")
|
||||||
|
self.assertFalse("17890714" in _20century)
|
||||||
|
_ac = DateRange("00010101")
|
||||||
|
self.assertTrue("19690721" in _ac)
|
||||||
|
_firstmilenium = DateRange(end="10000101")
|
||||||
|
self.assertTrue("07110427" in _firstmilenium)
|
||||||
|
|
||||||
|
def test_unified_dates(self):
|
||||||
|
self.assertEqual(unified_strdate('December 21, 2010'), '20101221')
|
||||||
|
self.assertEqual(unified_strdate('8/7/2009'), '20090708')
|
||||||
|
self.assertEqual(unified_strdate('Dec 14, 2012'), '20121214')
|
||||||
|
self.assertEqual(unified_strdate('2012/10/11 01:56:38 +0000'), '20121011')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
|
77
test/test_write_info_json.py
Normal file
77
test/test_write_info_json.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# coding: utf-8
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
# Allow direct execution
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
import youtube_dl.FileDownloader
|
||||||
|
import youtube_dl.InfoExtractors
|
||||||
|
from youtube_dl.utils import *
|
||||||
|
|
||||||
|
PARAMETERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "parameters.json")
|
||||||
|
|
||||||
|
# General configuration (from __init__, not very elegant...)
|
||||||
|
jar = compat_cookiejar.CookieJar()
|
||||||
|
cookie_processor = compat_urllib_request.HTTPCookieProcessor(jar)
|
||||||
|
proxy_handler = compat_urllib_request.ProxyHandler()
|
||||||
|
opener = compat_urllib_request.build_opener(proxy_handler, cookie_processor, YoutubeDLHandler())
|
||||||
|
compat_urllib_request.install_opener(opener)
|
||||||
|
|
||||||
|
class FileDownloader(youtube_dl.FileDownloader):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
youtube_dl.FileDownloader.__init__(self, *args, **kwargs)
|
||||||
|
self.to_stderr = self.to_screen
|
||||||
|
|
||||||
|
with io.open(PARAMETERS_FILE, encoding='utf-8') as pf:
|
||||||
|
params = json.load(pf)
|
||||||
|
params['writeinfojson'] = True
|
||||||
|
params['skip_download'] = True
|
||||||
|
params['writedescription'] = True
|
||||||
|
|
||||||
|
TEST_ID = 'BaW_jenozKc'
|
||||||
|
INFO_JSON_FILE = TEST_ID + '.mp4.info.json'
|
||||||
|
DESCRIPTION_FILE = TEST_ID + '.mp4.description'
|
||||||
|
EXPECTED_DESCRIPTION = u'''test chars: "'/\ä↭𝕐
|
||||||
|
|
||||||
|
This is a test video for youtube-dl.
|
||||||
|
|
||||||
|
For more information, contact phihag@phihag.de .'''
|
||||||
|
|
||||||
|
class TestInfoJSON(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
# Clear old files
|
||||||
|
self.tearDown()
|
||||||
|
|
||||||
|
def test_info_json(self):
|
||||||
|
ie = youtube_dl.InfoExtractors.YoutubeIE()
|
||||||
|
fd = FileDownloader(params)
|
||||||
|
fd.add_info_extractor(ie)
|
||||||
|
fd.download([TEST_ID])
|
||||||
|
self.assertTrue(os.path.exists(INFO_JSON_FILE))
|
||||||
|
with io.open(INFO_JSON_FILE, 'r', encoding='utf-8') as jsonf:
|
||||||
|
jd = json.load(jsonf)
|
||||||
|
self.assertEqual(jd['upload_date'], u'20121002')
|
||||||
|
self.assertEqual(jd['description'], EXPECTED_DESCRIPTION)
|
||||||
|
self.assertEqual(jd['id'], TEST_ID)
|
||||||
|
self.assertEqual(jd['extractor'], 'youtube')
|
||||||
|
self.assertEqual(jd['title'], u'''youtube-dl test video "'/\ä↭𝕐''')
|
||||||
|
self.assertEqual(jd['uploader'], 'Philipp Hagemeister')
|
||||||
|
|
||||||
|
self.assertTrue(os.path.exists(DESCRIPTION_FILE))
|
||||||
|
with io.open(DESCRIPTION_FILE, 'r', encoding='utf-8') as descf:
|
||||||
|
descr = descf.read()
|
||||||
|
self.assertEqual(descr, EXPECTED_DESCRIPTION)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
if os.path.exists(INFO_JSON_FILE):
|
||||||
|
os.remove(INFO_JSON_FILE)
|
||||||
|
if os.path.exists(DESCRIPTION_FILE):
|
||||||
|
os.remove(DESCRIPTION_FILE)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
114
test/test_youtube_lists.py
Normal file
114
test/test_youtube_lists.py
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Allow direct execution
|
||||||
|
import os
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from youtube_dl.InfoExtractors import YoutubeUserIE, YoutubePlaylistIE, YoutubeIE, YoutubeChannelIE
|
||||||
|
from youtube_dl.utils import *
|
||||||
|
from youtube_dl.FileDownloader import FileDownloader
|
||||||
|
|
||||||
|
PARAMETERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "parameters.json")
|
||||||
|
with io.open(PARAMETERS_FILE, encoding='utf-8') as pf:
|
||||||
|
parameters = json.load(pf)
|
||||||
|
|
||||||
|
# General configuration (from __init__, not very elegant...)
|
||||||
|
jar = compat_cookiejar.CookieJar()
|
||||||
|
cookie_processor = compat_urllib_request.HTTPCookieProcessor(jar)
|
||||||
|
proxy_handler = compat_urllib_request.ProxyHandler()
|
||||||
|
opener = compat_urllib_request.build_opener(proxy_handler, cookie_processor, YoutubeDLHandler())
|
||||||
|
compat_urllib_request.install_opener(opener)
|
||||||
|
|
||||||
|
class FakeDownloader(FileDownloader):
|
||||||
|
def __init__(self):
|
||||||
|
self.result = []
|
||||||
|
self.params = parameters
|
||||||
|
def to_screen(self, s):
|
||||||
|
print(s)
|
||||||
|
def trouble(self, s, tb=None):
|
||||||
|
raise Exception(s)
|
||||||
|
def extract_info(self, url):
|
||||||
|
self.result.append(url)
|
||||||
|
return url
|
||||||
|
|
||||||
|
class TestYoutubeLists(unittest.TestCase):
|
||||||
|
def assertIsPlaylist(self,info):
|
||||||
|
"""Make sure the info has '_type' set to 'playlist'"""
|
||||||
|
self.assertEqual(info['_type'], 'playlist')
|
||||||
|
|
||||||
|
def test_youtube_playlist(self):
|
||||||
|
dl = FakeDownloader()
|
||||||
|
ie = YoutubePlaylistIE(dl)
|
||||||
|
result = ie.extract('https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re')[0]
|
||||||
|
self.assertIsPlaylist(result)
|
||||||
|
self.assertEqual(result['title'], 'ytdl test PL')
|
||||||
|
ytie_results = [YoutubeIE()._extract_id(url['url']) for url in result['entries']]
|
||||||
|
self.assertEqual(ytie_results, [ 'bV9L5Ht9LgY', 'FXxLjLQi3Fg', 'tU3Bgo5qJZE'])
|
||||||
|
|
||||||
|
def test_issue_673(self):
|
||||||
|
dl = FakeDownloader()
|
||||||
|
ie = YoutubePlaylistIE(dl)
|
||||||
|
result = ie.extract('PLBB231211A4F62143')[0]
|
||||||
|
self.assertTrue(len(result['entries']) > 25)
|
||||||
|
|
||||||
|
def test_youtube_playlist_long(self):
|
||||||
|
dl = FakeDownloader()
|
||||||
|
ie = YoutubePlaylistIE(dl)
|
||||||
|
result = ie.extract('https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q')[0]
|
||||||
|
self.assertIsPlaylist(result)
|
||||||
|
self.assertTrue(len(result['entries']) >= 799)
|
||||||
|
|
||||||
|
def test_youtube_playlist_with_deleted(self):
|
||||||
|
#651
|
||||||
|
dl = FakeDownloader()
|
||||||
|
ie = YoutubePlaylistIE(dl)
|
||||||
|
result = ie.extract('https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC')[0]
|
||||||
|
ytie_results = [YoutubeIE()._extract_id(url['url']) for url in result['entries']]
|
||||||
|
self.assertFalse('pElCt5oNDuI' in ytie_results)
|
||||||
|
self.assertFalse('KdPEApIVdWM' in ytie_results)
|
||||||
|
|
||||||
|
def test_youtube_playlist_empty(self):
|
||||||
|
dl = FakeDownloader()
|
||||||
|
ie = YoutubePlaylistIE(dl)
|
||||||
|
result = ie.extract('https://www.youtube.com/playlist?list=PLtPgu7CB4gbZDA7i_euNxn75ISqxwZPYx')[0]
|
||||||
|
self.assertIsPlaylist(result)
|
||||||
|
self.assertEqual(len(result['entries']), 0)
|
||||||
|
|
||||||
|
def test_youtube_course(self):
|
||||||
|
dl = FakeDownloader()
|
||||||
|
ie = YoutubePlaylistIE(dl)
|
||||||
|
# TODO find a > 100 (paginating?) videos course
|
||||||
|
result = ie.extract('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')[0]
|
||||||
|
entries = result['entries']
|
||||||
|
self.assertEqual(YoutubeIE()._extract_id(entries[0]['url']), 'j9WZyLZCBzs')
|
||||||
|
self.assertEqual(len(entries), 25)
|
||||||
|
self.assertEqual(YoutubeIE()._extract_id(entries[-1]['url']), 'rYefUsYuEp0')
|
||||||
|
|
||||||
|
def test_youtube_channel(self):
|
||||||
|
dl = FakeDownloader()
|
||||||
|
ie = YoutubeChannelIE(dl)
|
||||||
|
#test paginated channel
|
||||||
|
result = ie.extract('https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w')[0]
|
||||||
|
self.assertTrue(len(result['entries']) > 90)
|
||||||
|
#test autogenerated channel
|
||||||
|
result = ie.extract('https://www.youtube.com/channel/HCtnHdj3df7iM/videos')[0]
|
||||||
|
self.assertTrue(len(result['entries']) >= 18)
|
||||||
|
|
||||||
|
def test_youtube_user(self):
|
||||||
|
dl = FakeDownloader()
|
||||||
|
ie = YoutubeUserIE(dl)
|
||||||
|
result = ie.extract('https://www.youtube.com/user/TheLinuxFoundation')[0]
|
||||||
|
self.assertTrue(len(result['entries']) >= 320)
|
||||||
|
|
||||||
|
def test_youtube_safe_search(self):
|
||||||
|
dl = FakeDownloader()
|
||||||
|
ie = YoutubePlaylistIE(dl)
|
||||||
|
result = ie.extract('PLtPgu7CB4gbY9oDN3drwC3cMbJggS7dKl')[0]
|
||||||
|
self.assertEqual(len(result['entries']), 2)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
111
test/test_youtube_subtitles.py
Normal file
111
test/test_youtube_subtitles.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
import json
|
||||||
|
import io
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
# Allow direct execution
|
||||||
|
import os
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from youtube_dl.InfoExtractors import YoutubeIE
|
||||||
|
from youtube_dl.utils import *
|
||||||
|
from youtube_dl import FileDownloader
|
||||||
|
|
||||||
|
PARAMETERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "parameters.json")
|
||||||
|
with io.open(PARAMETERS_FILE, encoding='utf-8') as pf:
|
||||||
|
parameters = json.load(pf)
|
||||||
|
|
||||||
|
# General configuration (from __init__, not very elegant...)
|
||||||
|
jar = compat_cookiejar.CookieJar()
|
||||||
|
cookie_processor = compat_urllib_request.HTTPCookieProcessor(jar)
|
||||||
|
proxy_handler = compat_urllib_request.ProxyHandler()
|
||||||
|
opener = compat_urllib_request.build_opener(proxy_handler, cookie_processor, YoutubeDLHandler())
|
||||||
|
compat_urllib_request.install_opener(opener)
|
||||||
|
|
||||||
|
class FakeDownloader(FileDownloader):
|
||||||
|
def __init__(self):
|
||||||
|
self.result = []
|
||||||
|
# Different instances of the downloader can't share the same dictionary
|
||||||
|
# some test set the "sublang" parameter, which would break the md5 checks.
|
||||||
|
self.params = dict(parameters)
|
||||||
|
def to_screen(self, s):
|
||||||
|
print(s)
|
||||||
|
def trouble(self, s, tb=None):
|
||||||
|
raise Exception(s)
|
||||||
|
def download(self, x):
|
||||||
|
self.result.append(x)
|
||||||
|
|
||||||
|
md5 = lambda s: hashlib.md5(s.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
|
class TestYoutubeSubtitles(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
DL = FakeDownloader()
|
||||||
|
DL.params['allsubtitles'] = False
|
||||||
|
DL.params['writesubtitles'] = False
|
||||||
|
DL.params['subtitlesformat'] = 'srt'
|
||||||
|
DL.params['listsubtitles'] = False
|
||||||
|
def test_youtube_no_subtitles(self):
|
||||||
|
DL = FakeDownloader()
|
||||||
|
DL.params['writesubtitles'] = False
|
||||||
|
IE = YoutubeIE(DL)
|
||||||
|
info_dict = IE.extract('QRS8MkLhQmM')
|
||||||
|
subtitles = info_dict[0]['subtitles']
|
||||||
|
self.assertEqual(subtitles, None)
|
||||||
|
def test_youtube_subtitles(self):
|
||||||
|
DL = FakeDownloader()
|
||||||
|
DL.params['writesubtitles'] = True
|
||||||
|
IE = YoutubeIE(DL)
|
||||||
|
info_dict = IE.extract('QRS8MkLhQmM')
|
||||||
|
sub = info_dict[0]['subtitles'][0]
|
||||||
|
self.assertEqual(md5(sub[2]), '4cd9278a35ba2305f47354ee13472260')
|
||||||
|
def test_youtube_subtitles_it(self):
|
||||||
|
DL = FakeDownloader()
|
||||||
|
DL.params['writesubtitles'] = True
|
||||||
|
DL.params['subtitleslang'] = 'it'
|
||||||
|
IE = YoutubeIE(DL)
|
||||||
|
info_dict = IE.extract('QRS8MkLhQmM')
|
||||||
|
sub = info_dict[0]['subtitles'][0]
|
||||||
|
self.assertEqual(md5(sub[2]), '164a51f16f260476a05b50fe4c2f161d')
|
||||||
|
def test_youtube_onlysubtitles(self):
|
||||||
|
DL = FakeDownloader()
|
||||||
|
DL.params['writesubtitles'] = True
|
||||||
|
DL.params['onlysubtitles'] = True
|
||||||
|
IE = YoutubeIE(DL)
|
||||||
|
info_dict = IE.extract('QRS8MkLhQmM')
|
||||||
|
sub = info_dict[0]['subtitles'][0]
|
||||||
|
self.assertEqual(md5(sub[2]), '4cd9278a35ba2305f47354ee13472260')
|
||||||
|
def test_youtube_allsubtitles(self):
|
||||||
|
DL = FakeDownloader()
|
||||||
|
DL.params['allsubtitles'] = True
|
||||||
|
IE = YoutubeIE(DL)
|
||||||
|
info_dict = IE.extract('QRS8MkLhQmM')
|
||||||
|
subtitles = info_dict[0]['subtitles']
|
||||||
|
self.assertEqual(len(subtitles), 13)
|
||||||
|
def test_youtube_subtitles_format(self):
|
||||||
|
DL = FakeDownloader()
|
||||||
|
DL.params['writesubtitles'] = True
|
||||||
|
DL.params['subtitlesformat'] = 'sbv'
|
||||||
|
IE = YoutubeIE(DL)
|
||||||
|
info_dict = IE.extract('QRS8MkLhQmM')
|
||||||
|
sub = info_dict[0]['subtitles'][0]
|
||||||
|
self.assertEqual(md5(sub[2]), '13aeaa0c245a8bed9a451cb643e3ad8b')
|
||||||
|
def test_youtube_list_subtitles(self):
|
||||||
|
DL = FakeDownloader()
|
||||||
|
DL.params['listsubtitles'] = True
|
||||||
|
IE = YoutubeIE(DL)
|
||||||
|
info_dict = IE.extract('QRS8MkLhQmM')
|
||||||
|
self.assertEqual(info_dict, None)
|
||||||
|
def test_youtube_automatic_captions(self):
|
||||||
|
DL = FakeDownloader()
|
||||||
|
DL.params['writesubtitles'] = True
|
||||||
|
DL.params['subtitleslang'] = 'it'
|
||||||
|
IE = YoutubeIE(DL)
|
||||||
|
info_dict = IE.extract('8YoUxe5ncPo')
|
||||||
|
sub = info_dict[0]['subtitles'][0]
|
||||||
|
self.assertTrue(sub[2] is not None)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
634
test/tests.json
Normal file
634
test/tests.json
Normal file
@@ -0,0 +1,634 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Youtube",
|
||||||
|
"url": "http://www.youtube.com/watch?v=BaW_jenozKc",
|
||||||
|
"file": "BaW_jenozKc.mp4",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "youtube-dl test video \"'/\\ä↭𝕐",
|
||||||
|
"uploader": "Philipp Hagemeister",
|
||||||
|
"uploader_id": "phihag",
|
||||||
|
"upload_date": "20121002",
|
||||||
|
"description": "test chars: \"'/\\ä↭𝕐\n\nThis is a test video for youtube-dl.\n\nFor more information, contact phihag@phihag.de ."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Dailymotion",
|
||||||
|
"md5": "392c4b85a60a90dc4792da41ce3144eb",
|
||||||
|
"url": "http://www.dailymotion.com/video/x33vw9_tutoriel-de-youtubeur-dl-des-video_tech",
|
||||||
|
"file": "x33vw9.mp4",
|
||||||
|
"info_dict": {
|
||||||
|
"uploader": "Alex and Van .",
|
||||||
|
"title": "Tutoriel de Youtubeur\"DL DES VIDEO DE YOUTUBE\""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Metacafe",
|
||||||
|
"add_ie": ["Youtube"],
|
||||||
|
"url": "http://metacafe.com/watch/yt-_aUehQsCQtM/the_electric_company_short_i_pbs_kids_go/",
|
||||||
|
"file": "_aUehQsCQtM.flv",
|
||||||
|
"info_dict": {
|
||||||
|
"upload_date": "20090102",
|
||||||
|
"title": "The Electric Company | \"Short I\" | PBS KIDS GO!",
|
||||||
|
"description": "md5:2439a8ef6d5a70e380c22f5ad323e5a8",
|
||||||
|
"uploader": "PBS",
|
||||||
|
"uploader_id": "PBS"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BlipTV",
|
||||||
|
"md5": "b2d849efcf7ee18917e4b4d9ff37cafe",
|
||||||
|
"url": "http://blip.tv/cbr/cbr-exclusive-gotham-city-imposters-bats-vs-jokerz-short-3-5796352",
|
||||||
|
"file": "5779306.m4v",
|
||||||
|
"info_dict": {
|
||||||
|
"upload_date": "20111205",
|
||||||
|
"description": "md5:9bc31f227219cde65e47eeec8d2dc596",
|
||||||
|
"uploader": "Comic Book Resources - CBR TV",
|
||||||
|
"title": "CBR EXCLUSIVE: \"Gotham City Imposters\" Bats VS Jokerz Short 3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "XVideos",
|
||||||
|
"md5": "1d0c835822f0a71a7bf011855db929d0",
|
||||||
|
"url": "http://www.xvideos.com/video939581/funny_porns_by_s_-1",
|
||||||
|
"file": "939581.flv",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "Funny Porns By >>>>S<<<<<< -1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "YouPorn",
|
||||||
|
"md5": "c37ddbaaa39058c76a7e86c6813423c1",
|
||||||
|
"url": "http://www.youporn.com/watch/505835/sex-ed-is-it-safe-to-masturbate-daily/",
|
||||||
|
"file": "505835.mp4",
|
||||||
|
"info_dict": {
|
||||||
|
"upload_date": "20101221",
|
||||||
|
"description": "Love & Sex Answers: http://bit.ly/DanAndJenn -- Is It Unhealthy To Masturbate Daily?",
|
||||||
|
"uploader": "Ask Dan And Jennifer",
|
||||||
|
"title": "Sex Ed: Is It Safe To Masturbate Daily?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pornotube",
|
||||||
|
"md5": "374dd6dcedd24234453b295209aa69b6",
|
||||||
|
"url": "http://pornotube.com/c/173/m/1689755/Marilyn-Monroe-Bathing",
|
||||||
|
"file": "1689755.flv",
|
||||||
|
"info_dict": {
|
||||||
|
"upload_date": "20090708",
|
||||||
|
"title": "Marilyn-Monroe-Bathing"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "YouJizz",
|
||||||
|
"md5": "07e15fa469ba384c7693fd246905547c",
|
||||||
|
"url": "http://www.youjizz.com/videos/zeichentrick-1-2189178.html",
|
||||||
|
"file": "2189178.flv",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "Zeichentrick 1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Vimeo",
|
||||||
|
"md5": "8879b6cc097e987f02484baf890129e5",
|
||||||
|
"url": "http://vimeo.com/56015672",
|
||||||
|
"file": "56015672.mp4",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "youtube-dl test video - ★ \" ' 幸 / \\ ä ↭ 𝕐",
|
||||||
|
"uploader": "Filippo Valsorda",
|
||||||
|
"uploader_id": "user7108434",
|
||||||
|
"upload_date": "20121220",
|
||||||
|
"description": "This is a test case for youtube-dl.\nFor more information, see github.com/rg3/youtube-dl\nTest chars: ★ \" ' 幸 / \\ ä ↭ 𝕐"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Soundcloud",
|
||||||
|
"md5": "ebef0a451b909710ed1d7787dddbf0d7",
|
||||||
|
"url": "http://soundcloud.com/ethmusic/lostin-powers-she-so-heavy",
|
||||||
|
"file": "62986583.mp3",
|
||||||
|
"info_dict": {
|
||||||
|
"upload_date": "20121011",
|
||||||
|
"description": "No Downloads untill we record the finished version this weekend, i was too pumped n i had to post it , earl is prolly gonna b hella p.o'd",
|
||||||
|
"uploader": "E.T. ExTerrestrial Music",
|
||||||
|
"title": "Lostin Powers - She so Heavy (SneakPreview) Adrian Ackers Blueprint 1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "StanfordOpenClassroom",
|
||||||
|
"md5": "544a9468546059d4e80d76265b0443b8",
|
||||||
|
"url": "http://openclassroom.stanford.edu/MainFolder/VideoPage.php?course=PracticalUnix&video=intro-environment&speed=100",
|
||||||
|
"file": "PracticalUnix_intro-environment.mp4",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "Intro Environment"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "XNXX",
|
||||||
|
"md5": "0831677e2b4761795f68d417e0b7b445",
|
||||||
|
"url": "http://video.xnxx.com/video1135332/lida_naked_funny_actress_5_",
|
||||||
|
"file": "1135332.flv",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "lida » Naked Funny Actress (5)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Youku",
|
||||||
|
"url": "http://v.youku.com/v_show/id_XNDgyMDQ2NTQw.html",
|
||||||
|
"file": "XNDgyMDQ2NTQw_part00.flv",
|
||||||
|
"md5": "ffe3f2e435663dc2d1eea34faeff5b5b",
|
||||||
|
"params": { "test": false },
|
||||||
|
"info_dict": {
|
||||||
|
"title": "youtube-dl test video \"'/\\ä↭𝕐"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NBA",
|
||||||
|
"url": "http://www.nba.com/video/games/nets/2012/12/04/0021200253-okc-bkn-recap.nba/index.html",
|
||||||
|
"file": "0021200253-okc-bkn-recap.nba.mp4",
|
||||||
|
"md5": "c0edcfc37607344e2ff8f13c378c88a4",
|
||||||
|
"info_dict": {
|
||||||
|
"description": "Kevin Durant scores 32 points and dishes out six assists as the Thunder beat the Nets in Brooklyn.",
|
||||||
|
"title": "Thunder vs. Nets"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "JustinTV",
|
||||||
|
"url": "http://www.twitch.tv/thegamedevhub/b/296128360",
|
||||||
|
"file": "296128360.flv",
|
||||||
|
"md5": "ecaa8a790c22a40770901460af191c9a",
|
||||||
|
"info_dict": {
|
||||||
|
"upload_date": "20110927",
|
||||||
|
"uploader_id": 25114803,
|
||||||
|
"uploader": "thegamedevhub",
|
||||||
|
"title": "Beginner Series - Scripting With Python Pt.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MyVideo",
|
||||||
|
"url": "http://www.myvideo.de/watch/8229274/bowling_fail_or_win",
|
||||||
|
"file": "8229274.flv",
|
||||||
|
"md5": "2d2753e8130479ba2cb7e0a37002053e",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "bowling-fail-or-win"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Escapist",
|
||||||
|
"url": "http://www.escapistmagazine.com/videos/view/the-escapist-presents/6618-Breaking-Down-Baldurs-Gate",
|
||||||
|
"file": "6618-Breaking-Down-Baldurs-Gate.mp4",
|
||||||
|
"md5": "c6793dbda81388f4264c1ba18684a74d",
|
||||||
|
"info_dict": {
|
||||||
|
"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": "the-escapist-presents",
|
||||||
|
"title": "Breaking Down Baldur's Gate"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GooglePlus",
|
||||||
|
"url": "https://plus.google.com/u/0/108897254135232129896/posts/ZButuJc6CtH",
|
||||||
|
"file": "ZButuJc6CtH.flv",
|
||||||
|
"info_dict": {
|
||||||
|
"upload_date": "20120613",
|
||||||
|
"uploader": "井上ヨシマサ",
|
||||||
|
"title": "嘆きの天使 降臨"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FunnyOrDie",
|
||||||
|
"url": "http://www.funnyordie.com/videos/0732f586d7/heart-shaped-box-literal-video-version",
|
||||||
|
"file": "0732f586d7.mp4",
|
||||||
|
"md5": "f647e9e90064b53b6e046e75d0241fbd",
|
||||||
|
"info_dict": {
|
||||||
|
"description": "Lyrics changed to match the video. Spoken cameo by Obscurus Lupa (from ThatGuyWithTheGlasses.com). Based on a concept by Dustin McLean (DustFilms.com). Performed, edited, and written by David A. Scott.",
|
||||||
|
"title": "Heart-Shaped Box: Literal Video Version"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Steam",
|
||||||
|
"url": "http://store.steampowered.com/video/105600/",
|
||||||
|
"playlist": [
|
||||||
|
{
|
||||||
|
"file": "81300.flv",
|
||||||
|
"md5": "f870007cee7065d7c76b88f0a45ecc07",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "Terraria 1.1 Trailer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "80859.flv",
|
||||||
|
"md5": "61aaf31a5c5c3041afb58fb83cbb5751",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "Terraria Trailer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ustream",
|
||||||
|
"url": "http://www.ustream.tv/recorded/20274954",
|
||||||
|
"file": "20274954.flv",
|
||||||
|
"md5": "088f151799e8f572f84eb62f17d73e5c",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "Young Americans for Liberty February 7, 2012 2:28 AM",
|
||||||
|
"uploader": "Young Americans for Liberty"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "InfoQ",
|
||||||
|
"url": "http://www.infoq.com/presentations/A-Few-of-My-Favorite-Python-Things",
|
||||||
|
"file": "12-jan-pythonthings.mp4",
|
||||||
|
"info_dict": {
|
||||||
|
"description": "Mike Pirnat presents some tips and tricks, standard libraries and third party packages that make programming in Python a richer experience.",
|
||||||
|
"title": "A Few of My Favorite [Python] Things"
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"skip_download": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ComedyCentral",
|
||||||
|
"url": "http://www.thedailyshow.com/watch/thu-december-13-2012/kristen-stewart",
|
||||||
|
"file": "422212.mp4",
|
||||||
|
"md5": "4e2f5cb088a83cd8cdb7756132f9739d",
|
||||||
|
"info_dict": {
|
||||||
|
"upload_date": "20121214",
|
||||||
|
"description": "Kristen Stewart",
|
||||||
|
"uploader": "thedailyshow",
|
||||||
|
"title": "thedailyshow-kristen-stewart part 1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RBMARadio",
|
||||||
|
"url": "http://www.rbmaradio.com/shows/ford-lopatin-live-at-primavera-sound-2011",
|
||||||
|
"file": "ford-lopatin-live-at-primavera-sound-2011.mp3",
|
||||||
|
"md5": "6bc6f9bcb18994b4c983bc3bf4384d95",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "Live at Primavera Sound 2011",
|
||||||
|
"description": "Joel Ford and Daniel \u2019Oneohtrix Point Never\u2019 Lopatin fly their midified pop extravaganza to Spain. Live at Primavera Sound 2011.",
|
||||||
|
"uploader": "Ford & Lopatin",
|
||||||
|
"uploader_id": "ford-lopatin",
|
||||||
|
"location": "Spain"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Facebook",
|
||||||
|
"url": "https://www.facebook.com/photo.php?v=120708114770723",
|
||||||
|
"file": "120708114770723.mp4",
|
||||||
|
"md5": "48975a41ccc4b7a581abd68651c1a5a8",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "PEOPLE ARE AWESOME 2013",
|
||||||
|
"duration": 279
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "EightTracks",
|
||||||
|
"url": "http://8tracks.com/ytdl/youtube-dl-test-tracks-a",
|
||||||
|
"playlist": [
|
||||||
|
{
|
||||||
|
"file": "11885610.m4a",
|
||||||
|
"md5": "96ce57f24389fc8734ce47f4c1abcc55",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "youtue-dl project<>\"' - youtube-dl test track 1 \"'/\\\u00e4\u21ad",
|
||||||
|
"uploader_id": "ytdl"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "11885608.m4a",
|
||||||
|
"md5": "4ab26f05c1f7291ea460a3920be8021f",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "youtube-dl project - youtube-dl test track 2 \"'/\\\u00e4\u21ad",
|
||||||
|
"uploader_id": "ytdl"
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "11885679.m4a",
|
||||||
|
"md5": "d30b5b5f74217410f4689605c35d1fd7",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "youtube-dl project as well - youtube-dl test track 3 \"'/\\\u00e4\u21ad",
|
||||||
|
"uploader_id": "ytdl"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "11885680.m4a",
|
||||||
|
"md5": "4eb0a669317cd725f6bbd336a29f923a",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "youtube-dl project as well - youtube-dl test track 4 \"'/\\\u00e4\u21ad",
|
||||||
|
"uploader_id": "ytdl"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "11885682.m4a",
|
||||||
|
"md5": "1893e872e263a2705558d1d319ad19e8",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "PH - youtube-dl test track 5 \"'/\\\u00e4\u21ad",
|
||||||
|
"uploader_id": "ytdl"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "11885683.m4a",
|
||||||
|
"md5": "b673c46f47a216ab1741ae8836af5899",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "PH - youtube-dl test track 6 \"'/\\\u00e4\u21ad",
|
||||||
|
"uploader_id": "ytdl"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "11885684.m4a",
|
||||||
|
"md5": "1d74534e95df54986da7f5abf7d842b7",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "phihag - youtube-dl test track 7 \"'/\\\u00e4\u21ad",
|
||||||
|
"uploader_id": "ytdl"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "11885685.m4a",
|
||||||
|
"md5": "f081f47af8f6ae782ed131d38b9cd1c0",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "phihag - youtube-dl test track 8 \"'/\\\u00e4\u21ad",
|
||||||
|
"uploader_id": "ytdl"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Keek",
|
||||||
|
"url": "http://www.keek.com/ytdl/keeks/NODfbab",
|
||||||
|
"file": "NODfbab.mp4",
|
||||||
|
"md5": "9b0636f8c0f7614afa4ea5e4c6e57e83",
|
||||||
|
"info_dict": {
|
||||||
|
"uploader": "ytdl",
|
||||||
|
"title": "test chars: \"'/\\ä<>This is a test video for youtube-dl.For more information, contact phihag@phihag.de ."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TED",
|
||||||
|
"url": "http://www.ted.com/talks/dan_dennett_on_our_consciousness.html",
|
||||||
|
"file": "102.mp4",
|
||||||
|
"md5": "8cd9dfa41ee000ce658fd48fb5d89a61",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "Dan Dennett: The illusion of consciousness",
|
||||||
|
"description": "md5:c6fa72e6eedbd938c9caf6b2702f5922"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MySpass",
|
||||||
|
"url": "http://www.myspass.de/myspass/shows/tvshows/absolute-mehrheit/Absolute-Mehrheit-vom-17022013-Die-Highlights-Teil-2--/11741/",
|
||||||
|
"file": "11741.mp4",
|
||||||
|
"md5": "0b49f4844a068f8b33f4b7c88405862b",
|
||||||
|
"info_dict": {
|
||||||
|
"description": "Wer kann in die Fußstapfen von Wolfgang Kubicki treten und die Mehrheit der Zuschauer hinter sich versammeln? Wird vielleicht sogar die Absolute Mehrheit geknackt und der Jackpot von 200.000 Euro mit nach Hause genommen?",
|
||||||
|
"title": "Absolute Mehrheit vom 17.02.2013 - Die Highlights, Teil 2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Generic",
|
||||||
|
"url": "http://www.hodiho.fr/2013/02/regis-plante-sa-jeep.html",
|
||||||
|
"file": "13601338388002.mp4",
|
||||||
|
"md5": "85b90ccc9d73b4acd9138d3af4c27f89",
|
||||||
|
"info_dict": {
|
||||||
|
"uploader": "www.hodiho.fr",
|
||||||
|
"title": "Régis plante sa Jeep"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Spiegel",
|
||||||
|
"url": "http://www.spiegel.de/video/vulkan-tungurahua-in-ecuador-ist-wieder-aktiv-video-1259285.html",
|
||||||
|
"file": "1259285.mp4",
|
||||||
|
"md5": "2c2754212136f35fb4b19767d242f66e",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "Vulkanausbruch in Ecuador: Der \"Feuerschlund\" ist wieder aktiv"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "LiveLeak",
|
||||||
|
"md5": "0813c2430bea7a46bf13acf3406992f4",
|
||||||
|
"url": "http://www.liveleak.com/view?i=757_1364311680",
|
||||||
|
"file": "757_1364311680.mp4",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "Most unlucky car accident",
|
||||||
|
"description": "extremely bad day for this guy..!",
|
||||||
|
"uploader": "ljfriel2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WorldStarHipHop",
|
||||||
|
"url": "http://www.worldstarhiphop.com/videos/video.php?v=wshh6a7q1ny0G34ZwuIO",
|
||||||
|
"file": "wshh6a7q1ny0G34ZwuIO.mp4",
|
||||||
|
"md5": "9d04de741161603bf7071bbf4e883186",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "Video: KO Of The Week: MMA Fighter Gets Knocked Out By Swift Head Kick!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ARD",
|
||||||
|
"url": "http://www.ardmediathek.de/das-erste/tagesschau-in-100-sek?documentId=14077640",
|
||||||
|
"file": "14077640.mp4",
|
||||||
|
"md5": "6ca8824255460c787376353f9e20bbd8",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "11.04.2013 09:23 Uhr - Tagesschau in 100 Sekunden"
|
||||||
|
},
|
||||||
|
"skip": "Requires rtmpdump"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Tumblr",
|
||||||
|
"url": "http://resigno.tumblr.com/post/53364321212/e-de-extrema-importancia-que-esse-video-seja",
|
||||||
|
"file": "53364321212.mp4",
|
||||||
|
"md5": "0716d3dd51baf68a28b40fdf1251494e",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "Rafael Lemos | Tumblr"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SoundcloudSet",
|
||||||
|
"url":"https://soundcloud.com/the-concept-band/sets/the-royal-concept-ep",
|
||||||
|
"playlist":[
|
||||||
|
{
|
||||||
|
"file":"30510138.mp3",
|
||||||
|
"md5":"f9136bf103901728f29e419d2c70f55d",
|
||||||
|
"info_dict": {
|
||||||
|
"upload_date": "20111213",
|
||||||
|
"description": "The Royal Concept from Stockholm\r\nFilip / Povel / David / Magnus\r\nwww.royalconceptband.com",
|
||||||
|
"uploader": "The Royal Concept",
|
||||||
|
"title": "D-D-Dance"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file":"47127625.mp3",
|
||||||
|
"md5":"09b6758a018470570f8fd423c9453dd8",
|
||||||
|
"info_dict": {
|
||||||
|
"upload_date": "20120521",
|
||||||
|
"description": "The Royal Concept from Stockholm\r\nFilip / Povel / David / Magnus\r\nwww.royalconceptband.com",
|
||||||
|
"uploader": "The Royal Concept",
|
||||||
|
"title": "The Royal Concept - Gimme Twice"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file":"47127627.mp3",
|
||||||
|
"md5":"154abd4e418cea19c3b901f1e1306d9c",
|
||||||
|
"info_dict": {
|
||||||
|
"upload_date": "20120521",
|
||||||
|
"uploader": "The Royal Concept",
|
||||||
|
"title": "Goldrushed"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file":"47127629.mp3",
|
||||||
|
"md5":"2f5471edc79ad3f33a683153e96a79c1",
|
||||||
|
"info_dict": {
|
||||||
|
"upload_date": "20120521",
|
||||||
|
"description": "The Royal Concept from Stockholm\r\nFilip / Povel / David / Magnus\r\nwww.royalconceptband.com",
|
||||||
|
"uploader": "The Royal Concept",
|
||||||
|
"title": "In the End"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file":"47127631.mp3",
|
||||||
|
"md5":"f9ba87aa940af7213f98949254f1c6e2",
|
||||||
|
"info_dict": {
|
||||||
|
"upload_date": "20120521",
|
||||||
|
"description": "The Royal Concept from Stockholm\r\nFilip / David / Povel / Magnus\r\nwww.theroyalconceptband.com",
|
||||||
|
"uploader": "The Royal Concept",
|
||||||
|
"title": "Knocked Up"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file":"75206121.mp3",
|
||||||
|
"md5":"f9d1fe9406717e302980c30de4af9353",
|
||||||
|
"info_dict": {
|
||||||
|
"upload_date": "20130116",
|
||||||
|
"description": "The unreleased track World on Fire premiered on the CW's hit show Arrow (8pm/7pm central). \r\nAs a gift to our fans we would like to offer you a free download of the track! ",
|
||||||
|
"uploader": "The Royal Concept",
|
||||||
|
"title": "World On Fire"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"Bandcamp",
|
||||||
|
"url":"http://youtube-dl.bandcamp.com/track/youtube-dl-test-song",
|
||||||
|
"file":"1812978515.mp3",
|
||||||
|
"md5":"cdeb30cdae1921719a3cbcab696ef53c",
|
||||||
|
"info_dict": {
|
||||||
|
"title":"youtube-dl test song \"'/\\ä↭"
|
||||||
|
},
|
||||||
|
"skip": "There is a limit of 200 free downloads / month for the test song"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RedTube",
|
||||||
|
"url": "http://www.redtube.com/66418",
|
||||||
|
"file": "66418.mp4",
|
||||||
|
"md5": "7b8c22b5e7098a3e1c09709df1126d2d",
|
||||||
|
"info_dict":{
|
||||||
|
"title":"Sucked on a toilet"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Photobucket",
|
||||||
|
"url": "http://media.photobucket.com/user/rachaneronas/media/TiredofLinkBuildingTryBacklinkMyDomaincom_zpsc0c3b9fa.mp4.html?filters[term]=search&filters[primary]=videos&filters[secondary]=images&sort=1&o=0",
|
||||||
|
"file": "zpsc0c3b9fa.mp4",
|
||||||
|
"md5": "7dabfb92b0a31f6c16cebc0f8e60ff99",
|
||||||
|
"info_dict": {
|
||||||
|
"upload_date": "20130504",
|
||||||
|
"uploader": "rachaneronas",
|
||||||
|
"title": "Tired of Link Building? Try BacklinkMyDomain.com!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ina",
|
||||||
|
"url": "www.ina.fr/video/I12055569/francois-hollande-je-crois-que-c-est-clair-video.html",
|
||||||
|
"file": "I12055569.mp4",
|
||||||
|
"md5": "a667021bf2b41f8dc6049479d9bb38a3",
|
||||||
|
"info_dict":{
|
||||||
|
"title":"François Hollande \"Je crois que c'est clair\""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Yahoo",
|
||||||
|
"url": "http://screen.yahoo.com/julian-smith-travis-legg-watch-214727115.html",
|
||||||
|
"file": "214727115.flv",
|
||||||
|
"md5": "2e717f169c1be93d84d3794a00d4a325",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "Julian Smith & Travis Legg Watch Julian Smith"
|
||||||
|
},
|
||||||
|
"skip": "Requires rtmpdump"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Howcast",
|
||||||
|
"url": "http://www.howcast.com/videos/390161-How-to-Tie-a-Square-Knot-Properly",
|
||||||
|
"file": "390161.mp4",
|
||||||
|
"md5": "1d7ba54e2c9d7dc6935ef39e00529138",
|
||||||
|
"info_dict":{
|
||||||
|
"title":"How to Tie a Square Knot Properly",
|
||||||
|
"description":"The square knot, also known as the reef knot, is one of the oldest, most basic knots to tie, and can be used in many different ways. Here's the proper way to tie a square knot."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Vine",
|
||||||
|
"url": "https://vine.co/v/b9KOOWX7HUx",
|
||||||
|
"file": "b9KOOWX7HUx.mp4",
|
||||||
|
"md5": "2f36fed6235b16da96ce9b4dc890940d",
|
||||||
|
"info_dict":{
|
||||||
|
"title": "Chicken.",
|
||||||
|
"uploader": "Jack Dorsey"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Flickr",
|
||||||
|
"url": "http://www.flickr.com/photos/forestwander-nature-pictures/5645318632/in/photostream/",
|
||||||
|
"file": "5645318632.mp4",
|
||||||
|
"md5": "6fdc01adbc89d72fc9c4f15b4a4ba87b",
|
||||||
|
"info_dict":{
|
||||||
|
"title": "Dark Hollow Waterfalls",
|
||||||
|
"uploader_id": "forestwander-nature-pictures",
|
||||||
|
"description": "Waterfalls in the Springtime at Dark Hollow Waterfalls. These are located just off of Skyline Drive in Virginia. They are only about 6/10 of a mile hike but it is a pretty steep hill and a good climb back up."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Teamcoco",
|
||||||
|
"url": "http://teamcoco.com/video/louis-ck-interview-george-w-bush",
|
||||||
|
"file": "19705.mp4",
|
||||||
|
"md5": "27b6f7527da5acf534b15f21b032656e",
|
||||||
|
"info_dict":{
|
||||||
|
"title": "Louis C.K. Interview Pt. 1 11/3/11",
|
||||||
|
"description": "Louis C.K. got starstruck by George W. Bush, so what? Part one."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "XHamster",
|
||||||
|
"url": "http://xhamster.com/movies/1509445/femaleagent_shy_beauty_takes_the_bait.html",
|
||||||
|
"file": "1509445.flv",
|
||||||
|
"md5": "9f48e0e8d58e3076bb236ff412ab62fa",
|
||||||
|
"info_dict": {
|
||||||
|
"upload_date": "20121014",
|
||||||
|
"uploader_id": "Ruseful2011",
|
||||||
|
"title": "FemaleAgent Shy beauty takes the bait"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Hypem",
|
||||||
|
"url": "http://hypem.com/track/1v6ga/BODYWORK+-+TAME",
|
||||||
|
"file": "1v6ga.mp3",
|
||||||
|
"md5": "b9cc91b5af8995e9f0c1cee04c575828",
|
||||||
|
"info_dict":{
|
||||||
|
"title":"Tame"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Vbox7",
|
||||||
|
"url": "http://vbox7.com/play:249bb972c2",
|
||||||
|
"file": "249bb972c2.flv",
|
||||||
|
"md5": "9c70d6d956f888bdc08c124acc120cfe",
|
||||||
|
"info_dict":{
|
||||||
|
"title":"Смях! Чудо - чист за секунди - Скрита камера"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gametrailers",
|
||||||
|
"url": "http://www.gametrailers.com/videos/zbvr8i/mirror-s-edge-2-e3-2013--debut-trailer",
|
||||||
|
"file": "zbvr8i.flv",
|
||||||
|
"md5": "c3edbc995ab4081976e16779bd96a878",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "E3 2013: Debut Trailer"
|
||||||
|
},
|
||||||
|
"skip": "Requires rtmpdump"
|
||||||
|
}
|
||||||
|
]
|
BIN
youtube-dl
BIN
youtube-dl
Binary file not shown.
306
youtube-dl.1
306
youtube-dl.1
@@ -1,306 +0,0 @@
|
|||||||
.TH YOUTUBE-DL 1 ""
|
|
||||||
.SH NAME
|
|
||||||
.PP
|
|
||||||
youtube-dl
|
|
||||||
.SH SYNOPSIS
|
|
||||||
.PP
|
|
||||||
\f[B]youtube-dl\f[] [OPTIONS] URL [URL...]
|
|
||||||
.SH DESCRIPTION
|
|
||||||
.PP
|
|
||||||
\f[B]youtube-dl\f[] is a small command-line program to download videos
|
|
||||||
from YouTube.com and a few more sites.
|
|
||||||
It requires the Python interpreter, version 2.x (x being at least 6),
|
|
||||||
and it is not platform specific.
|
|
||||||
It should work in your Unix box, in Windows or in Mac OS X.
|
|
||||||
It is released to the public domain, which means you can modify it,
|
|
||||||
redistribute it or use it however you like.
|
|
||||||
.SH OPTIONS
|
|
||||||
.IP
|
|
||||||
.nf
|
|
||||||
\f[C]
|
|
||||||
-h,\ --help\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ print\ this\ help\ text\ and\ exit
|
|
||||||
--version\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ print\ program\ version\ and\ exit
|
|
||||||
-U,\ --update\ \ \ \ \ \ \ \ \ \ \ \ \ update\ this\ program\ to\ latest\ version
|
|
||||||
-i,\ --ignore-errors\ \ \ \ \ \ continue\ on\ download\ errors
|
|
||||||
-r,\ --rate-limit\ LIMIT\ \ \ download\ rate\ limit\ (e.g.\ 50k\ or\ 44.6m)
|
|
||||||
-R,\ --retries\ RETRIES\ \ \ \ number\ of\ retries\ (default\ is\ 10)
|
|
||||||
--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.
|
|
||||||
--dump-user-agent\ \ \ \ \ \ \ \ display\ the\ current\ browser\ identification
|
|
||||||
--user-agent\ UA\ \ \ \ \ \ \ \ \ \ specify\ a\ custom\ user\ agent
|
|
||||||
--list-extractors\ \ \ \ \ \ \ \ List\ all\ supported\ extractors\ and\ the\ URLs\ they
|
|
||||||
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ would\ handle
|
|
||||||
\f[]
|
|
||||||
.fi
|
|
||||||
.SS Video Selection:
|
|
||||||
.IP
|
|
||||||
.nf
|
|
||||||
\f[C]
|
|
||||||
--playlist-start\ NUMBER\ \ playlist\ video\ to\ start\ at\ (default\ is\ 1)
|
|
||||||
--playlist-end\ NUMBER\ \ \ \ playlist\ video\ to\ end\ at\ (default\ is\ last)
|
|
||||||
--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)
|
|
||||||
--max-downloads\ NUMBER\ \ \ Abort\ after\ downloading\ NUMBER\ files
|
|
||||||
\f[]
|
|
||||||
.fi
|
|
||||||
.SS Filesystem Options:
|
|
||||||
.IP
|
|
||||||
.nf
|
|
||||||
\f[C]
|
|
||||||
-t,\ --title\ \ \ \ \ \ \ \ \ \ \ \ \ \ use\ title\ in\ file\ name
|
|
||||||
--id\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ use\ video\ ID\ in\ file\ name
|
|
||||||
-l,\ --literal\ \ \ \ \ \ \ \ \ \ \ \ [deprecated]\ alias\ of\ --title
|
|
||||||
-A,\ --auto-number\ \ \ \ \ \ \ \ number\ downloaded\ files\ starting\ from\ 00000
|
|
||||||
-o,\ --output\ TEMPLATE\ \ \ \ output\ filename\ template.\ Use\ %(title)s\ to\ get\ the
|
|
||||||
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ title,\ %(uploader)s\ for\ the\ uploader\ name,
|
|
||||||
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ %(autonumber)s\ to\ get\ an\ automatically\ incremented
|
|
||||||
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ number,\ %(ext)s\ for\ the\ filename\ extension,
|
|
||||||
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ %(upload_date)s\ for\ the\ upload\ date\ (YYYYMMDD),
|
|
||||||
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ %(extractor)s\ for\ the\ provider\ (youtube,\ metacafe,
|
|
||||||
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ etc),\ %(id)s\ for\ the\ video\ id\ and\ %%\ for\ a\ literal
|
|
||||||
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ percent.\ Use\ -\ to\ output\ to\ stdout.
|
|
||||||
--restrict-filenames\ \ \ \ \ Restrict\ filenames\ to\ only\ ASCII\ characters,\ and
|
|
||||||
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ avoid\ "&"\ and\ spaces\ in\ filenames
|
|
||||||
-a,\ --batch-file\ FILE\ \ \ \ file\ containing\ URLs\ to\ download\ (\[aq]-\[aq]\ for\ stdin)
|
|
||||||
-w,\ --no-overwrites\ \ \ \ \ \ do\ not\ overwrite\ files
|
|
||||||
-c,\ --continue\ \ \ \ \ \ \ \ \ \ \ resume\ partially\ downloaded\ files
|
|
||||||
--no-continue\ \ \ \ \ \ \ \ \ \ \ \ do\ not\ resume\ partially\ downloaded\ files\ (restart
|
|
||||||
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ from\ beginning)
|
|
||||||
--cookies\ FILE\ \ \ \ \ \ \ \ \ \ \ file\ to\ read\ cookies\ from\ and\ dump\ cookie\ jar\ in
|
|
||||||
--no-part\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ do\ not\ use\ .part\ files
|
|
||||||
--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-info-json\ \ \ \ \ \ \ \ write\ video\ metadata\ to\ a\ .info.json\ file
|
|
||||||
\f[]
|
|
||||||
.fi
|
|
||||||
.SS Verbosity / Simulation Options:
|
|
||||||
.IP
|
|
||||||
.nf
|
|
||||||
\f[C]
|
|
||||||
-q,\ --quiet\ \ \ \ \ \ \ \ \ \ \ \ \ \ activates\ quiet\ mode
|
|
||||||
-s,\ --simulate\ \ \ \ \ \ \ \ \ \ \ do\ not\ download\ the\ video\ and\ do\ not\ write\ anything
|
|
||||||
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ to\ disk
|
|
||||||
--skip-download\ \ \ \ \ \ \ \ \ \ do\ not\ download\ the\ video
|
|
||||||
-g,\ --get-url\ \ \ \ \ \ \ \ \ \ \ \ simulate,\ quiet\ but\ print\ URL
|
|
||||||
-e,\ --get-title\ \ \ \ \ \ \ \ \ \ simulate,\ quiet\ but\ print\ title
|
|
||||||
--get-thumbnail\ \ \ \ \ \ \ \ \ \ simulate,\ quiet\ but\ print\ thumbnail\ URL
|
|
||||||
--get-description\ \ \ \ \ \ \ \ simulate,\ quiet\ but\ print\ video\ description
|
|
||||||
--get-filename\ \ \ \ \ \ \ \ \ \ \ simulate,\ quiet\ but\ print\ output\ filename
|
|
||||||
--get-format\ \ \ \ \ \ \ \ \ \ \ \ \ simulate,\ quiet\ but\ print\ output\ format
|
|
||||||
--no-progress\ \ \ \ \ \ \ \ \ \ \ \ do\ not\ print\ progress\ bar
|
|
||||||
--console-title\ \ \ \ \ \ \ \ \ \ display\ progress\ in\ console\ titlebar
|
|
||||||
-v,\ --verbose\ \ \ \ \ \ \ \ \ \ \ \ print\ various\ debugging\ information
|
|
||||||
\f[]
|
|
||||||
.fi
|
|
||||||
.SS Video Format Options:
|
|
||||||
.IP
|
|
||||||
.nf
|
|
||||||
\f[C]
|
|
||||||
-f,\ --format\ FORMAT\ \ \ \ \ \ video\ format\ code
|
|
||||||
--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\ (currently\ youtube\ only)
|
|
||||||
--write-srt\ \ \ \ \ \ \ \ \ \ \ \ \ \ write\ video\ closed\ captions\ to\ a\ .srt\ file
|
|
||||||
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ (currently\ youtube\ only)
|
|
||||||
--srt-lang\ LANG\ \ \ \ \ \ \ \ \ \ language\ of\ the\ closed\ captions\ to\ download
|
|
||||||
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ (optional)\ use\ IETF\ language\ tags\ like\ \[aq]en\[aq]
|
|
||||||
\f[]
|
|
||||||
.fi
|
|
||||||
.SS Authentication Options:
|
|
||||||
.IP
|
|
||||||
.nf
|
|
||||||
\f[C]
|
|
||||||
-u,\ --username\ USERNAME\ \ account\ username
|
|
||||||
-p,\ --password\ PASSWORD\ \ account\ password
|
|
||||||
-n,\ --netrc\ \ \ \ \ \ \ \ \ \ \ \ \ \ use\ .netrc\ authentication\ data
|
|
||||||
\f[]
|
|
||||||
.fi
|
|
||||||
.SS Post-processing Options:
|
|
||||||
.IP
|
|
||||||
.nf
|
|
||||||
\f[C]
|
|
||||||
-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",\ 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\ (default\ 5)
|
|
||||||
-k,\ --keep-video\ \ \ \ \ \ \ \ \ keeps\ the\ video\ file\ on\ disk\ after\ the\ post-
|
|
||||||
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ processing;\ the\ video\ is\ erased\ by\ default
|
|
||||||
\f[]
|
|
||||||
.fi
|
|
||||||
.SH CONFIGURATION
|
|
||||||
.PP
|
|
||||||
You can configure youtube-dl by placing default arguments (such as
|
|
||||||
\f[C]--extract-audio\ --no-mtime\f[] to always extract the audio and not
|
|
||||||
copy the mtime) into \f[C]/etc/youtube-dl.conf\f[] and/or
|
|
||||||
\f[C]~/.local/config/youtube-dl.conf\f[].
|
|
||||||
.SH OUTPUT TEMPLATE
|
|
||||||
.PP
|
|
||||||
The \f[C]-o\f[] option allows users to indicate a template for the
|
|
||||||
output file names.
|
|
||||||
The basic usage is not to set any template arguments when downloading a
|
|
||||||
single file, like in
|
|
||||||
\f[C]youtube-dl\ -o\ funny_video.flv\ "http://some/video"\f[].
|
|
||||||
However, it may contain special sequences that will be replaced when
|
|
||||||
downloading each video.
|
|
||||||
The special sequences have the format \f[C]%(NAME)s\f[].
|
|
||||||
To clarify, that is a percent symbol followed by a name in parenthesis,
|
|
||||||
followed by a lowercase S.
|
|
||||||
Allowed names are:
|
|
||||||
.IP \[bu] 2
|
|
||||||
\f[C]id\f[]: The sequence will be replaced by the video identifier.
|
|
||||||
.IP \[bu] 2
|
|
||||||
\f[C]url\f[]: The sequence will be replaced by the video URL.
|
|
||||||
.IP \[bu] 2
|
|
||||||
\f[C]uploader\f[]: The sequence will be replaced by the nickname of the
|
|
||||||
person who uploaded the video.
|
|
||||||
.IP \[bu] 2
|
|
||||||
\f[C]upload_date\f[]: The sequence will be replaced by the upload date
|
|
||||||
in YYYYMMDD format.
|
|
||||||
.IP \[bu] 2
|
|
||||||
\f[C]title\f[]: The sequence will be replaced by the video title.
|
|
||||||
.IP \[bu] 2
|
|
||||||
\f[C]ext\f[]: The sequence will be replaced by the appropriate extension
|
|
||||||
(like flv or mp4).
|
|
||||||
.IP \[bu] 2
|
|
||||||
\f[C]epoch\f[]: The sequence will be replaced by the Unix epoch when
|
|
||||||
creating the file.
|
|
||||||
.IP \[bu] 2
|
|
||||||
\f[C]autonumber\f[]: The sequence will be replaced by a five-digit
|
|
||||||
number that will be increased with each download, starting at zero.
|
|
||||||
.PP
|
|
||||||
The current default template is \f[C]%(id)s.%(ext)s\f[], but that will
|
|
||||||
be switchted to \f[C]%(title)s-%(id)s.%(ext)s\f[] (which can be
|
|
||||||
requested with \f[C]-t\f[] at the moment).
|
|
||||||
.PP
|
|
||||||
In some cases, you don\[aq]t want special characters such as 中, spaces,
|
|
||||||
or &, such as when transferring the downloaded filename to a Windows
|
|
||||||
system or the filename through an 8bit-unsafe channel.
|
|
||||||
In these cases, add the \f[C]--restrict-filenames\f[] flag to get a
|
|
||||||
shorter title:
|
|
||||||
.IP
|
|
||||||
.nf
|
|
||||||
\f[C]
|
|
||||||
$\ youtube-dl\ --get-filename\ -o\ "%(title)s.%(ext)s"\ BaW_jenozKc
|
|
||||||
youtube-dl\ test\ video\ \[aq]\[aq]_ä↭𝕐.mp4\ \ \ \ #\ All\ kinds\ of\ weird\ characters
|
|
||||||
$\ youtube-dl\ --get-filename\ -o\ "%(title)s.%(ext)s"\ BaW_jenozKc\ --restrict-filenames
|
|
||||||
youtube-dl_test_video_.mp4\ \ \ \ \ \ \ \ \ \ #\ A\ simple\ file\ name
|
|
||||||
\f[]
|
|
||||||
.fi
|
|
||||||
.SH FAQ
|
|
||||||
.SS Can you please put the -b option back?
|
|
||||||
.PP
|
|
||||||
Most people asking this question are not aware that youtube-dl now
|
|
||||||
defaults to downloading the highest available quality as reported by
|
|
||||||
YouTube, which will be 1080p or 720p in some cases, so you no longer
|
|
||||||
need the -b option.
|
|
||||||
For some specific videos, maybe YouTube does not report them to be
|
|
||||||
available in a specific high quality format you\[aq]\[aq]re interested
|
|
||||||
in.
|
|
||||||
In that case, simply request it with the -f option and youtube-dl will
|
|
||||||
try to download it.
|
|
||||||
.SS I get HTTP error 402 when trying to download a video. What\[aq]s
|
|
||||||
this?
|
|
||||||
.PP
|
|
||||||
Apparently YouTube requires you to pass a CAPTCHA test if you download
|
|
||||||
too much.
|
|
||||||
We\[aq]\[aq]re considering to provide a way to let you solve the
|
|
||||||
CAPTCHA (https://github.com/rg3/youtube-dl/issues/154), but at the
|
|
||||||
moment, your best course of action is pointing a webbrowser to the
|
|
||||||
youtube URL, solving the CAPTCHA, and restart youtube-dl.
|
|
||||||
.SS I have downloaded a video but how can I play it?
|
|
||||||
.PP
|
|
||||||
Once the video is fully downloaded, use any video player, such as
|
|
||||||
vlc (http://www.videolan.org) or mplayer (http://www.mplayerhq.hu/).
|
|
||||||
.SS The links provided by youtube-dl -g are not working anymore
|
|
||||||
.PP
|
|
||||||
The URLs youtube-dl outputs require the downloader to have the correct
|
|
||||||
cookies.
|
|
||||||
Use the \f[C]--cookies\f[] option to write the required cookies into a
|
|
||||||
file, and advise your downloader to read cookies from that file.
|
|
||||||
Some sites also require a common user agent to be used, use
|
|
||||||
\f[C]--dump-user-agent\f[] to see the one in use by youtube-dl.
|
|
||||||
.SS ERROR: no fmt_url_map or conn information found in video info
|
|
||||||
.PP
|
|
||||||
youtube has switched to a new video info format in July 2011 which is
|
|
||||||
not supported by old versions of youtube-dl.
|
|
||||||
You can update youtube-dl with \f[C]sudo\ youtube-dl\ --update\f[].
|
|
||||||
.SS ERROR: unable to download video
|
|
||||||
.PP
|
|
||||||
youtube requires an additional signature since September 2012 which is
|
|
||||||
not supported by old versions of youtube-dl.
|
|
||||||
You can update youtube-dl with \f[C]sudo\ youtube-dl\ --update\f[].
|
|
||||||
.SS SyntaxError: Non-ASCII character
|
|
||||||
.PP
|
|
||||||
The error
|
|
||||||
.IP
|
|
||||||
.nf
|
|
||||||
\f[C]
|
|
||||||
File\ "youtube-dl",\ line\ 2
|
|
||||||
SyntaxError:\ Non-ASCII\ character\ \[aq]\\x93\[aq]\ ...
|
|
||||||
\f[]
|
|
||||||
.fi
|
|
||||||
.PP
|
|
||||||
means you\[aq]re using an outdated version of Python.
|
|
||||||
Please update to Python 2.6 or 2.7.
|
|
||||||
.PP
|
|
||||||
To run youtube-dl under Python 2.5, you\[aq]ll have to manually check it
|
|
||||||
out like this:
|
|
||||||
.IP
|
|
||||||
.nf
|
|
||||||
\f[C]
|
|
||||||
git\ clone\ git://github.com/rg3/youtube-dl.git
|
|
||||||
cd\ youtube-dl
|
|
||||||
python\ -m\ youtube_dl\ --help
|
|
||||||
\f[]
|
|
||||||
.fi
|
|
||||||
.PP
|
|
||||||
Please note that Python 2.5 is not supported anymore.
|
|
||||||
.SS What is this binary file? Where has the code gone?
|
|
||||||
.PP
|
|
||||||
Since June 2012 (#342) youtube-dl is packed as an executable zipfile,
|
|
||||||
simply unzip it (might need renaming to \f[C]youtube-dl.zip\f[] first on
|
|
||||||
some systems) or clone the git repository, as laid out above.
|
|
||||||
If you modify the code, you can run it by executing the
|
|
||||||
\f[C]__main__.py\f[] file.
|
|
||||||
To recompile the executable, run \f[C]make\ youtube-dl\f[].
|
|
||||||
.SS The exe throws a \f[I]Runtime error from Visual C++\f[]
|
|
||||||
.PP
|
|
||||||
To run the exe you need to install first the Microsoft Visual C++ 2008
|
|
||||||
Redistributable
|
|
||||||
Package (http://www.microsoft.com/en-us/download/details.aspx?id=29).
|
|
||||||
.SH COPYRIGHT
|
|
||||||
.PP
|
|
||||||
youtube-dl is released into the public domain by the copyright holders.
|
|
||||||
.PP
|
|
||||||
This README file was originally written by Daniel Bolton
|
|
||||||
(<https://github.com/dbbolton>) and is likewise released into the public
|
|
||||||
domain.
|
|
||||||
.SH BUGS
|
|
||||||
.PP
|
|
||||||
Bugs and suggestions should be reported at:
|
|
||||||
<https://github.com/rg3/youtube-dl/issues>
|
|
||||||
.PP
|
|
||||||
Please include:
|
|
||||||
.IP \[bu] 2
|
|
||||||
Your exact command line, like
|
|
||||||
\f[C]youtube-dl\ -t\ "http://www.youtube.com/watch?v=uHlDtZ6Oc3s&feature=channel_video_title"\f[].
|
|
||||||
A common mistake is not to escape the \f[C]&\f[].
|
|
||||||
Putting URLs in quotes should solve this problem.
|
|
||||||
.IP \[bu] 2
|
|
||||||
The output of \f[C]youtube-dl\ --version\f[]
|
|
||||||
.IP \[bu] 2
|
|
||||||
The output of \f[C]python\ --version\f[]
|
|
||||||
.IP \[bu] 2
|
|
||||||
The name and version of your Operating System ("Ubuntu 11.04 x64" or
|
|
||||||
"Windows 7 x64" is usually enough).
|
|
@@ -1,14 +0,0 @@
|
|||||||
__youtube-dl()
|
|
||||||
{
|
|
||||||
local cur prev opts
|
|
||||||
COMPREPLY=()
|
|
||||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
|
||||||
opts="--all-formats --audio-format --audio-quality --auto-number --batch-file --buffer-size --console-title --continue --cookies --dump-user-agent --extract-audio --format --get-description --get-filename --get-format --get-thumbnail --get-title --get-url --help --id --ignore-errors --keep-video --list-extractors --list-formats --literal --match-title --max-downloads --max-quality --netrc --no-continue --no-mtime --no-overwrites --no-part --no-progress --no-resize-buffer --output --password --playlist-end --playlist-start --prefer-free-formats --quiet --rate-limit --reject-title --restrict-filenames --retries --simulate --skip-download --srt-lang --title --update --user-agent --username --verbose --version --write-description --write-info-json --write-srt"
|
|
||||||
|
|
||||||
if [[ ${cur} == * ]] ; then
|
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
complete -F __youtube-dl youtube-dl
|
|
BIN
youtube-dl.exe
BIN
youtube-dl.exe
Binary file not shown.
@@ -1,20 +1,24 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import httplib
|
from __future__ import absolute_import
|
||||||
|
|
||||||
import math
|
import math
|
||||||
|
import io
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import urllib2
|
import traceback
|
||||||
|
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
import ctypes
|
import ctypes
|
||||||
|
|
||||||
from utils import *
|
from .utils import *
|
||||||
|
from .InfoExtractors import get_info_extractor
|
||||||
|
|
||||||
|
|
||||||
class FileDownloader(object):
|
class FileDownloader(object):
|
||||||
@@ -50,6 +54,7 @@ class FileDownloader(object):
|
|||||||
quiet: Do not print messages to stdout.
|
quiet: Do not print messages to stdout.
|
||||||
forceurl: Force printing final URL.
|
forceurl: Force printing final URL.
|
||||||
forcetitle: Force printing title.
|
forcetitle: Force printing title.
|
||||||
|
forceid: Force printing ID.
|
||||||
forcethumbnail: Force printing thumbnail URL.
|
forcethumbnail: Force printing thumbnail URL.
|
||||||
forcedescription: Force printing description.
|
forcedescription: Force printing description.
|
||||||
forcefilename: Force printing final filename.
|
forcefilename: Force printing final filename.
|
||||||
@@ -76,8 +81,18 @@ class FileDownloader(object):
|
|||||||
updatetime: Use the Last-modified header to set output file timestamps.
|
updatetime: Use the Last-modified header to set output file timestamps.
|
||||||
writedescription: Write the video description to a .description file
|
writedescription: Write the video description to a .description file
|
||||||
writeinfojson: Write the video description to a .info.json file
|
writeinfojson: Write the video description to a .info.json file
|
||||||
writesubtitles: Write the video subtitles to a .srt file
|
writethumbnail: Write the thumbnail image to a file
|
||||||
|
writesubtitles: Write the video subtitles to a file
|
||||||
|
allsubtitles: Downloads all the subtitles of the video
|
||||||
|
listsubtitles: Lists all available subtitles for the video
|
||||||
|
subtitlesformat: Subtitle format [sbv/srt] (default=srt)
|
||||||
subtitleslang: Language of the subtitles to download
|
subtitleslang: Language of the subtitles to download
|
||||||
|
test: Download only first bytes to test the downloader.
|
||||||
|
keepvideo: Keep the video file after post-processing
|
||||||
|
min_filesize: Skip files smaller than this size
|
||||||
|
max_filesize: Skip files larger than this size
|
||||||
|
daterange: A DateRange object, download only if the upload_date is in the range.
|
||||||
|
skip_download: Skip the actual download of the video file
|
||||||
"""
|
"""
|
||||||
|
|
||||||
params = None
|
params = None
|
||||||
@@ -91,13 +106,14 @@ class FileDownloader(object):
|
|||||||
"""Create a FileDownloader object with the given options."""
|
"""Create a FileDownloader object with the given options."""
|
||||||
self._ies = []
|
self._ies = []
|
||||||
self._pps = []
|
self._pps = []
|
||||||
|
self._progress_hooks = []
|
||||||
self._download_retcode = 0
|
self._download_retcode = 0
|
||||||
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.params = params
|
self.params = params
|
||||||
|
|
||||||
if '%(stitle)s' in self.params['outtmpl']:
|
if '%(stitle)s' in self.params['outtmpl']:
|
||||||
self.to_stderr(u'WARNING: %(stitle)s is deprecated. Use the %(title)s and the --restrict-filenames flag(which also secures %(uploader)s et al) instead.')
|
self.report_warning(u'%(stitle)s is deprecated. Use the %(title)s and the --restrict-filenames flag(which also secures %(uploader)s et al) instead.')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def format_bytes(bytes):
|
def format_bytes(bytes):
|
||||||
@@ -108,8 +124,8 @@ class FileDownloader(object):
|
|||||||
if bytes == 0.0:
|
if bytes == 0.0:
|
||||||
exponent = 0
|
exponent = 0
|
||||||
else:
|
else:
|
||||||
exponent = long(math.log(bytes, 1024.0))
|
exponent = int(math.log(bytes, 1024.0))
|
||||||
suffix = 'bkMGTPEZY'[exponent]
|
suffix = ['B','KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB'][exponent]
|
||||||
converted = float(bytes) / float(1024 ** exponent)
|
converted = float(bytes) / float(1024 ** exponent)
|
||||||
return '%.2f%s' % (converted, suffix)
|
return '%.2f%s' % (converted, suffix)
|
||||||
|
|
||||||
@@ -127,7 +143,7 @@ class FileDownloader(object):
|
|||||||
if current == 0 or dif < 0.001: # One millisecond
|
if current == 0 or dif < 0.001: # One millisecond
|
||||||
return '--:--'
|
return '--:--'
|
||||||
rate = float(current) / dif
|
rate = float(current) / dif
|
||||||
eta = long((float(total) - float(current)) / rate)
|
eta = int((float(total) - float(current)) / rate)
|
||||||
(eta_mins, eta_secs) = divmod(eta, 60)
|
(eta_mins, eta_secs) = divmod(eta, 60)
|
||||||
if eta_mins > 99:
|
if eta_mins > 99:
|
||||||
return '--:--'
|
return '--:--'
|
||||||
@@ -179,7 +195,7 @@ class FileDownloader(object):
|
|||||||
if not self.params.get('quiet', False):
|
if not self.params.get('quiet', False):
|
||||||
terminator = [u'\n', u''][skip_eol]
|
terminator = [u'\n', u''][skip_eol]
|
||||||
output = message + terminator
|
output = message + terminator
|
||||||
if 'b' not in self._screen_file.mode or sys.version_info[0] < 3: # Python 2 lies about the mode of sys.stdout/sys.stderr
|
if 'b' in getattr(self._screen_file, 'mode', '') or sys.version_info[0] < 3: # Python 2 lies about the mode of sys.stdout/sys.stderr
|
||||||
output = output.encode(preferredencoding(), 'ignore')
|
output = output.encode(preferredencoding(), 'ignore')
|
||||||
self._screen_file.write(output)
|
self._screen_file.write(output)
|
||||||
self._screen_file.flush()
|
self._screen_file.flush()
|
||||||
@@ -187,7 +203,10 @@ class FileDownloader(object):
|
|||||||
def to_stderr(self, message):
|
def to_stderr(self, message):
|
||||||
"""Print message to stderr."""
|
"""Print message to stderr."""
|
||||||
assert type(message) == type(u'')
|
assert type(message) == type(u'')
|
||||||
sys.stderr.write((message + u'\n').encode(preferredencoding()))
|
output = message + u'\n'
|
||||||
|
if 'b' in getattr(self._screen_file, 'mode', '') or sys.version_info[0] < 3: # Python 2 lies about the mode of sys.stdout/sys.stderr
|
||||||
|
output = output.encode(preferredencoding())
|
||||||
|
sys.stderr.write(output)
|
||||||
|
|
||||||
def to_cons_title(self, message):
|
def to_cons_title(self, message):
|
||||||
"""Set console/terminal window title to message."""
|
"""Set console/terminal window title to message."""
|
||||||
@@ -198,25 +217,66 @@ class FileDownloader(object):
|
|||||||
# already of type unicode()
|
# already of type unicode()
|
||||||
ctypes.windll.kernel32.SetConsoleTitleW(ctypes.c_wchar_p(message))
|
ctypes.windll.kernel32.SetConsoleTitleW(ctypes.c_wchar_p(message))
|
||||||
elif 'TERM' in os.environ:
|
elif 'TERM' in os.environ:
|
||||||
sys.stderr.write('\033]0;%s\007' % message.encode(preferredencoding()))
|
self.to_screen('\033]0;%s\007' % message, skip_eol=True)
|
||||||
|
|
||||||
def fixed_template(self):
|
def fixed_template(self):
|
||||||
"""Checks if the output template is fixed."""
|
"""Checks if the output template is fixed."""
|
||||||
return (re.search(ur'(?u)%\(.+?\)s', self.params['outtmpl']) is None)
|
return (re.search(u'(?u)%\\(.+?\\)s', self.params['outtmpl']) is None)
|
||||||
|
|
||||||
def trouble(self, message=None):
|
def trouble(self, message=None, tb=None):
|
||||||
"""Determine action to take when a download problem appears.
|
"""Determine action to take when a download problem appears.
|
||||||
|
|
||||||
Depending on if the downloader has been configured to ignore
|
Depending on if the downloader has been configured to ignore
|
||||||
download errors or not, this method may throw an exception or
|
download errors or not, this method may throw an exception or
|
||||||
not when errors are found, after printing the message.
|
not when errors are found, after printing the message.
|
||||||
|
|
||||||
|
tb, if given, is additional traceback information.
|
||||||
"""
|
"""
|
||||||
if message is not None:
|
if message is not None:
|
||||||
self.to_stderr(message)
|
self.to_stderr(message)
|
||||||
|
if self.params.get('verbose'):
|
||||||
|
if tb is None:
|
||||||
|
if sys.exc_info()[0]: # if .trouble has been called from an except block
|
||||||
|
tb = u''
|
||||||
|
if hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
|
||||||
|
tb += u''.join(traceback.format_exception(*sys.exc_info()[1].exc_info))
|
||||||
|
tb += compat_str(traceback.format_exc())
|
||||||
|
else:
|
||||||
|
tb_data = traceback.format_list(traceback.extract_stack())
|
||||||
|
tb = u''.join(tb_data)
|
||||||
|
self.to_stderr(tb)
|
||||||
if not self.params.get('ignoreerrors', False):
|
if not self.params.get('ignoreerrors', False):
|
||||||
raise DownloadError(message)
|
if sys.exc_info()[0] and hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
|
||||||
|
exc_info = sys.exc_info()[1].exc_info
|
||||||
|
else:
|
||||||
|
exc_info = sys.exc_info()
|
||||||
|
raise DownloadError(message, exc_info)
|
||||||
self._download_retcode = 1
|
self._download_retcode = 1
|
||||||
|
|
||||||
|
def report_warning(self, message):
|
||||||
|
'''
|
||||||
|
Print the message to stderr, it will be prefixed with 'WARNING:'
|
||||||
|
If stderr is a tty file the 'WARNING:' will be colored
|
||||||
|
'''
|
||||||
|
if sys.stderr.isatty() and os.name != 'nt':
|
||||||
|
_msg_header=u'\033[0;33mWARNING:\033[0m'
|
||||||
|
else:
|
||||||
|
_msg_header=u'WARNING:'
|
||||||
|
warning_message=u'%s %s' % (_msg_header,message)
|
||||||
|
self.to_stderr(warning_message)
|
||||||
|
|
||||||
|
def report_error(self, message, tb=None):
|
||||||
|
'''
|
||||||
|
Do the same as trouble, but prefixes the message with 'ERROR:', colored
|
||||||
|
in red if stderr is a tty file.
|
||||||
|
'''
|
||||||
|
if sys.stderr.isatty() and os.name != 'nt':
|
||||||
|
_msg_header = u'\033[0;31mERROR:\033[0m'
|
||||||
|
else:
|
||||||
|
_msg_header = u'ERROR:'
|
||||||
|
error_message = u'%s %s' % (_msg_header, message)
|
||||||
|
self.trouble(error_message, tb)
|
||||||
|
|
||||||
def slow_down(self, start_time, byte_counter):
|
def slow_down(self, start_time, byte_counter):
|
||||||
"""Sleep if the download speed is over the rate limit."""
|
"""Sleep if the download speed is over the rate limit."""
|
||||||
rate_limit = self.params.get('ratelimit', None)
|
rate_limit = self.params.get('ratelimit', None)
|
||||||
@@ -247,8 +307,8 @@ class FileDownloader(object):
|
|||||||
if old_filename == new_filename:
|
if old_filename == new_filename:
|
||||||
return
|
return
|
||||||
os.rename(encodeFilename(old_filename), encodeFilename(new_filename))
|
os.rename(encodeFilename(old_filename), encodeFilename(new_filename))
|
||||||
except (IOError, OSError), err:
|
except (IOError, OSError) as err:
|
||||||
self.trouble(u'ERROR: unable to rename file')
|
self.report_error(u'unable to rename file')
|
||||||
|
|
||||||
def try_utime(self, filename, last_modified_hdr):
|
def try_utime(self, filename, last_modified_hdr):
|
||||||
"""Try to set the last-modified time of the given file."""
|
"""Try to set the last-modified time of the given file."""
|
||||||
@@ -262,6 +322,9 @@ class FileDownloader(object):
|
|||||||
filetime = timeconvert(timestr)
|
filetime = timeconvert(timestr)
|
||||||
if filetime is None:
|
if filetime is None:
|
||||||
return filetime
|
return filetime
|
||||||
|
# Ignore obviously invalid dates
|
||||||
|
if filetime == 0:
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
os.utime(filename, (time.time(), filetime))
|
os.utime(filename, (time.time(), filetime))
|
||||||
except:
|
except:
|
||||||
@@ -272,9 +335,9 @@ class FileDownloader(object):
|
|||||||
""" Report that the description file is being written """
|
""" Report that the description file is being written """
|
||||||
self.to_screen(u'[info] Writing video description to: ' + descfn)
|
self.to_screen(u'[info] Writing video description to: ' + descfn)
|
||||||
|
|
||||||
def report_writesubtitles(self, srtfn):
|
def report_writesubtitles(self, sub_filename):
|
||||||
""" Report that the subtitles file is being written """
|
""" Report that the subtitles file is being written """
|
||||||
self.to_screen(u'[info] Writing video subtitles to: ' + srtfn)
|
self.to_screen(u'[info] Writing video subtitles to: ' + sub_filename)
|
||||||
|
|
||||||
def report_writeinfojson(self, infofn):
|
def report_writeinfojson(self, infofn):
|
||||||
""" Report that the metadata file has been written """
|
""" Report that the metadata file has been written """
|
||||||
@@ -288,8 +351,13 @@ class FileDownloader(object):
|
|||||||
"""Report download progress."""
|
"""Report download progress."""
|
||||||
if self.params.get('noprogress', False):
|
if self.params.get('noprogress', False):
|
||||||
return
|
return
|
||||||
self.to_screen(u'\r[download] %s of %s at %s ETA %s' %
|
clear_line = (u'\x1b[K' if sys.stderr.isatty() and os.name != 'nt' else u'')
|
||||||
(percent_str, data_len_str, speed_str, eta_str), skip_eol=True)
|
if self.params.get('progress_with_newline', False):
|
||||||
|
self.to_screen(u'[download] %s of %s at %s ETA %s' %
|
||||||
|
(percent_str, data_len_str, speed_str, eta_str))
|
||||||
|
else:
|
||||||
|
self.to_screen(u'\r%s[download] %s of %s at %s ETA %s' %
|
||||||
|
(clear_line, percent_str, data_len_str, speed_str, eta_str), skip_eol=True)
|
||||||
self.to_cons_title(u'youtube-dl - %s of %s at %s ETA %s' %
|
self.to_cons_title(u'youtube-dl - %s of %s at %s ETA %s' %
|
||||||
(percent_str.strip(), data_len_str.strip(), speed_str.strip(), eta_str.strip()))
|
(percent_str.strip(), data_len_str.strip(), speed_str.strip(), eta_str.strip()))
|
||||||
|
|
||||||
@@ -305,7 +373,7 @@ class FileDownloader(object):
|
|||||||
"""Report file has already been fully downloaded."""
|
"""Report file has already been fully downloaded."""
|
||||||
try:
|
try:
|
||||||
self.to_screen(u'[download] %s has already been downloaded' % file_name)
|
self.to_screen(u'[download] %s has already been downloaded' % file_name)
|
||||||
except (UnicodeEncodeError), err:
|
except (UnicodeEncodeError) as err:
|
||||||
self.to_screen(u'[download] The file has already been downloaded')
|
self.to_screen(u'[download] The file has already been downloaded')
|
||||||
|
|
||||||
def report_unable_to_resume(self):
|
def report_unable_to_resume(self):
|
||||||
@@ -327,14 +395,29 @@ class FileDownloader(object):
|
|||||||
"""Generate the output filename."""
|
"""Generate the output filename."""
|
||||||
try:
|
try:
|
||||||
template_dict = dict(info_dict)
|
template_dict = dict(info_dict)
|
||||||
template_dict['epoch'] = int(time.time())
|
|
||||||
template_dict['autonumber'] = u'%05d' % self._num_downloads
|
|
||||||
|
|
||||||
template_dict = dict((k, sanitize_filename(compat_str(v), self.params.get('restrictfilenames'))) for k,v in template_dict.items())
|
template_dict['epoch'] = int(time.time())
|
||||||
|
autonumber_size = self.params.get('autonumber_size')
|
||||||
|
if autonumber_size is None:
|
||||||
|
autonumber_size = 5
|
||||||
|
autonumber_templ = u'%0' + str(autonumber_size) + u'd'
|
||||||
|
template_dict['autonumber'] = autonumber_templ % self._num_downloads
|
||||||
|
if template_dict['playlist_index'] is not None:
|
||||||
|
template_dict['playlist_index'] = u'%05d' % template_dict['playlist_index']
|
||||||
|
|
||||||
|
sanitize = lambda k,v: sanitize_filename(
|
||||||
|
u'NA' if v is None else compat_str(v),
|
||||||
|
restricted=self.params.get('restrictfilenames'),
|
||||||
|
is_id=(k==u'id'))
|
||||||
|
template_dict = dict((k, sanitize(k, v)) for k,v in template_dict.items())
|
||||||
|
|
||||||
filename = self.params['outtmpl'] % template_dict
|
filename = self.params['outtmpl'] % template_dict
|
||||||
return filename
|
return filename
|
||||||
except (ValueError, KeyError), err:
|
except KeyError as err:
|
||||||
self.trouble(u'ERROR: invalid system charset or erroneous output template')
|
self.report_error(u'Erroneous output template')
|
||||||
|
return None
|
||||||
|
except ValueError as err:
|
||||||
|
self.report_error(u'Insufficient system charset ' + repr(preferredencoding()))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _match_entry(self, info_dict):
|
def _match_entry(self, info_dict):
|
||||||
@@ -343,22 +426,162 @@ class FileDownloader(object):
|
|||||||
title = info_dict['title']
|
title = info_dict['title']
|
||||||
matchtitle = self.params.get('matchtitle', False)
|
matchtitle = self.params.get('matchtitle', False)
|
||||||
if matchtitle:
|
if matchtitle:
|
||||||
matchtitle = matchtitle.decode('utf8')
|
|
||||||
if not re.search(matchtitle, title, re.IGNORECASE):
|
if not re.search(matchtitle, title, re.IGNORECASE):
|
||||||
return u'[download] "' + title + '" title did not match pattern "' + matchtitle + '"'
|
return u'[download] "' + title + '" title did not match pattern "' + matchtitle + '"'
|
||||||
rejecttitle = self.params.get('rejecttitle', False)
|
rejecttitle = self.params.get('rejecttitle', False)
|
||||||
if rejecttitle:
|
if rejecttitle:
|
||||||
rejecttitle = rejecttitle.decode('utf8')
|
|
||||||
if re.search(rejecttitle, title, re.IGNORECASE):
|
if re.search(rejecttitle, title, re.IGNORECASE):
|
||||||
return u'"' + title + '" title matched reject pattern "' + rejecttitle + '"'
|
return u'"' + title + '" title matched reject pattern "' + rejecttitle + '"'
|
||||||
|
date = info_dict.get('upload_date', None)
|
||||||
|
if date is not None:
|
||||||
|
dateRange = self.params.get('daterange', DateRange())
|
||||||
|
if date not in dateRange:
|
||||||
|
return u'[download] %s upload date is not in range %s' % (date_from_str(date).isoformat(), dateRange)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def extract_info(self, url, download=True, ie_key=None, extra_info={}):
|
||||||
|
'''
|
||||||
|
Returns a list with a dictionary for each video we find.
|
||||||
|
If 'download', also downloads the videos.
|
||||||
|
extra_info is a dict containing the extra values to add to each result
|
||||||
|
'''
|
||||||
|
|
||||||
|
if ie_key:
|
||||||
|
ie = get_info_extractor(ie_key)()
|
||||||
|
ie.set_downloader(self)
|
||||||
|
ies = [ie]
|
||||||
|
else:
|
||||||
|
ies = self._ies
|
||||||
|
|
||||||
|
for ie in ies:
|
||||||
|
if not ie.suitable(url):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not ie.working():
|
||||||
|
self.report_warning(u'The program functionality for this site has been marked as broken, '
|
||||||
|
u'and will probably not work.')
|
||||||
|
|
||||||
|
try:
|
||||||
|
ie_result = ie.extract(url)
|
||||||
|
if ie_result is None: # Finished already (backwards compatibility; listformats and friends should be moved here)
|
||||||
|
break
|
||||||
|
if isinstance(ie_result, list):
|
||||||
|
# Backwards compatibility: old IE result format
|
||||||
|
for result in ie_result:
|
||||||
|
result.update(extra_info)
|
||||||
|
ie_result = {
|
||||||
|
'_type': 'compat_list',
|
||||||
|
'entries': ie_result,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
ie_result.update(extra_info)
|
||||||
|
if 'extractor' not in ie_result:
|
||||||
|
ie_result['extractor'] = ie.IE_NAME
|
||||||
|
return self.process_ie_result(ie_result, download=download)
|
||||||
|
except ExtractorError as de: # An error we somewhat expected
|
||||||
|
self.report_error(compat_str(de), de.format_traceback())
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
if self.params.get('ignoreerrors', False):
|
||||||
|
self.report_error(compat_str(e), tb=compat_str(traceback.format_exc()))
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
self.report_error(u'no suitable InfoExtractor: %s' % url)
|
||||||
|
|
||||||
|
def process_ie_result(self, ie_result, download=True, extra_info={}):
|
||||||
|
"""
|
||||||
|
Take the result of the ie(may be modified) and resolve all unresolved
|
||||||
|
references (URLs, playlist items).
|
||||||
|
|
||||||
|
It will also download the videos if 'download'.
|
||||||
|
Returns the resolved ie_result.
|
||||||
|
"""
|
||||||
|
|
||||||
|
result_type = ie_result.get('_type', 'video') # If not given we suppose it's a video, support the default old system
|
||||||
|
if result_type == 'video':
|
||||||
|
if 'playlist' not in ie_result:
|
||||||
|
# It isn't part of a playlist
|
||||||
|
ie_result['playlist'] = None
|
||||||
|
ie_result['playlist_index'] = None
|
||||||
|
if download:
|
||||||
|
self.process_info(ie_result)
|
||||||
|
return ie_result
|
||||||
|
elif result_type == 'url':
|
||||||
|
# We have to add extra_info to the results because it may be
|
||||||
|
# contained in a playlist
|
||||||
|
return self.extract_info(ie_result['url'],
|
||||||
|
download,
|
||||||
|
ie_key=ie_result.get('ie_key'),
|
||||||
|
extra_info=extra_info)
|
||||||
|
elif result_type == 'playlist':
|
||||||
|
# We process each entry in the playlist
|
||||||
|
playlist = ie_result.get('title', None) or ie_result.get('id', None)
|
||||||
|
self.to_screen(u'[download] Downloading playlist: %s' % playlist)
|
||||||
|
|
||||||
|
playlist_results = []
|
||||||
|
|
||||||
|
n_all_entries = len(ie_result['entries'])
|
||||||
|
playliststart = self.params.get('playliststart', 1) - 1
|
||||||
|
playlistend = self.params.get('playlistend', -1)
|
||||||
|
|
||||||
|
if playlistend == -1:
|
||||||
|
entries = ie_result['entries'][playliststart:]
|
||||||
|
else:
|
||||||
|
entries = ie_result['entries'][playliststart:playlistend]
|
||||||
|
|
||||||
|
n_entries = len(entries)
|
||||||
|
|
||||||
|
self.to_screen(u"[%s] playlist '%s': Collected %d video ids (downloading %d of them)" %
|
||||||
|
(ie_result['extractor'], playlist, n_all_entries, n_entries))
|
||||||
|
|
||||||
|
for i,entry in enumerate(entries,1):
|
||||||
|
self.to_screen(u'[download] Downloading video #%s of %s' %(i, n_entries))
|
||||||
|
extra = {
|
||||||
|
'playlist': playlist,
|
||||||
|
'playlist_index': i + playliststart,
|
||||||
|
}
|
||||||
|
if not 'extractor' in entry:
|
||||||
|
# We set the extractor, if it's an url it will be set then to
|
||||||
|
# the new extractor, but if it's already a video we must make
|
||||||
|
# sure it's present: see issue #877
|
||||||
|
entry['extractor'] = ie_result['extractor']
|
||||||
|
entry_result = self.process_ie_result(entry,
|
||||||
|
download=download,
|
||||||
|
extra_info=extra)
|
||||||
|
playlist_results.append(entry_result)
|
||||||
|
ie_result['entries'] = playlist_results
|
||||||
|
return ie_result
|
||||||
|
elif result_type == 'compat_list':
|
||||||
|
def _fixup(r):
|
||||||
|
r.setdefault('extractor', ie_result['extractor'])
|
||||||
|
return r
|
||||||
|
ie_result['entries'] = [
|
||||||
|
self.process_ie_result(_fixup(r), download=download)
|
||||||
|
for r in ie_result['entries']
|
||||||
|
]
|
||||||
|
return ie_result
|
||||||
|
else:
|
||||||
|
raise Exception('Invalid result type: %s' % result_type)
|
||||||
|
|
||||||
def process_info(self, info_dict):
|
def process_info(self, info_dict):
|
||||||
"""Process a single dictionary returned by an InfoExtractor."""
|
"""Process a single resolved IE result."""
|
||||||
|
|
||||||
|
assert info_dict.get('_type', 'video') == 'video'
|
||||||
|
#We increment the download the download count here to match the previous behaviour.
|
||||||
|
self.increment_downloads()
|
||||||
|
|
||||||
|
info_dict['fulltitle'] = info_dict['title']
|
||||||
|
if len(info_dict['title']) > 200:
|
||||||
|
info_dict['title'] = info_dict['title'][:197] + u'...'
|
||||||
|
|
||||||
# Keep for backwards compatibility
|
# Keep for backwards compatibility
|
||||||
info_dict['stitle'] = info_dict['title']
|
info_dict['stitle'] = info_dict['title']
|
||||||
|
|
||||||
|
if not 'format' in info_dict:
|
||||||
|
info_dict['format'] = info_dict['ext']
|
||||||
|
|
||||||
reason = self._match_entry(info_dict)
|
reason = self._match_entry(info_dict)
|
||||||
if reason is not None:
|
if reason is not None:
|
||||||
self.to_screen(u'[download] ' + reason)
|
self.to_screen(u'[download] ' + reason)
|
||||||
@@ -373,17 +596,19 @@ class FileDownloader(object):
|
|||||||
|
|
||||||
# Forced printings
|
# Forced printings
|
||||||
if self.params.get('forcetitle', False):
|
if self.params.get('forcetitle', False):
|
||||||
print(info_dict['title'].encode(preferredencoding(), 'xmlcharrefreplace'))
|
compat_print(info_dict['title'])
|
||||||
|
if self.params.get('forceid', False):
|
||||||
|
compat_print(info_dict['id'])
|
||||||
if self.params.get('forceurl', False):
|
if self.params.get('forceurl', False):
|
||||||
print(info_dict['url'].encode(preferredencoding(), 'xmlcharrefreplace'))
|
compat_print(info_dict['url'])
|
||||||
if self.params.get('forcethumbnail', False) and 'thumbnail' in info_dict:
|
if self.params.get('forcethumbnail', False) and 'thumbnail' in info_dict:
|
||||||
print(info_dict['thumbnail'].encode(preferredencoding(), 'xmlcharrefreplace'))
|
compat_print(info_dict['thumbnail'])
|
||||||
if self.params.get('forcedescription', False) and 'description' in info_dict:
|
if self.params.get('forcedescription', False) and 'description' in info_dict:
|
||||||
print(info_dict['description'].encode(preferredencoding(), 'xmlcharrefreplace'))
|
compat_print(info_dict['description'])
|
||||||
if self.params.get('forcefilename', False) and filename is not None:
|
if self.params.get('forcefilename', False) and filename is not None:
|
||||||
print(filename.encode(preferredencoding(), 'xmlcharrefreplace'))
|
compat_print(filename)
|
||||||
if self.params.get('forceformat', False):
|
if self.params.get('forceformat', False):
|
||||||
print(info_dict['format'].encode(preferredencoding(), 'xmlcharrefreplace'))
|
compat_print(info_dict['format'])
|
||||||
|
|
||||||
# Do nothing else if in simulate mode
|
# Do nothing else if in simulate mode
|
||||||
if self.params.get('simulate', False):
|
if self.params.get('simulate', False):
|
||||||
@@ -394,79 +619,101 @@ class FileDownloader(object):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
dn = os.path.dirname(encodeFilename(filename))
|
dn = os.path.dirname(encodeFilename(filename))
|
||||||
if dn != '' and not os.path.exists(dn): # dn is already encoded
|
if dn != '' and not os.path.exists(dn):
|
||||||
os.makedirs(dn)
|
os.makedirs(dn)
|
||||||
except (OSError, IOError), err:
|
except (OSError, IOError) as err:
|
||||||
self.trouble(u'ERROR: unable to create directory ' + unicode(err))
|
self.report_error(u'unable to create directory ' + compat_str(err))
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.params.get('writedescription', False):
|
if self.params.get('writedescription', False):
|
||||||
try:
|
try:
|
||||||
descfn = filename + u'.description'
|
descfn = filename + u'.description'
|
||||||
self.report_writedescription(descfn)
|
self.report_writedescription(descfn)
|
||||||
descfile = open(encodeFilename(descfn), 'wb')
|
with io.open(encodeFilename(descfn), 'w', encoding='utf-8') as descfile:
|
||||||
try:
|
descfile.write(info_dict['description'])
|
||||||
descfile.write(info_dict['description'].encode('utf-8'))
|
|
||||||
finally:
|
|
||||||
descfile.close()
|
|
||||||
except (OSError, IOError):
|
except (OSError, IOError):
|
||||||
self.trouble(u'ERROR: Cannot write description file ' + descfn)
|
self.report_error(u'Cannot write description file ' + descfn)
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.params.get('writesubtitles', False) and 'subtitles' in info_dict and info_dict['subtitles']:
|
if self.params.get('writesubtitles', False) and 'subtitles' in info_dict and info_dict['subtitles']:
|
||||||
# subtitles download errors are already managed as troubles in relevant IE
|
# subtitles download errors are already managed as troubles in relevant IE
|
||||||
# that way it will silently go on when used with unsupporting IE
|
# that way it will silently go on when used with unsupporting IE
|
||||||
|
subtitle = info_dict['subtitles'][0]
|
||||||
|
(sub_error, sub_lang, sub) = subtitle
|
||||||
|
sub_format = self.params.get('subtitlesformat')
|
||||||
|
if sub_error:
|
||||||
|
self.report_warning("Some error while getting the subtitles")
|
||||||
|
else:
|
||||||
try:
|
try:
|
||||||
srtfn = filename.rsplit('.', 1)[0] + u'.srt'
|
sub_filename = filename.rsplit('.', 1)[0] + u'.' + sub_lang + u'.' + sub_format
|
||||||
self.report_writesubtitles(srtfn)
|
self.report_writesubtitles(sub_filename)
|
||||||
srtfile = open(encodeFilename(srtfn), 'wb')
|
with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8') as subfile:
|
||||||
try:
|
subfile.write(sub)
|
||||||
srtfile.write(info_dict['subtitles'].encode('utf-8'))
|
|
||||||
finally:
|
|
||||||
srtfile.close()
|
|
||||||
except (OSError, IOError):
|
except (OSError, IOError):
|
||||||
self.trouble(u'ERROR: Cannot write subtitles file ' + descfn)
|
self.report_error(u'Cannot write subtitles file ' + descfn)
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.params.get('allsubtitles', False) and 'subtitles' in info_dict and info_dict['subtitles']:
|
||||||
|
subtitles = info_dict['subtitles']
|
||||||
|
sub_format = self.params.get('subtitlesformat')
|
||||||
|
for subtitle in subtitles:
|
||||||
|
(sub_error, sub_lang, sub) = subtitle
|
||||||
|
if sub_error:
|
||||||
|
self.report_warning("Some error while getting the subtitles")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
sub_filename = filename.rsplit('.', 1)[0] + u'.' + sub_lang + u'.' + sub_format
|
||||||
|
self.report_writesubtitles(sub_filename)
|
||||||
|
with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8') as subfile:
|
||||||
|
subfile.write(sub)
|
||||||
|
except (OSError, IOError):
|
||||||
|
self.report_error(u'Cannot write subtitles file ' + descfn)
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.params.get('writeinfojson', False):
|
if self.params.get('writeinfojson', False):
|
||||||
infofn = filename + u'.info.json'
|
infofn = filename + u'.info.json'
|
||||||
self.report_writeinfojson(infofn)
|
self.report_writeinfojson(infofn)
|
||||||
try:
|
try:
|
||||||
json.dump
|
json_info_dict = dict((k, v) for k,v in info_dict.items() if not k in ['urlhandle'])
|
||||||
except (NameError,AttributeError):
|
write_json_file(json_info_dict, encodeFilename(infofn))
|
||||||
self.trouble(u'ERROR: No JSON encoder found. Update to Python 2.6+, setup a json module, or leave out --write-info-json.')
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
infof = open(encodeFilename(infofn), 'wb')
|
|
||||||
try:
|
|
||||||
json_info_dict = dict((k,v) for k,v in info_dict.iteritems() if not k in ('urlhandle',))
|
|
||||||
json.dump(json_info_dict, infof)
|
|
||||||
finally:
|
|
||||||
infof.close()
|
|
||||||
except (OSError, IOError):
|
except (OSError, IOError):
|
||||||
self.trouble(u'ERROR: Cannot write metadata to JSON file ' + infofn)
|
self.report_error(u'Cannot write metadata to JSON file ' + infofn)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if self.params.get('writethumbnail', False):
|
||||||
|
if 'thumbnail' in info_dict:
|
||||||
|
thumb_format = info_dict['thumbnail'].rpartition(u'/')[2].rpartition(u'.')[2]
|
||||||
|
if not thumb_format:
|
||||||
|
thumb_format = 'jpg'
|
||||||
|
thumb_filename = filename.rpartition('.')[0] + u'.' + thumb_format
|
||||||
|
self.to_screen(u'[%s] %s: Downloading thumbnail ...' %
|
||||||
|
(info_dict['extractor'], info_dict['id']))
|
||||||
|
uf = compat_urllib_request.urlopen(info_dict['thumbnail'])
|
||||||
|
with open(thumb_filename, 'wb') as thumbf:
|
||||||
|
shutil.copyfileobj(uf, thumbf)
|
||||||
|
self.to_screen(u'[%s] %s: Writing thumbnail to: %s' %
|
||||||
|
(info_dict['extractor'], info_dict['id'], thumb_filename))
|
||||||
|
|
||||||
if not self.params.get('skip_download', False):
|
if not self.params.get('skip_download', False):
|
||||||
if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(filename)):
|
if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(filename)):
|
||||||
success = True
|
success = True
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
success = self._do_download(filename, info_dict)
|
success = self._do_download(filename, info_dict)
|
||||||
except (OSError, IOError), err:
|
except (OSError, IOError) as err:
|
||||||
raise UnavailableVideoError
|
raise UnavailableVideoError()
|
||||||
except (urllib2.URLError, httplib.HTTPException, socket.error), err:
|
except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
|
||||||
self.trouble(u'ERROR: unable to download video data: %s' % str(err))
|
self.report_error(u'unable to download video data: %s' % str(err))
|
||||||
return
|
return
|
||||||
except (ContentTooShortError, ), err:
|
except (ContentTooShortError, ) as err:
|
||||||
self.trouble(u'ERROR: content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded))
|
self.report_error(u'content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded))
|
||||||
return
|
return
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
try:
|
try:
|
||||||
self.post_process(filename, info_dict)
|
self.post_process(filename, info_dict)
|
||||||
except (PostProcessingError), err:
|
except (PostProcessingError) as err:
|
||||||
self.trouble(u'ERROR: postprocessing: %s' % str(err))
|
self.report_error(u'postprocessing: %s' % str(err))
|
||||||
return
|
return
|
||||||
|
|
||||||
def download(self, url_list):
|
def download(self, url_list):
|
||||||
@@ -475,58 +722,65 @@ class FileDownloader(object):
|
|||||||
raise SameFileError(self.params['outtmpl'])
|
raise SameFileError(self.params['outtmpl'])
|
||||||
|
|
||||||
for url in url_list:
|
for url in url_list:
|
||||||
suitable_found = False
|
|
||||||
for ie in self._ies:
|
|
||||||
# Go to next InfoExtractor if not suitable
|
|
||||||
if not ie.suitable(url):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Suitable InfoExtractor found
|
|
||||||
suitable_found = True
|
|
||||||
|
|
||||||
# Extract information from URL and process it
|
|
||||||
videos = ie.extract(url)
|
|
||||||
for video in videos or []:
|
|
||||||
video['extractor'] = ie.IE_NAME
|
|
||||||
try:
|
try:
|
||||||
self.increment_downloads()
|
#It also downloads the videos
|
||||||
self.process_info(video)
|
videos = self.extract_info(url)
|
||||||
except UnavailableVideoError:
|
except UnavailableVideoError:
|
||||||
self.trouble(u'\nERROR: unable to download video')
|
self.report_error(u'unable to download video')
|
||||||
|
except MaxDownloadsReached:
|
||||||
# Suitable InfoExtractor had been found; go to next URL
|
self.to_screen(u'[info] Maximum number of downloaded files reached.')
|
||||||
break
|
raise
|
||||||
|
|
||||||
if not suitable_found:
|
|
||||||
self.trouble(u'ERROR: no suitable InfoExtractor: %s' % url)
|
|
||||||
|
|
||||||
return self._download_retcode
|
return self._download_retcode
|
||||||
|
|
||||||
def post_process(self, filename, ie_info):
|
def post_process(self, filename, ie_info):
|
||||||
"""Run the postprocessing chain on the given file."""
|
"""Run all the postprocessors on the given file."""
|
||||||
info = dict(ie_info)
|
info = dict(ie_info)
|
||||||
info['filepath'] = filename
|
info['filepath'] = filename
|
||||||
|
keep_video = None
|
||||||
for pp in self._pps:
|
for pp in self._pps:
|
||||||
info = pp.run(info)
|
try:
|
||||||
if info is None:
|
keep_video_wish,new_info = pp.run(info)
|
||||||
break
|
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:
|
||||||
|
self.to_stderr(u'ERROR: ' + e.msg)
|
||||||
|
if keep_video is False and not self.params.get('keepvideo', False):
|
||||||
|
try:
|
||||||
|
self.to_screen(u'Deleting original file %s (pass -k to keep)' % filename)
|
||||||
|
os.remove(encodeFilename(filename))
|
||||||
|
except (IOError, OSError):
|
||||||
|
self.report_warning(u'Unable to remove downloaded video file')
|
||||||
|
|
||||||
def _download_with_rtmpdump(self, filename, url, player_url):
|
def _download_with_rtmpdump(self, filename, url, player_url, page_url, play_path, tc_url):
|
||||||
self.report_destination(filename)
|
self.report_destination(filename)
|
||||||
tmpfilename = self.temp_name(filename)
|
tmpfilename = self.temp_name(filename)
|
||||||
|
|
||||||
# Check for rtmpdump first
|
# Check for rtmpdump first
|
||||||
try:
|
try:
|
||||||
subprocess.call(['rtmpdump', '-h'], stdout=(file(os.path.devnull, 'w')), stderr=subprocess.STDOUT)
|
subprocess.call(['rtmpdump', '-h'], stdout=(open(os.path.devnull, 'w')), stderr=subprocess.STDOUT)
|
||||||
except (OSError, IOError):
|
except (OSError, IOError):
|
||||||
self.trouble(u'ERROR: RTMP download detected but "rtmpdump" could not be run')
|
self.report_error(u'RTMP download detected but "rtmpdump" could not be run')
|
||||||
return False
|
return False
|
||||||
|
verbosity_option = '--verbose' if self.params.get('verbose', False) else '--quiet'
|
||||||
|
|
||||||
# Download using rtmpdump. rtmpdump returns exit code 2 when
|
# Download using rtmpdump. rtmpdump returns exit code 2 when
|
||||||
# the connection was interrumpted and resuming appears to be
|
# the connection was interrumpted and resuming appears to be
|
||||||
# possible. This is part of rtmpdump's normal usage, AFAIK.
|
# possible. This is part of rtmpdump's normal usage, AFAIK.
|
||||||
basic_args = ['rtmpdump', '-q'] + [[], ['-W', player_url]][player_url is not None] + ['-r', url, '-o', tmpfilename]
|
basic_args = ['rtmpdump', verbosity_option, '-r', url, '-o', tmpfilename]
|
||||||
args = basic_args + [[], ['-e', '-k', '1']][self.params.get('continuedl', False)]
|
if player_url is not None:
|
||||||
|
basic_args += ['--swfVfy', player_url]
|
||||||
|
if page_url is not None:
|
||||||
|
basic_args += ['--pageUrl', page_url]
|
||||||
|
if play_path is not None:
|
||||||
|
basic_args += ['--playpath', play_path]
|
||||||
|
if tc_url is not None:
|
||||||
|
basic_args += ['--tcUrl', url]
|
||||||
|
args = basic_args + [[], ['--resume', '--skip', '1']][self.params.get('continuedl', False)]
|
||||||
if self.params.get('verbose', False):
|
if self.params.get('verbose', False):
|
||||||
try:
|
try:
|
||||||
import pipes
|
import pipes
|
||||||
@@ -549,33 +803,88 @@ class FileDownloader(object):
|
|||||||
retval = 0
|
retval = 0
|
||||||
break
|
break
|
||||||
if retval == 0:
|
if retval == 0:
|
||||||
self.to_screen(u'\r[rtmpdump] %s bytes' % os.path.getsize(encodeFilename(tmpfilename)))
|
fsize = os.path.getsize(encodeFilename(tmpfilename))
|
||||||
|
self.to_screen(u'\r[rtmpdump] %s bytes' % fsize)
|
||||||
self.try_rename(tmpfilename, filename)
|
self.try_rename(tmpfilename, filename)
|
||||||
|
self._hook_progress({
|
||||||
|
'downloaded_bytes': fsize,
|
||||||
|
'total_bytes': fsize,
|
||||||
|
'filename': filename,
|
||||||
|
'status': 'finished',
|
||||||
|
})
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
self.trouble(u'\nERROR: rtmpdump exited with code %d' % retval)
|
self.to_stderr(u"\n")
|
||||||
|
self.report_error(u'rtmpdump exited with code %d' % retval)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _download_with_mplayer(self, filename, url):
|
||||||
|
self.report_destination(filename)
|
||||||
|
tmpfilename = self.temp_name(filename)
|
||||||
|
|
||||||
|
args = ['mplayer', '-really-quiet', '-vo', 'null', '-vc', 'dummy', '-dumpstream', '-dumpfile', tmpfilename, url]
|
||||||
|
# Check for mplayer first
|
||||||
|
try:
|
||||||
|
subprocess.call(['mplayer', '-h'], stdout=(open(os.path.devnull, 'w')), stderr=subprocess.STDOUT)
|
||||||
|
except (OSError, IOError):
|
||||||
|
self.report_error(u'MMS or RTSP download detected but "%s" could not be run' % args[0] )
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Download using mplayer.
|
||||||
|
retval = subprocess.call(args)
|
||||||
|
if retval == 0:
|
||||||
|
fsize = os.path.getsize(encodeFilename(tmpfilename))
|
||||||
|
self.to_screen(u'\r[%s] %s bytes' % (args[0], fsize))
|
||||||
|
self.try_rename(tmpfilename, filename)
|
||||||
|
self._hook_progress({
|
||||||
|
'downloaded_bytes': fsize,
|
||||||
|
'total_bytes': fsize,
|
||||||
|
'filename': filename,
|
||||||
|
'status': 'finished',
|
||||||
|
})
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.to_stderr(u"\n")
|
||||||
|
self.report_error(u'mplayer exited with code %d' % retval)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _do_download(self, filename, info_dict):
|
def _do_download(self, filename, info_dict):
|
||||||
url = info_dict['url']
|
url = info_dict['url']
|
||||||
player_url = info_dict.get('player_url', None)
|
|
||||||
|
|
||||||
# Check file already present
|
# Check file already present
|
||||||
if self.params.get('continuedl', False) and os.path.isfile(encodeFilename(filename)) and not self.params.get('nopart', False):
|
if self.params.get('continuedl', False) and os.path.isfile(encodeFilename(filename)) and not self.params.get('nopart', False):
|
||||||
self.report_file_already_downloaded(filename)
|
self.report_file_already_downloaded(filename)
|
||||||
|
self._hook_progress({
|
||||||
|
'filename': filename,
|
||||||
|
'status': 'finished',
|
||||||
|
})
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Attempt to download using rtmpdump
|
# Attempt to download using rtmpdump
|
||||||
if url.startswith('rtmp'):
|
if url.startswith('rtmp'):
|
||||||
return self._download_with_rtmpdump(filename, url, player_url)
|
return self._download_with_rtmpdump(filename, url,
|
||||||
|
info_dict.get('player_url', None),
|
||||||
|
info_dict.get('page_url', None),
|
||||||
|
info_dict.get('play_path', None),
|
||||||
|
info_dict.get('tc_url', None))
|
||||||
|
|
||||||
|
# Attempt to download using mplayer
|
||||||
|
if url.startswith('mms') or url.startswith('rtsp'):
|
||||||
|
return self._download_with_mplayer(filename, url)
|
||||||
|
|
||||||
tmpfilename = self.temp_name(filename)
|
tmpfilename = self.temp_name(filename)
|
||||||
stream = None
|
stream = None
|
||||||
|
|
||||||
# Do not include the Accept-Encoding header
|
# Do not include the Accept-Encoding header
|
||||||
headers = {'Youtubedl-no-compression': 'True'}
|
headers = {'Youtubedl-no-compression': 'True'}
|
||||||
basic_request = urllib2.Request(url, None, headers)
|
if 'user_agent' in info_dict:
|
||||||
request = urllib2.Request(url, None, headers)
|
headers['Youtubedl-user-agent'] = info_dict['user_agent']
|
||||||
|
basic_request = compat_urllib_request.Request(url, None, headers)
|
||||||
|
request = compat_urllib_request.Request(url, None, headers)
|
||||||
|
|
||||||
|
if self.params.get('test', False):
|
||||||
|
request.add_header('Range','bytes=0-10240')
|
||||||
|
|
||||||
# Establish possible resume length
|
# Establish possible resume length
|
||||||
if os.path.isfile(encodeFilename(tmpfilename)):
|
if os.path.isfile(encodeFilename(tmpfilename)):
|
||||||
@@ -599,9 +908,9 @@ class FileDownloader(object):
|
|||||||
try:
|
try:
|
||||||
if count == 0 and 'urlhandle' in info_dict:
|
if count == 0 and 'urlhandle' in info_dict:
|
||||||
data = info_dict['urlhandle']
|
data = info_dict['urlhandle']
|
||||||
data = urllib2.urlopen(request)
|
data = compat_urllib_request.urlopen(request)
|
||||||
break
|
break
|
||||||
except (urllib2.HTTPError, ), 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:
|
||||||
# Unexpected HTTP error
|
# Unexpected HTTP error
|
||||||
raise
|
raise
|
||||||
@@ -609,15 +918,15 @@ class FileDownloader(object):
|
|||||||
# Unable to resume (requested range not satisfiable)
|
# Unable to resume (requested range not satisfiable)
|
||||||
try:
|
try:
|
||||||
# Open the connection again without the range header
|
# Open the connection again without the range header
|
||||||
data = urllib2.urlopen(basic_request)
|
data = compat_urllib_request.urlopen(basic_request)
|
||||||
content_length = data.info()['Content-Length']
|
content_length = data.info()['Content-Length']
|
||||||
except (urllib2.HTTPError, ), err:
|
except (compat_urllib_error.HTTPError, ) as err:
|
||||||
if err.code < 500 or err.code >= 600:
|
if err.code < 500 or err.code >= 600:
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
# Examine the reported length
|
# Examine the reported length
|
||||||
if (content_length is not None and
|
if (content_length is not None and
|
||||||
(resume_len - 100 < long(content_length) < resume_len + 100)):
|
(resume_len - 100 < int(content_length) < resume_len + 100)):
|
||||||
# The file had already been fully downloaded.
|
# The file had already been fully downloaded.
|
||||||
# Explanation to the above condition: in issue #175 it was revealed that
|
# Explanation to the above condition: in issue #175 it was revealed that
|
||||||
# YouTube sometimes adds or removes a few bytes from the end of the file,
|
# YouTube sometimes adds or removes a few bytes from the end of the file,
|
||||||
@@ -627,6 +936,10 @@ class FileDownloader(object):
|
|||||||
# the one in the hard drive.
|
# the one in the hard drive.
|
||||||
self.report_file_already_downloaded(filename)
|
self.report_file_already_downloaded(filename)
|
||||||
self.try_rename(tmpfilename, filename)
|
self.try_rename(tmpfilename, filename)
|
||||||
|
self._hook_progress({
|
||||||
|
'filename': filename,
|
||||||
|
'status': 'finished',
|
||||||
|
})
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
# The length does not match, we start the download over
|
# The length does not match, we start the download over
|
||||||
@@ -639,12 +952,21 @@ class FileDownloader(object):
|
|||||||
self.report_retry(count, retries)
|
self.report_retry(count, retries)
|
||||||
|
|
||||||
if count > retries:
|
if count > retries:
|
||||||
self.trouble(u'ERROR: giving up after %s retries' % retries)
|
self.report_error(u'giving up after %s retries' % retries)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
data_len = data.info().get('Content-length', None)
|
data_len = data.info().get('Content-length', None)
|
||||||
if data_len is not None:
|
if data_len is not None:
|
||||||
data_len = long(data_len) + resume_len
|
data_len = int(data_len) + resume_len
|
||||||
|
min_data_len = self.params.get("min_filesize", None)
|
||||||
|
max_data_len = self.params.get("max_filesize", None)
|
||||||
|
if min_data_len is not None and data_len < min_data_len:
|
||||||
|
self.to_screen(u'\r[download] File is smaller than min-filesize (%s bytes < %s bytes). Aborting.' % (data_len, min_data_len))
|
||||||
|
return False
|
||||||
|
if max_data_len is not None and data_len > max_data_len:
|
||||||
|
self.to_screen(u'\r[download] File is larger than max-filesize (%s bytes > %s bytes). Aborting.' % (data_len, max_data_len))
|
||||||
|
return False
|
||||||
|
|
||||||
data_len_str = self.format_bytes(data_len)
|
data_len_str = self.format_bytes(data_len)
|
||||||
byte_counter = 0 + resume_len
|
byte_counter = 0 + resume_len
|
||||||
block_size = self.params.get('buffersize', 1024)
|
block_size = self.params.get('buffersize', 1024)
|
||||||
@@ -665,13 +987,14 @@ class FileDownloader(object):
|
|||||||
assert stream is not None
|
assert stream is not None
|
||||||
filename = self.undo_temp_name(tmpfilename)
|
filename = self.undo_temp_name(tmpfilename)
|
||||||
self.report_destination(filename)
|
self.report_destination(filename)
|
||||||
except (OSError, IOError), err:
|
except (OSError, IOError) as err:
|
||||||
self.trouble(u'ERROR: unable to open for writing: %s' % str(err))
|
self.report_error(u'unable to open for writing: %s' % str(err))
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
stream.write(data_block)
|
stream.write(data_block)
|
||||||
except (IOError, OSError), err:
|
except (IOError, OSError) as err:
|
||||||
self.trouble(u'\nERROR: unable to write data: %s' % str(err))
|
self.to_stderr(u"\n")
|
||||||
|
self.report_error(u'unable to write data: %s' % str(err))
|
||||||
return False
|
return False
|
||||||
if not self.params.get('noresizebuffer', False):
|
if not self.params.get('noresizebuffer', False):
|
||||||
block_size = self.best_block_size(after - before, len(data_block))
|
block_size = self.best_block_size(after - before, len(data_block))
|
||||||
@@ -685,20 +1008,56 @@ class FileDownloader(object):
|
|||||||
eta_str = self.calc_eta(start, time.time(), data_len - resume_len, byte_counter - resume_len)
|
eta_str = self.calc_eta(start, time.time(), data_len - resume_len, byte_counter - resume_len)
|
||||||
self.report_progress(percent_str, data_len_str, speed_str, eta_str)
|
self.report_progress(percent_str, data_len_str, speed_str, eta_str)
|
||||||
|
|
||||||
|
self._hook_progress({
|
||||||
|
'downloaded_bytes': byte_counter,
|
||||||
|
'total_bytes': data_len,
|
||||||
|
'tmpfilename': tmpfilename,
|
||||||
|
'filename': filename,
|
||||||
|
'status': 'downloading',
|
||||||
|
})
|
||||||
|
|
||||||
# Apply rate limit
|
# Apply rate limit
|
||||||
self.slow_down(start, byte_counter - resume_len)
|
self.slow_down(start, byte_counter - resume_len)
|
||||||
|
|
||||||
if stream is None:
|
if stream is None:
|
||||||
self.trouble(u'\nERROR: Did not get any data blocks')
|
self.to_stderr(u"\n")
|
||||||
|
self.report_error(u'Did not get any data blocks')
|
||||||
return False
|
return False
|
||||||
stream.close()
|
stream.close()
|
||||||
self.report_finish()
|
self.report_finish()
|
||||||
if data_len is not None and byte_counter != data_len:
|
if data_len is not None and byte_counter != data_len:
|
||||||
raise ContentTooShortError(byte_counter, long(data_len))
|
raise ContentTooShortError(byte_counter, int(data_len))
|
||||||
self.try_rename(tmpfilename, filename)
|
self.try_rename(tmpfilename, filename)
|
||||||
|
|
||||||
# Update file modification time
|
# Update file modification time
|
||||||
if self.params.get('updatetime', True):
|
if self.params.get('updatetime', True):
|
||||||
info_dict['filetime'] = self.try_utime(filename, data.info().get('last-modified', None))
|
info_dict['filetime'] = self.try_utime(filename, data.info().get('last-modified', None))
|
||||||
|
|
||||||
|
self._hook_progress({
|
||||||
|
'downloaded_bytes': byte_counter,
|
||||||
|
'total_bytes': byte_counter,
|
||||||
|
'filename': filename,
|
||||||
|
'status': 'finished',
|
||||||
|
})
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _hook_progress(self, status):
|
||||||
|
for ph in self._progress_hooks:
|
||||||
|
ph(status)
|
||||||
|
|
||||||
|
def add_progress_hook(self, ph):
|
||||||
|
""" ph gets called on download progress, with a dictionary with the entries
|
||||||
|
* filename: The final filename
|
||||||
|
* status: One of "downloading" and "finished"
|
||||||
|
|
||||||
|
It can also have some of the following entries:
|
||||||
|
|
||||||
|
* downloaded_bytes: Bytes on disks
|
||||||
|
* total_bytes: Total bytes, None if unknown
|
||||||
|
* tmpfilename: The filename we're currently writing to
|
||||||
|
|
||||||
|
Hooks are guaranteed to be called at least once (with status "finished")
|
||||||
|
if the download is successful.
|
||||||
|
"""
|
||||||
|
self._progress_hooks.append(ph)
|
||||||
|
4623
youtube_dl/InfoExtractors.py
Normal file → Executable file
4623
youtube_dl/InfoExtractors.py
Normal file → Executable file
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,14 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from utils import *
|
from .utils import *
|
||||||
|
|
||||||
|
|
||||||
class PostProcessor(object):
|
class PostProcessor(object):
|
||||||
@@ -43,30 +45,24 @@ class PostProcessor(object):
|
|||||||
one has an extra field called "filepath" that points to the
|
one has an extra field called "filepath" that points to the
|
||||||
downloaded file.
|
downloaded file.
|
||||||
|
|
||||||
When this method returns None, the postprocessing chain is
|
This method returns a tuple, the first element of which describes
|
||||||
stopped. However, this method may return an information
|
whether the original file should be kept (i.e. not deleted - None for
|
||||||
dictionary that will be passed to the next postprocessing
|
no preference), and the second of which is the updated information.
|
||||||
object in the chain. It can be the one it received after
|
|
||||||
changing some fields.
|
|
||||||
|
|
||||||
In addition, this method may raise a PostProcessingError
|
In addition, this method may raise a PostProcessingError
|
||||||
exception that will be taken into account by the downloader
|
exception if post processing fails.
|
||||||
it was called from.
|
|
||||||
"""
|
"""
|
||||||
return information # by default, do nothing
|
return None, information # by default, keep file and do nothing
|
||||||
|
|
||||||
class AudioConversionError(BaseException):
|
class FFmpegPostProcessorError(PostProcessingError):
|
||||||
def __init__(self, message):
|
pass
|
||||||
self.message = message
|
|
||||||
|
|
||||||
class FFmpegExtractAudioPP(PostProcessor):
|
class AudioConversionError(PostProcessingError):
|
||||||
def __init__(self, downloader=None, preferredcodec=None, preferredquality=None, keepvideo=False):
|
pass
|
||||||
|
|
||||||
|
class FFmpegPostProcessor(PostProcessor):
|
||||||
|
def __init__(self,downloader=None):
|
||||||
PostProcessor.__init__(self, downloader)
|
PostProcessor.__init__(self, downloader)
|
||||||
if preferredcodec is None:
|
|
||||||
preferredcodec = 'best'
|
|
||||||
self._preferredcodec = preferredcodec
|
|
||||||
self._preferredquality = preferredquality
|
|
||||||
self._keepvideo = keepvideo
|
|
||||||
self._exes = self.detect_executables()
|
self._exes = self.detect_executables()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -80,18 +76,46 @@ class FFmpegExtractAudioPP(PostProcessor):
|
|||||||
programs = ['avprobe', 'avconv', 'ffmpeg', 'ffprobe']
|
programs = ['avprobe', 'avconv', 'ffmpeg', 'ffprobe']
|
||||||
return dict((program, executable(program)) for program in programs)
|
return dict((program, executable(program)) for program in programs)
|
||||||
|
|
||||||
|
def run_ffmpeg(self, path, out_path, opts):
|
||||||
|
if not self._exes['ffmpeg'] and not self._exes['avconv']:
|
||||||
|
raise FFmpegPostProcessorError(u'ffmpeg or avconv not found. Please install one.')
|
||||||
|
cmd = ([self._exes['avconv'] or self._exes['ffmpeg'], '-y', '-i', encodeFilename(path)]
|
||||||
|
+ opts +
|
||||||
|
[encodeFilename(self._ffmpeg_filename_argument(out_path))])
|
||||||
|
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
stdout,stderr = p.communicate()
|
||||||
|
if p.returncode != 0:
|
||||||
|
stderr = stderr.decode('utf-8', 'replace')
|
||||||
|
msg = stderr.strip().split('\n')[-1]
|
||||||
|
raise FFmpegPostProcessorError(msg)
|
||||||
|
|
||||||
|
def _ffmpeg_filename_argument(self, fn):
|
||||||
|
# ffmpeg broke --, see https://ffmpeg.org/trac/ffmpeg/ticket/2127 for details
|
||||||
|
if fn.startswith(u'-'):
|
||||||
|
return u'./' + fn
|
||||||
|
return fn
|
||||||
|
|
||||||
|
class FFmpegExtractAudioPP(FFmpegPostProcessor):
|
||||||
|
def __init__(self, downloader=None, preferredcodec=None, preferredquality=None, nopostoverwrites=False):
|
||||||
|
FFmpegPostProcessor.__init__(self, downloader)
|
||||||
|
if preferredcodec is None:
|
||||||
|
preferredcodec = 'best'
|
||||||
|
self._preferredcodec = preferredcodec
|
||||||
|
self._preferredquality = preferredquality
|
||||||
|
self._nopostoverwrites = nopostoverwrites
|
||||||
|
|
||||||
def get_audio_codec(self, path):
|
def get_audio_codec(self, path):
|
||||||
if not self._exes['ffprobe'] and not self._exes['avprobe']: return None
|
if not self._exes['ffprobe'] and not self._exes['avprobe']: return None
|
||||||
try:
|
try:
|
||||||
cmd = [self._exes['avprobe'] or self._exes['ffprobe'], '-show_streams', '--', encodeFilename(path)]
|
cmd = [self._exes['avprobe'] or self._exes['ffprobe'], '-show_streams', encodeFilename(self._ffmpeg_filename_argument(path))]
|
||||||
handle = subprocess.Popen(cmd, stderr=file(os.path.devnull, 'w'), stdout=subprocess.PIPE)
|
handle = subprocess.Popen(cmd, stderr=compat_subprocess_get_DEVNULL(), stdout=subprocess.PIPE)
|
||||||
output = handle.communicate()[0]
|
output = handle.communicate()[0]
|
||||||
if handle.wait() != 0:
|
if handle.wait() != 0:
|
||||||
return None
|
return None
|
||||||
except (IOError, OSError):
|
except (IOError, OSError):
|
||||||
return None
|
return None
|
||||||
audio_codec = None
|
audio_codec = None
|
||||||
for line in output.split('\n'):
|
for line in output.decode('ascii', 'ignore').split('\n'):
|
||||||
if line.startswith('codec_name='):
|
if line.startswith('codec_name='):
|
||||||
audio_codec = line.split('=')[1].strip()
|
audio_codec = line.split('=')[1].strip()
|
||||||
elif line.strip() == 'codec_type=audio' and audio_codec is not None:
|
elif line.strip() == 'codec_type=audio' and audio_codec is not None:
|
||||||
@@ -105,31 +129,27 @@ class FFmpegExtractAudioPP(PostProcessor):
|
|||||||
acodec_opts = []
|
acodec_opts = []
|
||||||
else:
|
else:
|
||||||
acodec_opts = ['-acodec', codec]
|
acodec_opts = ['-acodec', codec]
|
||||||
cmd = ([self._exes['avconv'] or self._exes['ffmpeg'], '-y', '-i', encodeFilename(path), '-vn']
|
opts = ['-vn'] + acodec_opts + more_opts
|
||||||
+ acodec_opts + more_opts +
|
try:
|
||||||
['--', encodeFilename(out_path)])
|
FFmpegPostProcessor.run_ffmpeg(self, path, out_path, opts)
|
||||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
except FFmpegPostProcessorError as err:
|
||||||
stdout,stderr = p.communicate()
|
raise AudioConversionError(err.message)
|
||||||
if p.returncode != 0:
|
|
||||||
msg = stderr.strip().split('\n')[-1]
|
|
||||||
raise AudioConversionError(msg)
|
|
||||||
|
|
||||||
def run(self, information):
|
def run(self, information):
|
||||||
path = information['filepath']
|
path = information['filepath']
|
||||||
|
|
||||||
filecodec = self.get_audio_codec(path)
|
filecodec = self.get_audio_codec(path)
|
||||||
if filecodec is None:
|
if filecodec is None:
|
||||||
self._downloader.to_stderr(u'WARNING: unable to obtain file audio codec with ffprobe')
|
raise PostProcessingError(u'WARNING: unable to obtain file audio codec with ffprobe')
|
||||||
return None
|
|
||||||
|
|
||||||
more_opts = []
|
more_opts = []
|
||||||
if self._preferredcodec == 'best' or self._preferredcodec == filecodec or (self._preferredcodec == 'm4a' and filecodec == 'aac'):
|
if self._preferredcodec == 'best' or self._preferredcodec == filecodec or (self._preferredcodec == 'm4a' and filecodec == 'aac'):
|
||||||
if self._preferredcodec == 'm4a' and filecodec == 'aac':
|
if filecodec == 'aac' and self._preferredcodec in ['m4a', 'best']:
|
||||||
# Lossless, but in another container
|
# Lossless, but in another container
|
||||||
acodec = 'copy'
|
acodec = 'copy'
|
||||||
extension = self._preferredcodec
|
extension = 'm4a'
|
||||||
more_opts = [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
|
more_opts = [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
|
||||||
elif filecodec in ['aac', 'mp3', 'vorbis']:
|
elif filecodec in ['aac', 'mp3', 'vorbis', 'opus']:
|
||||||
# Lossless if possible
|
# Lossless if possible
|
||||||
acodec = 'copy'
|
acodec = 'copy'
|
||||||
extension = filecodec
|
extension = filecodec
|
||||||
@@ -149,7 +169,7 @@ class FFmpegExtractAudioPP(PostProcessor):
|
|||||||
more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality + 'k']
|
more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality + 'k']
|
||||||
else:
|
else:
|
||||||
# We convert the audio (lossy)
|
# We convert the audio (lossy)
|
||||||
acodec = {'mp3': 'libmp3lame', 'aac': 'aac', 'm4a': 'aac', 'vorbis': 'libvorbis', 'wav': None}[self._preferredcodec]
|
acodec = {'mp3': 'libmp3lame', 'aac': 'aac', 'm4a': 'aac', 'opus': 'opus', 'vorbis': 'libvorbis', 'wav': None}[self._preferredcodec]
|
||||||
extension = self._preferredcodec
|
extension = self._preferredcodec
|
||||||
more_opts = []
|
more_opts = []
|
||||||
if self._preferredquality is not None:
|
if self._preferredquality is not None:
|
||||||
@@ -169,16 +189,24 @@ class FFmpegExtractAudioPP(PostProcessor):
|
|||||||
|
|
||||||
prefix, sep, ext = path.rpartition(u'.') # not os.path.splitext, since the latter does not work on unicode in all setups
|
prefix, sep, ext = path.rpartition(u'.') # not os.path.splitext, since the latter does not work on unicode in all setups
|
||||||
new_path = prefix + sep + extension
|
new_path = prefix + sep + extension
|
||||||
self._downloader.to_screen(u'[' + (self._exes['avconv'] and 'avconv' or 'ffmpeg') + '] Destination: ' + new_path)
|
|
||||||
|
# If we download foo.mp3 and convert it to... foo.mp3, then don't delete foo.mp3, silly.
|
||||||
|
if new_path == path:
|
||||||
|
self._nopostoverwrites = True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if self._nopostoverwrites and os.path.exists(encodeFilename(new_path)):
|
||||||
|
self._downloader.to_screen(u'[youtube] Post-process file %s exists, skipping' % new_path)
|
||||||
|
else:
|
||||||
|
self._downloader.to_screen(u'[' + (self._exes['avconv'] and 'avconv' or 'ffmpeg') + '] Destination: ' + new_path)
|
||||||
self.run_ffmpeg(path, new_path, acodec, more_opts)
|
self.run_ffmpeg(path, new_path, acodec, more_opts)
|
||||||
except:
|
except:
|
||||||
etype,e,tb = sys.exc_info()
|
etype,e,tb = sys.exc_info()
|
||||||
if isinstance(e, AudioConversionError):
|
if isinstance(e, AudioConversionError):
|
||||||
self._downloader.to_stderr(u'ERROR: audio conversion failed: ' + e.message)
|
msg = u'audio conversion failed: ' + e.message
|
||||||
else:
|
else:
|
||||||
self._downloader.to_stderr(u'ERROR: error running ' + (self._exes['avconv'] and 'avconv' or 'ffmpeg'))
|
msg = u'error running ' + (self._exes['avconv'] and 'avconv' or 'ffmpeg')
|
||||||
return None
|
raise PostProcessingError(msg)
|
||||||
|
|
||||||
# Try to update the date time for extracted audio file.
|
# Try to update the date time for extracted audio file.
|
||||||
if information.get('filetime') is not None:
|
if information.get('filetime') is not None:
|
||||||
@@ -187,12 +215,24 @@ class FFmpegExtractAudioPP(PostProcessor):
|
|||||||
except:
|
except:
|
||||||
self._downloader.to_stderr(u'WARNING: Cannot update utime of audio file')
|
self._downloader.to_stderr(u'WARNING: Cannot update utime of audio file')
|
||||||
|
|
||||||
if not self._keepvideo:
|
|
||||||
try:
|
|
||||||
os.remove(encodeFilename(path))
|
|
||||||
except (IOError, OSError):
|
|
||||||
self._downloader.to_stderr(u'WARNING: Unable to remove downloaded video file')
|
|
||||||
return None
|
|
||||||
|
|
||||||
information['filepath'] = new_path
|
information['filepath'] = new_path
|
||||||
return information
|
return self._nopostoverwrites,information
|
||||||
|
|
||||||
|
class FFmpegVideoConvertor(FFmpegPostProcessor):
|
||||||
|
def __init__(self, downloader=None,preferedformat=None):
|
||||||
|
super(FFmpegVideoConvertor, self).__init__(downloader)
|
||||||
|
self._preferedformat=preferedformat
|
||||||
|
|
||||||
|
def run(self, information):
|
||||||
|
path = information['filepath']
|
||||||
|
prefix, sep, ext = path.rpartition(u'.')
|
||||||
|
outpath = prefix + sep + self._preferedformat
|
||||||
|
if information['ext'] == self._preferedformat:
|
||||||
|
self._downloader.to_screen(u'[ffmpeg] Not converting video file %s - already is in target format %s' % (path, self._preferedformat))
|
||||||
|
return True,information
|
||||||
|
self._downloader.to_screen(u'['+'ffmpeg'+'] Converting video from %s to %s, Destination: ' % (information['ext'], self._preferedformat) +outpath)
|
||||||
|
self.run_ffmpeg(path, outpath, [])
|
||||||
|
information['filepath'] = outpath
|
||||||
|
information['format'] = self._preferedformat
|
||||||
|
information['ext'] = self._preferedformat
|
||||||
|
return False,information
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from __future__ import with_statement
|
from __future__ import with_statement
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
__authors__ = (
|
__authors__ = (
|
||||||
'Ricardo Garcia Gonzalez',
|
'Ricardo Garcia Gonzalez',
|
||||||
@@ -19,17 +20,19 @@ __authors__ = (
|
|||||||
'shizeeg',
|
'shizeeg',
|
||||||
'Filippo Valsorda',
|
'Filippo Valsorda',
|
||||||
'Christian Albrecht',
|
'Christian Albrecht',
|
||||||
|
'Dave Vasilevsky',
|
||||||
|
'Jaime Marquínez Ferrándiz',
|
||||||
|
'Jeff Crouse',
|
||||||
|
'Osama Khalid',
|
||||||
|
'Michael Walter',
|
||||||
|
'M. Yasoob Ullah Khalid',
|
||||||
|
'Julien Fraichard',
|
||||||
|
'Johny Mo Swag',
|
||||||
)
|
)
|
||||||
|
|
||||||
__license__ = 'Public Domain'
|
__license__ = 'Public Domain'
|
||||||
__version__ = '2012.11.29'
|
|
||||||
|
|
||||||
UPDATE_URL = 'https://raw.github.com/rg3/youtube-dl/master/youtube-dl'
|
import codecs
|
||||||
UPDATE_URL_VERSION = 'https://raw.github.com/rg3/youtube-dl/master/LATEST_VERSION'
|
|
||||||
UPDATE_URL_EXE = 'https://raw.github.com/rg3/youtube-dl/master/youtube-dl.exe'
|
|
||||||
|
|
||||||
|
|
||||||
import cookielib
|
|
||||||
import getpass
|
import getpass
|
||||||
import optparse
|
import optparse
|
||||||
import os
|
import os
|
||||||
@@ -38,77 +41,17 @@ import shlex
|
|||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import urllib2
|
|
||||||
import warnings
|
import warnings
|
||||||
|
import platform
|
||||||
|
|
||||||
from utils import *
|
from .utils import *
|
||||||
from FileDownloader import *
|
from .update import update_self
|
||||||
from InfoExtractors import *
|
from .version import __version__
|
||||||
from PostProcessor import *
|
from .FileDownloader import *
|
||||||
|
from .InfoExtractors import gen_extractors
|
||||||
|
from .PostProcessor import *
|
||||||
|
|
||||||
def updateSelf(downloader, filename):
|
def parseOpts(overrideArguments=None):
|
||||||
''' Update the program file with the latest version from the repository '''
|
|
||||||
# Note: downloader only used for options
|
|
||||||
|
|
||||||
if not os.access(filename, os.W_OK):
|
|
||||||
sys.exit('ERROR: no write permissions on %s' % filename)
|
|
||||||
|
|
||||||
downloader.to_screen(u'Updating to latest version...')
|
|
||||||
|
|
||||||
urlv = urllib2.urlopen(UPDATE_URL_VERSION)
|
|
||||||
newversion = urlv.read().strip()
|
|
||||||
if newversion == __version__:
|
|
||||||
downloader.to_screen(u'youtube-dl is up-to-date (' + __version__ + ')')
|
|
||||||
return
|
|
||||||
urlv.close()
|
|
||||||
|
|
||||||
if hasattr(sys, "frozen"): #py2exe
|
|
||||||
exe = os.path.abspath(filename)
|
|
||||||
directory = os.path.dirname(exe)
|
|
||||||
if not os.access(directory, os.W_OK):
|
|
||||||
sys.exit('ERROR: no write permissions on %s' % directory)
|
|
||||||
|
|
||||||
try:
|
|
||||||
urlh = urllib2.urlopen(UPDATE_URL_EXE)
|
|
||||||
newcontent = urlh.read()
|
|
||||||
urlh.close()
|
|
||||||
with open(exe + '.new', 'wb') as outf:
|
|
||||||
outf.write(newcontent)
|
|
||||||
except (IOError, OSError), err:
|
|
||||||
sys.exit('ERROR: unable to download latest version')
|
|
||||||
|
|
||||||
try:
|
|
||||||
bat = os.path.join(directory, 'youtube-dl-updater.bat')
|
|
||||||
b = open(bat, 'w')
|
|
||||||
b.write("""
|
|
||||||
echo Updating youtube-dl...
|
|
||||||
ping 127.0.0.1 -n 5 -w 1000 > NUL
|
|
||||||
move /Y "%s.new" "%s"
|
|
||||||
del "%s"
|
|
||||||
\n""" %(exe, exe, bat))
|
|
||||||
b.close()
|
|
||||||
|
|
||||||
os.startfile(bat)
|
|
||||||
except (IOError, OSError), err:
|
|
||||||
sys.exit('ERROR: unable to overwrite current version')
|
|
||||||
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
urlh = urllib2.urlopen(UPDATE_URL)
|
|
||||||
newcontent = urlh.read()
|
|
||||||
urlh.close()
|
|
||||||
except (IOError, OSError), err:
|
|
||||||
sys.exit('ERROR: unable to download latest version')
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(filename, 'wb') as outf:
|
|
||||||
outf.write(newcontent)
|
|
||||||
except (IOError, OSError), err:
|
|
||||||
sys.exit('ERROR: unable to overwrite current version')
|
|
||||||
|
|
||||||
downloader.to_screen(u'Updated youtube-dl. Restart youtube-dl to use the new version.')
|
|
||||||
|
|
||||||
def parseOpts():
|
|
||||||
def _readOptions(filename_bytes):
|
def _readOptions(filename_bytes):
|
||||||
try:
|
try:
|
||||||
optionf = open(filename_bytes)
|
optionf = open(filename_bytes)
|
||||||
@@ -127,9 +70,12 @@ def parseOpts():
|
|||||||
|
|
||||||
opts = []
|
opts = []
|
||||||
|
|
||||||
if option._short_opts: opts.append(option._short_opts[0])
|
if option._short_opts:
|
||||||
if option._long_opts: opts.append(option._long_opts[0])
|
opts.append(option._short_opts[0])
|
||||||
if len(opts) > 1: opts.insert(1, ', ')
|
if option._long_opts:
|
||||||
|
opts.append(option._long_opts[0])
|
||||||
|
if len(opts) > 1:
|
||||||
|
opts.insert(1, ', ')
|
||||||
|
|
||||||
if option.takes_value(): opts.append(' %s' % option.metavar)
|
if option.takes_value(): opts.append(' %s' % option.metavar)
|
||||||
|
|
||||||
@@ -185,7 +131,7 @@ def parseOpts():
|
|||||||
general.add_option('-i', '--ignore-errors',
|
general.add_option('-i', '--ignore-errors',
|
||||||
action='store_true', dest='ignoreerrors', help='continue on download errors', default=False)
|
action='store_true', dest='ignoreerrors', help='continue on download errors', default=False)
|
||||||
general.add_option('-r', '--rate-limit',
|
general.add_option('-r', '--rate-limit',
|
||||||
dest='ratelimit', metavar='LIMIT', help='download rate limit (e.g. 50k or 44.6m)')
|
dest='ratelimit', metavar='LIMIT', help='maximum download rate (e.g. 50k or 44.6m)')
|
||||||
general.add_option('-R', '--retries',
|
general.add_option('-R', '--retries',
|
||||||
dest='retries', metavar='RETRIES', help='number of retries (default is %default)', default=10)
|
dest='retries', metavar='RETRIES', help='number of retries (default is %default)', default=10)
|
||||||
general.add_option('--buffer-size',
|
general.add_option('--buffer-size',
|
||||||
@@ -198,9 +144,15 @@ def parseOpts():
|
|||||||
help='display the current browser identification', default=False)
|
help='display the current browser identification', default=False)
|
||||||
general.add_option('--user-agent',
|
general.add_option('--user-agent',
|
||||||
dest='user_agent', help='specify a custom user agent', metavar='UA')
|
dest='user_agent', help='specify a custom user agent', metavar='UA')
|
||||||
|
general.add_option('--referer',
|
||||||
|
dest='referer', help='specify a custom referer, use if the video access is restricted to one domain',
|
||||||
|
metavar='REF', default=None)
|
||||||
general.add_option('--list-extractors',
|
general.add_option('--list-extractors',
|
||||||
action='store_true', dest='list_extractors',
|
action='store_true', dest='list_extractors',
|
||||||
help='List all supported extractors and the URLs they would handle', default=False)
|
help='List all supported extractors and the URLs they would handle', default=False)
|
||||||
|
general.add_option('--proxy', dest='proxy', default=None, help='Use the specified HTTP/HTTPS proxy', metavar='URL')
|
||||||
|
general.add_option('--no-check-certificate', action='store_true', dest='no_check_certificate', default=False, help='Suppress HTTPS certificate validation.')
|
||||||
|
general.add_option('--test', action='store_true', dest='test', default=False, help=optparse.SUPPRESS_HELP)
|
||||||
|
|
||||||
selection.add_option('--playlist-start',
|
selection.add_option('--playlist-start',
|
||||||
dest='playliststart', metavar='NUMBER', help='playlist video to start at (default is %default)', default=1)
|
dest='playliststart', metavar='NUMBER', help='playlist video to start at (default is %default)', default=1)
|
||||||
@@ -209,6 +161,12 @@ def parseOpts():
|
|||||||
selection.add_option('--match-title', dest='matchtitle', metavar='REGEX',help='download only matching titles (regex or caseless sub-string)')
|
selection.add_option('--match-title', dest='matchtitle', metavar='REGEX',help='download only matching titles (regex or caseless sub-string)')
|
||||||
selection.add_option('--reject-title', dest='rejecttitle', metavar='REGEX',help='skip download for matching titles (regex or caseless sub-string)')
|
selection.add_option('--reject-title', dest='rejecttitle', metavar='REGEX',help='skip download for matching titles (regex or caseless sub-string)')
|
||||||
selection.add_option('--max-downloads', metavar='NUMBER', dest='max_downloads', help='Abort after downloading NUMBER files', default=None)
|
selection.add_option('--max-downloads', metavar='NUMBER', dest='max_downloads', help='Abort after downloading NUMBER files', default=None)
|
||||||
|
selection.add_option('--min-filesize', metavar='SIZE', dest='min_filesize', help="Do not download any videos smaller than SIZE (e.g. 50k or 44.6m)", default=None)
|
||||||
|
selection.add_option('--max-filesize', metavar='SIZE', dest='max_filesize', help="Do not download any videos larger than SIZE (e.g. 50k or 44.6m)", default=None)
|
||||||
|
selection.add_option('--date', metavar='DATE', dest='date', help='download only videos uploaded in this date', default=None)
|
||||||
|
selection.add_option('--datebefore', metavar='DATE', dest='datebefore', help='download only videos uploaded before this date', default=None)
|
||||||
|
selection.add_option('--dateafter', metavar='DATE', dest='dateafter', help='download only videos uploaded after this date', default=None)
|
||||||
|
|
||||||
|
|
||||||
authentication.add_option('-u', '--username',
|
authentication.add_option('-u', '--username',
|
||||||
dest='username', metavar='USERNAME', help='account username')
|
dest='username', metavar='USERNAME', help='account username')
|
||||||
@@ -219,7 +177,8 @@ def parseOpts():
|
|||||||
|
|
||||||
|
|
||||||
video_format.add_option('-f', '--format',
|
video_format.add_option('-f', '--format',
|
||||||
action='store', dest='format', metavar='FORMAT', help='video format code')
|
action='store', dest='format', metavar='FORMAT',
|
||||||
|
help='video format code, specifiy the order of preference using slashes: "-f 22/17/18"')
|
||||||
video_format.add_option('--all-formats',
|
video_format.add_option('--all-formats',
|
||||||
action='store_const', dest='format', help='download all available video formats', const='all')
|
action='store_const', dest='format', help='download all available video formats', const='all')
|
||||||
video_format.add_option('--prefer-free-formats',
|
video_format.add_option('--prefer-free-formats',
|
||||||
@@ -228,13 +187,24 @@ def parseOpts():
|
|||||||
action='store', dest='format_limit', metavar='FORMAT', help='highest quality format to download')
|
action='store', dest='format_limit', metavar='FORMAT', help='highest quality format to download')
|
||||||
video_format.add_option('-F', '--list-formats',
|
video_format.add_option('-F', '--list-formats',
|
||||||
action='store_true', dest='listformats', help='list all available formats (currently youtube only)')
|
action='store_true', dest='listformats', help='list all available formats (currently youtube only)')
|
||||||
video_format.add_option('--write-srt',
|
video_format.add_option('--write-sub', '--write-srt',
|
||||||
action='store_true', dest='writesubtitles',
|
action='store_true', dest='writesubtitles',
|
||||||
help='write video closed captions to a .srt file (currently youtube only)', default=False)
|
help='write subtitle file (currently youtube only)', default=False)
|
||||||
video_format.add_option('--srt-lang',
|
video_format.add_option('--only-sub',
|
||||||
|
action='store_true', dest='skip_download',
|
||||||
|
help='[deprecated] alias of --skip-download', default=False)
|
||||||
|
video_format.add_option('--all-subs',
|
||||||
|
action='store_true', dest='allsubtitles',
|
||||||
|
help='downloads all the available subtitles of the video (currently youtube only)', default=False)
|
||||||
|
video_format.add_option('--list-subs',
|
||||||
|
action='store_true', dest='listsubtitles',
|
||||||
|
help='lists all available subtitles for the video (currently youtube only)', default=False)
|
||||||
|
video_format.add_option('--sub-format',
|
||||||
|
action='store', dest='subtitlesformat', metavar='LANG',
|
||||||
|
help='subtitle format [srt/sbv] (default=srt) (currently youtube only)', default='srt')
|
||||||
|
video_format.add_option('--sub-lang', '--srt-lang',
|
||||||
action='store', dest='subtitleslang', metavar='LANG',
|
action='store', dest='subtitleslang', metavar='LANG',
|
||||||
help='language of the closed captions to download (optional) use IETF language tags like \'en\'')
|
help='language of the subtitles to download (optional) use IETF language tags like \'en\'')
|
||||||
|
|
||||||
|
|
||||||
verbosity.add_option('-q', '--quiet',
|
verbosity.add_option('-q', '--quiet',
|
||||||
action='store_true', dest='quiet', help='activates quiet mode', default=False)
|
action='store_true', dest='quiet', help='activates quiet mode', default=False)
|
||||||
@@ -246,6 +216,8 @@ def parseOpts():
|
|||||||
action='store_true', dest='geturl', help='simulate, quiet but print URL', default=False)
|
action='store_true', dest='geturl', help='simulate, quiet but print URL', default=False)
|
||||||
verbosity.add_option('-e', '--get-title',
|
verbosity.add_option('-e', '--get-title',
|
||||||
action='store_true', dest='gettitle', help='simulate, quiet but print title', default=False)
|
action='store_true', dest='gettitle', help='simulate, quiet but print title', default=False)
|
||||||
|
verbosity.add_option('--get-id',
|
||||||
|
action='store_true', dest='getid', help='simulate, quiet but print id', default=False)
|
||||||
verbosity.add_option('--get-thumbnail',
|
verbosity.add_option('--get-thumbnail',
|
||||||
action='store_true', dest='getthumbnail',
|
action='store_true', dest='getthumbnail',
|
||||||
help='simulate, quiet but print thumbnail URL', default=False)
|
help='simulate, quiet but print thumbnail URL', default=False)
|
||||||
@@ -258,6 +230,8 @@ def parseOpts():
|
|||||||
verbosity.add_option('--get-format',
|
verbosity.add_option('--get-format',
|
||||||
action='store_true', dest='getformat',
|
action='store_true', dest='getformat',
|
||||||
help='simulate, quiet but print output format', default=False)
|
help='simulate, quiet but print output format', default=False)
|
||||||
|
verbosity.add_option('--newline',
|
||||||
|
action='store_true', dest='progress_with_newline', help='output progress bar as new lines', default=False)
|
||||||
verbosity.add_option('--no-progress',
|
verbosity.add_option('--no-progress',
|
||||||
action='store_true', dest='noprogress', help='do not print progress bar', default=False)
|
action='store_true', dest='noprogress', help='do not print progress bar', default=False)
|
||||||
verbosity.add_option('--console-title',
|
verbosity.add_option('--console-title',
|
||||||
@@ -265,19 +239,33 @@ def parseOpts():
|
|||||||
help='display progress in console titlebar', default=False)
|
help='display progress in console titlebar', default=False)
|
||||||
verbosity.add_option('-v', '--verbose',
|
verbosity.add_option('-v', '--verbose',
|
||||||
action='store_true', dest='verbose', help='print various debugging information', default=False)
|
action='store_true', dest='verbose', help='print various debugging information', default=False)
|
||||||
|
verbosity.add_option('--dump-intermediate-pages',
|
||||||
|
action='store_true', dest='dump_intermediate_pages', default=False,
|
||||||
|
help='print downloaded pages to debug problems(very verbose)')
|
||||||
|
|
||||||
filesystem.add_option('-t', '--title',
|
filesystem.add_option('-t', '--title',
|
||||||
action='store_true', dest='usetitle', help='use title in file name', default=False)
|
action='store_true', dest='usetitle', help='use title in file name (default)', default=False)
|
||||||
filesystem.add_option('--id',
|
filesystem.add_option('--id',
|
||||||
action='store_true', dest='useid', help='use video ID in file name', default=False)
|
action='store_true', dest='useid', help='use only video ID in file name', default=False)
|
||||||
filesystem.add_option('-l', '--literal',
|
filesystem.add_option('-l', '--literal',
|
||||||
action='store_true', dest='usetitle', help='[deprecated] alias of --title', default=False)
|
action='store_true', dest='usetitle', help='[deprecated] alias of --title', default=False)
|
||||||
filesystem.add_option('-A', '--auto-number',
|
filesystem.add_option('-A', '--auto-number',
|
||||||
action='store_true', dest='autonumber',
|
action='store_true', dest='autonumber',
|
||||||
help='number downloaded files starting from 00000', default=False)
|
help='number downloaded files starting from 00000', default=False)
|
||||||
filesystem.add_option('-o', '--output',
|
filesystem.add_option('-o', '--output',
|
||||||
dest='outtmpl', metavar='TEMPLATE', help='output filename template. Use %(title)s to get the title, %(uploader)s for the uploader name, %(autonumber)s to get an automatically incremented number, %(ext)s for the filename extension, %(upload_date)s for the upload date (YYYYMMDD), %(extractor)s for the provider (youtube, metacafe, etc), %(id)s for the video id and %% for a literal percent. Use - to output to stdout.')
|
dest='outtmpl', metavar='TEMPLATE',
|
||||||
|
help=('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, %(upload_date)s for the upload date (YYYYMMDD), '
|
||||||
|
'%(extractor)s for the provider (youtube, metacafe, etc), '
|
||||||
|
'%(id)s for the video id , %(playlist)s for the playlist the video is in, '
|
||||||
|
'%(playlist_index)s for the position in the playlist and %% 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\' .'))
|
||||||
|
filesystem.add_option('--autonumber-size',
|
||||||
|
dest='autonumber_size', metavar='NUMBER',
|
||||||
|
help='Specifies the number of digits in %(autonumber)s when it is present in output filename template or --autonumber option is given')
|
||||||
filesystem.add_option('--restrict-filenames',
|
filesystem.add_option('--restrict-filenames',
|
||||||
action='store_true', dest='restrictfilenames',
|
action='store_true', dest='restrictfilenames',
|
||||||
help='Restrict filenames to only ASCII characters, and avoid "&" and spaces in filenames', default=False)
|
help='Restrict filenames to only ASCII characters, and avoid "&" and spaces in filenames', default=False)
|
||||||
@@ -303,16 +291,23 @@ def parseOpts():
|
|||||||
filesystem.add_option('--write-info-json',
|
filesystem.add_option('--write-info-json',
|
||||||
action='store_true', dest='writeinfojson',
|
action='store_true', dest='writeinfojson',
|
||||||
help='write video metadata to a .info.json file', default=False)
|
help='write video metadata to a .info.json file', default=False)
|
||||||
|
filesystem.add_option('--write-thumbnail',
|
||||||
|
action='store_true', dest='writethumbnail',
|
||||||
|
help='write thumbnail image to disk', default=False)
|
||||||
|
|
||||||
|
|
||||||
postproc.add_option('-x', '--extract-audio', action='store_true', dest='extractaudio', default=False,
|
postproc.add_option('-x', '--extract-audio', action='store_true', dest='extractaudio', default=False,
|
||||||
help='convert video files to audio-only files (requires ffmpeg or avconv and ffprobe or avprobe)')
|
help='convert video files to audio-only files (requires ffmpeg or avconv and ffprobe or avprobe)')
|
||||||
postproc.add_option('--audio-format', metavar='FORMAT', dest='audioformat', default='best',
|
postproc.add_option('--audio-format', metavar='FORMAT', dest='audioformat', default='best',
|
||||||
help='"best", "aac", "vorbis", "mp3", "m4a", or "wav"; best by default')
|
help='"best", "aac", "vorbis", "mp3", "m4a", "opus", or "wav"; best by default')
|
||||||
postproc.add_option('--audio-quality', metavar='QUALITY', dest='audioquality', default='5',
|
postproc.add_option('--audio-quality', metavar='QUALITY', dest='audioquality', default='5',
|
||||||
help='ffmpeg/avconv audio quality specification, insert a value between 0 (better) and 9 (worse) for VBR or a specific bitrate like 128K (default 5)')
|
help='ffmpeg/avconv audio quality specification, insert a value between 0 (better) and 9 (worse) for VBR or a specific bitrate like 128K (default 5)')
|
||||||
|
postproc.add_option('--recode-video', metavar='FORMAT', dest='recodevideo', default=None,
|
||||||
|
help='Encode the video to another format if necessary (currently supported: mp4|flv|ogg|webm)')
|
||||||
postproc.add_option('-k', '--keep-video', action='store_true', dest='keepvideo', default=False,
|
postproc.add_option('-k', '--keep-video', action='store_true', dest='keepvideo', default=False,
|
||||||
help='keeps the video file on disk after the post-processing; the video is erased by default')
|
help='keeps the video file on disk after the post-processing; the video is erased by default')
|
||||||
|
postproc.add_option('--no-post-overwrites', action='store_true', dest='nopostoverwrites', default=False,
|
||||||
|
help='do not overwrite post-processed files; the post-processed files are overwritten by default')
|
||||||
|
|
||||||
|
|
||||||
parser.add_option_group(general)
|
parser.add_option_group(general)
|
||||||
@@ -323,75 +318,60 @@ def parseOpts():
|
|||||||
parser.add_option_group(authentication)
|
parser.add_option_group(authentication)
|
||||||
parser.add_option_group(postproc)
|
parser.add_option_group(postproc)
|
||||||
|
|
||||||
|
if overrideArguments is not None:
|
||||||
|
opts, args = parser.parse_args(overrideArguments)
|
||||||
|
if opts.verbose:
|
||||||
|
print(u'[debug] Override config: ' + repr(overrideArguments))
|
||||||
|
else:
|
||||||
xdg_config_home = os.environ.get('XDG_CONFIG_HOME')
|
xdg_config_home = os.environ.get('XDG_CONFIG_HOME')
|
||||||
if xdg_config_home:
|
if xdg_config_home:
|
||||||
userConf = os.path.join(xdg_config_home, 'youtube-dl.conf')
|
userConfFile = os.path.join(xdg_config_home, 'youtube-dl.conf')
|
||||||
else:
|
else:
|
||||||
userConf = os.path.join(os.path.expanduser('~'), '.config', 'youtube-dl.conf')
|
userConfFile = os.path.join(os.path.expanduser('~'), '.config', 'youtube-dl.conf')
|
||||||
argv = _readOptions('/etc/youtube-dl.conf') + _readOptions(userConf) + sys.argv[1:]
|
systemConf = _readOptions('/etc/youtube-dl.conf')
|
||||||
|
userConf = _readOptions(userConfFile)
|
||||||
|
commandLineConf = sys.argv[1:]
|
||||||
|
argv = systemConf + userConf + commandLineConf
|
||||||
opts, args = parser.parse_args(argv)
|
opts, args = parser.parse_args(argv)
|
||||||
|
if opts.verbose:
|
||||||
|
print(u'[debug] System config: ' + repr(systemConf))
|
||||||
|
print(u'[debug] User config: ' + repr(userConf))
|
||||||
|
print(u'[debug] Command-line args: ' + repr(commandLineConf))
|
||||||
|
|
||||||
return parser, opts, args
|
return parser, opts, args
|
||||||
|
|
||||||
def gen_extractors():
|
def _real_main(argv=None):
|
||||||
""" Return a list of an instance of every supported extractor.
|
# Compatibility fixes for Windows
|
||||||
The order does matter; the first extractor matched is the one handling the URL.
|
if sys.platform == 'win32':
|
||||||
"""
|
# https://github.com/rg3/youtube-dl/issues/820
|
||||||
return [
|
codecs.register(lambda name: codecs.lookup('utf-8') if name == 'cp65001' else None)
|
||||||
YoutubePlaylistIE(),
|
|
||||||
YoutubeChannelIE(),
|
|
||||||
YoutubeUserIE(),
|
|
||||||
YoutubeSearchIE(),
|
|
||||||
YoutubeIE(),
|
|
||||||
MetacafeIE(),
|
|
||||||
DailymotionIE(),
|
|
||||||
GoogleIE(),
|
|
||||||
GoogleSearchIE(),
|
|
||||||
PhotobucketIE(),
|
|
||||||
YahooIE(),
|
|
||||||
YahooSearchIE(),
|
|
||||||
DepositFilesIE(),
|
|
||||||
FacebookIE(),
|
|
||||||
BlipTVUserIE(),
|
|
||||||
BlipTVIE(),
|
|
||||||
VimeoIE(),
|
|
||||||
MyVideoIE(),
|
|
||||||
ComedyCentralIE(),
|
|
||||||
EscapistIE(),
|
|
||||||
CollegeHumorIE(),
|
|
||||||
XVideosIE(),
|
|
||||||
SoundcloudIE(),
|
|
||||||
InfoQIE(),
|
|
||||||
MixcloudIE(),
|
|
||||||
StanfordOpenClassroomIE(),
|
|
||||||
MTVIE(),
|
|
||||||
YoukuIE(),
|
|
||||||
XNXXIE(),
|
|
||||||
GooglePlusIE(),
|
|
||||||
ArteTvIE(),
|
|
||||||
GenericIE()
|
|
||||||
]
|
|
||||||
|
|
||||||
def _real_main():
|
parser, opts, args = parseOpts(argv)
|
||||||
parser, opts, args = parseOpts()
|
|
||||||
|
|
||||||
# Open appropriate CookieJar
|
# Open appropriate CookieJar
|
||||||
if opts.cookiefile is None:
|
if opts.cookiefile is None:
|
||||||
jar = cookielib.CookieJar()
|
jar = compat_cookiejar.CookieJar()
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
jar = cookielib.MozillaCookieJar(opts.cookiefile)
|
jar = compat_cookiejar.MozillaCookieJar(opts.cookiefile)
|
||||||
if os.path.isfile(opts.cookiefile) and os.access(opts.cookiefile, os.R_OK):
|
if os.access(opts.cookiefile, os.R_OK):
|
||||||
jar.load()
|
jar.load()
|
||||||
except (IOError, OSError), err:
|
except (IOError, OSError) as err:
|
||||||
sys.exit(u'ERROR: unable to open cookie file')
|
if opts.verbose:
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.stderr.write(u'ERROR: unable to open cookie file\n')
|
||||||
|
sys.exit(101)
|
||||||
# Set user agent
|
# Set user agent
|
||||||
if opts.user_agent is not None:
|
if opts.user_agent is not None:
|
||||||
std_headers['User-Agent'] = opts.user_agent
|
std_headers['User-Agent'] = opts.user_agent
|
||||||
|
|
||||||
|
# Set referer
|
||||||
|
if opts.referer is not None:
|
||||||
|
std_headers['Referer'] = opts.referer
|
||||||
|
|
||||||
# Dump user agent
|
# Dump user agent
|
||||||
if opts.dump_user_agent:
|
if opts.dump_user_agent:
|
||||||
print std_headers['User-Agent']
|
print(std_headers['User-Agent'])
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
# Batch file verification
|
# Batch file verification
|
||||||
@@ -408,22 +388,33 @@ def _real_main():
|
|||||||
except IOError:
|
except IOError:
|
||||||
sys.exit(u'ERROR: batch file could not be read')
|
sys.exit(u'ERROR: batch file could not be read')
|
||||||
all_urls = batchurls + args
|
all_urls = batchurls + args
|
||||||
all_urls = map(lambda url: url.strip(), all_urls)
|
all_urls = [url.strip() for url in all_urls]
|
||||||
|
|
||||||
# General configuration
|
# General configuration
|
||||||
cookie_processor = urllib2.HTTPCookieProcessor(jar)
|
cookie_processor = compat_urllib_request.HTTPCookieProcessor(jar)
|
||||||
proxy_handler = urllib2.ProxyHandler()
|
if opts.proxy is not None:
|
||||||
opener = urllib2.build_opener(proxy_handler, cookie_processor, YoutubeDLHandler())
|
if opts.proxy == '':
|
||||||
urllib2.install_opener(opener)
|
proxies = {}
|
||||||
|
else:
|
||||||
|
proxies = {'http': opts.proxy, 'https': opts.proxy}
|
||||||
|
else:
|
||||||
|
proxies = compat_urllib_request.getproxies()
|
||||||
|
# Set HTTPS proxy to HTTP one if given (https://github.com/rg3/youtube-dl/issues/805)
|
||||||
|
if 'http' in proxies and 'https' not in proxies:
|
||||||
|
proxies['https'] = proxies['http']
|
||||||
|
proxy_handler = compat_urllib_request.ProxyHandler(proxies)
|
||||||
|
https_handler = make_HTTPS_handler(opts)
|
||||||
|
opener = compat_urllib_request.build_opener(https_handler, proxy_handler, cookie_processor, YoutubeDLHandler())
|
||||||
|
compat_urllib_request.install_opener(opener)
|
||||||
socket.setdefaulttimeout(300) # 5 minutes should be enough (famous last words)
|
socket.setdefaulttimeout(300) # 5 minutes should be enough (famous last words)
|
||||||
|
|
||||||
extractors = gen_extractors()
|
extractors = gen_extractors()
|
||||||
|
|
||||||
if opts.list_extractors:
|
if opts.list_extractors:
|
||||||
for ie in extractors:
|
for ie in extractors:
|
||||||
print(ie.IE_NAME)
|
print(ie.IE_NAME + (' (CURRENTLY BROKEN)' if not ie._WORKING else ''))
|
||||||
matchedUrls = filter(lambda url: ie.suitable(url), all_urls)
|
matchedUrls = [url for url in all_urls if ie.suitable(url)]
|
||||||
all_urls = filter(lambda url: url not in matchedUrls, all_urls)
|
all_urls = [url for url in all_urls if url not in matchedUrls]
|
||||||
for mu in matchedUrls:
|
for mu in matchedUrls:
|
||||||
print(u' ' + mu)
|
print(u' ' + mu)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
@@ -444,10 +435,20 @@ def _real_main():
|
|||||||
if numeric_limit is None:
|
if numeric_limit is None:
|
||||||
parser.error(u'invalid rate limit specified')
|
parser.error(u'invalid rate limit specified')
|
||||||
opts.ratelimit = numeric_limit
|
opts.ratelimit = numeric_limit
|
||||||
|
if opts.min_filesize is not None:
|
||||||
|
numeric_limit = FileDownloader.parse_bytes(opts.min_filesize)
|
||||||
|
if numeric_limit is None:
|
||||||
|
parser.error(u'invalid min_filesize specified')
|
||||||
|
opts.min_filesize = numeric_limit
|
||||||
|
if opts.max_filesize is not None:
|
||||||
|
numeric_limit = FileDownloader.parse_bytes(opts.max_filesize)
|
||||||
|
if numeric_limit is None:
|
||||||
|
parser.error(u'invalid max_filesize specified')
|
||||||
|
opts.max_filesize = numeric_limit
|
||||||
if opts.retries is not None:
|
if opts.retries is not None:
|
||||||
try:
|
try:
|
||||||
opts.retries = long(opts.retries)
|
opts.retries = int(opts.retries)
|
||||||
except (TypeError, ValueError), err:
|
except (TypeError, ValueError) as err:
|
||||||
parser.error(u'invalid retry count specified')
|
parser.error(u'invalid retry count specified')
|
||||||
if opts.buffersize is not None:
|
if opts.buffersize is not None:
|
||||||
numeric_buffersize = FileDownloader.parse_bytes(opts.buffersize)
|
numeric_buffersize = FileDownloader.parse_bytes(opts.buffersize)
|
||||||
@@ -458,47 +459,62 @@ def _real_main():
|
|||||||
opts.playliststart = int(opts.playliststart)
|
opts.playliststart = int(opts.playliststart)
|
||||||
if opts.playliststart <= 0:
|
if opts.playliststart <= 0:
|
||||||
raise ValueError(u'Playlist start must be positive')
|
raise ValueError(u'Playlist start must be positive')
|
||||||
except (TypeError, ValueError), err:
|
except (TypeError, ValueError) as err:
|
||||||
parser.error(u'invalid playlist start number specified')
|
parser.error(u'invalid playlist start number specified')
|
||||||
try:
|
try:
|
||||||
opts.playlistend = int(opts.playlistend)
|
opts.playlistend = int(opts.playlistend)
|
||||||
if opts.playlistend != -1 and (opts.playlistend <= 0 or opts.playlistend < opts.playliststart):
|
if opts.playlistend != -1 and (opts.playlistend <= 0 or opts.playlistend < opts.playliststart):
|
||||||
raise ValueError(u'Playlist end must be greater than playlist start')
|
raise ValueError(u'Playlist end must be greater than playlist start')
|
||||||
except (TypeError, ValueError), err:
|
except (TypeError, ValueError) as err:
|
||||||
parser.error(u'invalid playlist end number specified')
|
parser.error(u'invalid playlist end number specified')
|
||||||
if opts.extractaudio:
|
if opts.extractaudio:
|
||||||
if opts.audioformat not in ['best', 'aac', 'mp3', 'vorbis', 'm4a', 'wav']:
|
if opts.audioformat not in ['best', 'aac', 'mp3', 'm4a', 'opus', 'vorbis', 'wav']:
|
||||||
parser.error(u'invalid audio format specified')
|
parser.error(u'invalid audio format specified')
|
||||||
if opts.audioquality:
|
if opts.audioquality:
|
||||||
opts.audioquality = opts.audioquality.strip('k').strip('K')
|
opts.audioquality = opts.audioquality.strip('k').strip('K')
|
||||||
if not opts.audioquality.isdigit():
|
if not opts.audioquality.isdigit():
|
||||||
parser.error(u'invalid audio quality specified')
|
parser.error(u'invalid audio quality specified')
|
||||||
|
if opts.recodevideo is not None:
|
||||||
|
if opts.recodevideo not in ['mp4', 'flv', 'webm', 'ogg']:
|
||||||
|
parser.error(u'invalid video recode format specified')
|
||||||
|
if opts.date is not None:
|
||||||
|
date = DateRange.day(opts.date)
|
||||||
|
else:
|
||||||
|
date = DateRange(opts.dateafter, opts.datebefore)
|
||||||
|
|
||||||
# File downloader
|
if sys.version_info < (3,):
|
||||||
fd = FileDownloader({
|
# In Python 2, sys.argv is a bytestring (also note http://bugs.python.org/issue2128 for Windows systems)
|
||||||
'usenetrc': opts.usenetrc,
|
if opts.outtmpl is not None:
|
||||||
'username': opts.username,
|
opts.outtmpl = opts.outtmpl.decode(preferredencoding())
|
||||||
'password': opts.password,
|
outtmpl =((opts.outtmpl is not None and opts.outtmpl)
|
||||||
'quiet': (opts.quiet or opts.geturl or opts.gettitle or opts.getthumbnail or opts.getdescription or opts.getfilename or opts.getformat),
|
|
||||||
'forceurl': opts.geturl,
|
|
||||||
'forcetitle': opts.gettitle,
|
|
||||||
'forcethumbnail': opts.getthumbnail,
|
|
||||||
'forcedescription': opts.getdescription,
|
|
||||||
'forcefilename': opts.getfilename,
|
|
||||||
'forceformat': opts.getformat,
|
|
||||||
'simulate': opts.simulate,
|
|
||||||
'skip_download': (opts.skip_download or opts.simulate or opts.geturl or opts.gettitle or opts.getthumbnail or opts.getdescription or opts.getfilename or opts.getformat),
|
|
||||||
'format': opts.format,
|
|
||||||
'format_limit': opts.format_limit,
|
|
||||||
'listformats': opts.listformats,
|
|
||||||
'outtmpl': ((opts.outtmpl is not None and opts.outtmpl.decode(preferredencoding()))
|
|
||||||
or (opts.format == '-1' and opts.usetitle and u'%(title)s-%(id)s-%(format)s.%(ext)s')
|
or (opts.format == '-1' and opts.usetitle and u'%(title)s-%(id)s-%(format)s.%(ext)s')
|
||||||
or (opts.format == '-1' and u'%(id)s-%(format)s.%(ext)s')
|
or (opts.format == '-1' and u'%(id)s-%(format)s.%(ext)s')
|
||||||
or (opts.usetitle and opts.autonumber and u'%(autonumber)s-%(title)s-%(id)s.%(ext)s')
|
or (opts.usetitle and opts.autonumber and u'%(autonumber)s-%(title)s-%(id)s.%(ext)s')
|
||||||
or (opts.usetitle and u'%(title)s-%(id)s.%(ext)s')
|
or (opts.usetitle and u'%(title)s-%(id)s.%(ext)s')
|
||||||
or (opts.useid and u'%(id)s.%(ext)s')
|
or (opts.useid and u'%(id)s.%(ext)s')
|
||||||
or (opts.autonumber and u'%(autonumber)s-%(id)s.%(ext)s')
|
or (opts.autonumber and u'%(autonumber)s-%(id)s.%(ext)s')
|
||||||
or u'%(id)s.%(ext)s'),
|
or u'%(title)s-%(id)s.%(ext)s')
|
||||||
|
|
||||||
|
# File downloader
|
||||||
|
fd = FileDownloader({
|
||||||
|
'usenetrc': opts.usenetrc,
|
||||||
|
'username': opts.username,
|
||||||
|
'password': opts.password,
|
||||||
|
'quiet': (opts.quiet or opts.geturl or opts.gettitle or opts.getid or opts.getthumbnail or opts.getdescription or opts.getfilename or opts.getformat),
|
||||||
|
'forceurl': opts.geturl,
|
||||||
|
'forcetitle': opts.gettitle,
|
||||||
|
'forceid': opts.getid,
|
||||||
|
'forcethumbnail': opts.getthumbnail,
|
||||||
|
'forcedescription': opts.getdescription,
|
||||||
|
'forcefilename': opts.getfilename,
|
||||||
|
'forceformat': opts.getformat,
|
||||||
|
'simulate': opts.simulate,
|
||||||
|
'skip_download': (opts.skip_download or opts.simulate or opts.geturl or opts.gettitle or opts.getid or opts.getthumbnail or opts.getdescription or opts.getfilename or opts.getformat),
|
||||||
|
'format': opts.format,
|
||||||
|
'format_limit': opts.format_limit,
|
||||||
|
'listformats': opts.listformats,
|
||||||
|
'outtmpl': outtmpl,
|
||||||
|
'autonumber_size': opts.autonumber_size,
|
||||||
'restrictfilenames': opts.restrictfilenames,
|
'restrictfilenames': opts.restrictfilenames,
|
||||||
'ignoreerrors': opts.ignoreerrors,
|
'ignoreerrors': opts.ignoreerrors,
|
||||||
'ratelimit': opts.ratelimit,
|
'ratelimit': opts.ratelimit,
|
||||||
@@ -508,6 +524,7 @@ def _real_main():
|
|||||||
'noresizebuffer': opts.noresizebuffer,
|
'noresizebuffer': opts.noresizebuffer,
|
||||||
'continuedl': opts.continue_dl,
|
'continuedl': opts.continue_dl,
|
||||||
'noprogress': opts.noprogress,
|
'noprogress': opts.noprogress,
|
||||||
|
'progress_with_newline': opts.progress_with_newline,
|
||||||
'playliststart': opts.playliststart,
|
'playliststart': opts.playliststart,
|
||||||
'playlistend': opts.playlistend,
|
'playlistend': opts.playlistend,
|
||||||
'logtostderr': opts.outtmpl == '-',
|
'logtostderr': opts.outtmpl == '-',
|
||||||
@@ -516,16 +533,37 @@ def _real_main():
|
|||||||
'updatetime': opts.updatetime,
|
'updatetime': opts.updatetime,
|
||||||
'writedescription': opts.writedescription,
|
'writedescription': opts.writedescription,
|
||||||
'writeinfojson': opts.writeinfojson,
|
'writeinfojson': opts.writeinfojson,
|
||||||
|
'writethumbnail': opts.writethumbnail,
|
||||||
'writesubtitles': opts.writesubtitles,
|
'writesubtitles': opts.writesubtitles,
|
||||||
|
'allsubtitles': opts.allsubtitles,
|
||||||
|
'listsubtitles': opts.listsubtitles,
|
||||||
|
'subtitlesformat': opts.subtitlesformat,
|
||||||
'subtitleslang': opts.subtitleslang,
|
'subtitleslang': opts.subtitleslang,
|
||||||
'matchtitle': opts.matchtitle,
|
'matchtitle': decodeOption(opts.matchtitle),
|
||||||
'rejecttitle': opts.rejecttitle,
|
'rejecttitle': decodeOption(opts.rejecttitle),
|
||||||
'max_downloads': opts.max_downloads,
|
'max_downloads': opts.max_downloads,
|
||||||
'prefer_free_formats': opts.prefer_free_formats,
|
'prefer_free_formats': opts.prefer_free_formats,
|
||||||
'verbose': opts.verbose,
|
'verbose': opts.verbose,
|
||||||
|
'dump_intermediate_pages': opts.dump_intermediate_pages,
|
||||||
|
'test': opts.test,
|
||||||
|
'keepvideo': opts.keepvideo,
|
||||||
|
'min_filesize': opts.min_filesize,
|
||||||
|
'max_filesize': opts.max_filesize,
|
||||||
|
'daterange': date,
|
||||||
})
|
})
|
||||||
|
|
||||||
if opts.verbose:
|
if opts.verbose:
|
||||||
|
fd.to_screen(u'[debug] youtube-dl version ' + __version__)
|
||||||
|
try:
|
||||||
|
sp = subprocess.Popen(['git', 'rev-parse', '--short', 'HEAD'], stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||||
|
cwd=os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
out, err = sp.communicate()
|
||||||
|
out = out.decode().strip()
|
||||||
|
if re.match('[0-9a-f]+', out):
|
||||||
|
fd.to_screen(u'[debug] Git HEAD: ' + out)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
fd.to_screen(u'[debug] Python version %s - %s' %(platform.python_version(), platform.platform()))
|
||||||
fd.to_screen(u'[debug] Proxy map: ' + str(proxy_handler.proxies))
|
fd.to_screen(u'[debug] Proxy map: ' + str(proxy_handler.proxies))
|
||||||
|
|
||||||
for extractor in extractors:
|
for extractor in extractors:
|
||||||
@@ -533,11 +571,13 @@ def _real_main():
|
|||||||
|
|
||||||
# PostProcessors
|
# PostProcessors
|
||||||
if opts.extractaudio:
|
if opts.extractaudio:
|
||||||
fd.add_post_processor(FFmpegExtractAudioPP(preferredcodec=opts.audioformat, preferredquality=opts.audioquality, keepvideo=opts.keepvideo))
|
fd.add_post_processor(FFmpegExtractAudioPP(preferredcodec=opts.audioformat, preferredquality=opts.audioquality, nopostoverwrites=opts.nopostoverwrites))
|
||||||
|
if opts.recodevideo:
|
||||||
|
fd.add_post_processor(FFmpegVideoConvertor(preferedformat=opts.recodevideo))
|
||||||
|
|
||||||
# Update version
|
# Update version
|
||||||
if opts.update_self:
|
if opts.update_self:
|
||||||
updateSelf(fd, sys.argv[0])
|
update_self(fd.to_screen, opts.verbose, sys.argv[0])
|
||||||
|
|
||||||
# Maybe do nothing
|
# Maybe do nothing
|
||||||
if len(all_urls) < 1:
|
if len(all_urls) < 1:
|
||||||
@@ -556,14 +596,14 @@ def _real_main():
|
|||||||
if opts.cookiefile is not None:
|
if opts.cookiefile is not None:
|
||||||
try:
|
try:
|
||||||
jar.save()
|
jar.save()
|
||||||
except (IOError, OSError), err:
|
except (IOError, OSError) as err:
|
||||||
sys.exit(u'ERROR: unable to save cookie jar')
|
sys.exit(u'ERROR: unable to save cookie jar')
|
||||||
|
|
||||||
sys.exit(retcode)
|
sys.exit(retcode)
|
||||||
|
|
||||||
def main():
|
def main(argv=None):
|
||||||
try:
|
try:
|
||||||
_real_main()
|
_real_main(argv)
|
||||||
except DownloadError:
|
except DownloadError:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except SameFileError:
|
except SameFileError:
|
||||||
|
@@ -1,7 +1,18 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import __init__
|
# Execute with
|
||||||
|
# $ python youtube_dl/__main__.py (2.6+)
|
||||||
|
# $ python -m youtube_dl (2.7+)
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if __package__ is None and not hasattr(sys, "frozen"):
|
||||||
|
# direct call of __main__.py
|
||||||
|
import os.path
|
||||||
|
path = os.path.realpath(os.path.abspath(__file__))
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(path)))
|
||||||
|
|
||||||
|
import youtube_dl
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
__init__.main()
|
youtube_dl.main()
|
||||||
|
172
youtube_dl/update.py
Normal file
172
youtube_dl/update.py
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import json
|
||||||
|
import traceback
|
||||||
|
import hashlib
|
||||||
|
from zipimport import zipimporter
|
||||||
|
|
||||||
|
from .utils import *
|
||||||
|
from .version import __version__
|
||||||
|
|
||||||
|
def rsa_verify(message, signature, key):
|
||||||
|
from struct import pack
|
||||||
|
from hashlib import sha256
|
||||||
|
from sys import version_info
|
||||||
|
def b(x):
|
||||||
|
if version_info[0] == 2: return x
|
||||||
|
else: return x.encode('latin1')
|
||||||
|
assert(type(message) == type(b('')))
|
||||||
|
block_size = 0
|
||||||
|
n = key[0]
|
||||||
|
while n:
|
||||||
|
block_size += 1
|
||||||
|
n >>= 8
|
||||||
|
signature = pow(int(signature, 16), key[1], key[0])
|
||||||
|
raw_bytes = []
|
||||||
|
while signature:
|
||||||
|
raw_bytes.insert(0, pack("B", signature & 0xFF))
|
||||||
|
signature >>= 8
|
||||||
|
signature = (block_size - len(raw_bytes)) * b('\x00') + b('').join(raw_bytes)
|
||||||
|
if signature[0:2] != b('\x00\x01'): return False
|
||||||
|
signature = signature[2:]
|
||||||
|
if not b('\x00') in signature: return False
|
||||||
|
signature = signature[signature.index(b('\x00'))+1:]
|
||||||
|
if not signature.startswith(b('\x30\x31\x30\x0D\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20')): return False
|
||||||
|
signature = signature[19:]
|
||||||
|
if signature != sha256(message).digest(): return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def update_self(to_screen, verbose, filename):
|
||||||
|
"""Update the program file with the latest version from the repository"""
|
||||||
|
|
||||||
|
UPDATE_URL = "http://rg3.github.io/youtube-dl/update/"
|
||||||
|
VERSION_URL = UPDATE_URL + 'LATEST_VERSION'
|
||||||
|
JSON_URL = UPDATE_URL + 'versions.json'
|
||||||
|
UPDATES_RSA_KEY = (0x9d60ee4d8f805312fdb15a62f87b95bd66177b91df176765d13514a0f1754bcd2057295c5b6f1d35daa6742c3ffc9a82d3e118861c207995a8031e151d863c9927e304576bc80692bc8e094896fcf11b66f3e29e04e3a71e9a11558558acea1840aec37fc396fb6b65dc81a1c4144e03bd1c011de62e3f1357b327d08426fe93, 65537)
|
||||||
|
|
||||||
|
|
||||||
|
if not isinstance(globals().get('__loader__'), zipimporter) and not hasattr(sys, "frozen"):
|
||||||
|
to_screen(u'It looks like you installed youtube-dl with pip, setup.py or a tarball. Please use that to update.')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if there is a new version
|
||||||
|
try:
|
||||||
|
newversion = compat_urllib_request.urlopen(VERSION_URL).read().decode('utf-8').strip()
|
||||||
|
except:
|
||||||
|
if verbose: to_screen(compat_str(traceback.format_exc()))
|
||||||
|
to_screen(u'ERROR: can\'t find the current version. Please try again later.')
|
||||||
|
return
|
||||||
|
if newversion == __version__:
|
||||||
|
to_screen(u'youtube-dl is up-to-date (' + __version__ + ')')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Download and check versions info
|
||||||
|
try:
|
||||||
|
versions_info = compat_urllib_request.urlopen(JSON_URL).read().decode('utf-8')
|
||||||
|
versions_info = json.loads(versions_info)
|
||||||
|
except:
|
||||||
|
if verbose: to_screen(compat_str(traceback.format_exc()))
|
||||||
|
to_screen(u'ERROR: can\'t obtain versions info. Please try again later.')
|
||||||
|
return
|
||||||
|
if not 'signature' in versions_info:
|
||||||
|
to_screen(u'ERROR: the versions file is not signed or corrupted. Aborting.')
|
||||||
|
return
|
||||||
|
signature = versions_info['signature']
|
||||||
|
del versions_info['signature']
|
||||||
|
if not rsa_verify(json.dumps(versions_info, sort_keys=True).encode('utf-8'), signature, UPDATES_RSA_KEY):
|
||||||
|
to_screen(u'ERROR: the versions file signature is invalid. Aborting.')
|
||||||
|
return
|
||||||
|
|
||||||
|
to_screen(u'Updating to version ' + versions_info['latest'] + '...')
|
||||||
|
version = versions_info['versions'][versions_info['latest']]
|
||||||
|
|
||||||
|
print_notes(to_screen, versions_info['versions'])
|
||||||
|
|
||||||
|
if not os.access(filename, os.W_OK):
|
||||||
|
to_screen(u'ERROR: no write permissions on %s' % filename)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Py2EXE
|
||||||
|
if hasattr(sys, "frozen"):
|
||||||
|
exe = os.path.abspath(filename)
|
||||||
|
directory = os.path.dirname(exe)
|
||||||
|
if not os.access(directory, os.W_OK):
|
||||||
|
to_screen(u'ERROR: no write permissions on %s' % directory)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
urlh = compat_urllib_request.urlopen(version['exe'][0])
|
||||||
|
newcontent = urlh.read()
|
||||||
|
urlh.close()
|
||||||
|
except (IOError, OSError) as err:
|
||||||
|
if verbose: to_screen(compat_str(traceback.format_exc()))
|
||||||
|
to_screen(u'ERROR: unable to download latest version')
|
||||||
|
return
|
||||||
|
|
||||||
|
newcontent_hash = hashlib.sha256(newcontent).hexdigest()
|
||||||
|
if newcontent_hash != version['exe'][1]:
|
||||||
|
to_screen(u'ERROR: the downloaded file hash does not match. Aborting.')
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(exe + '.new', 'wb') as outf:
|
||||||
|
outf.write(newcontent)
|
||||||
|
except (IOError, OSError) as err:
|
||||||
|
if verbose: to_screen(compat_str(traceback.format_exc()))
|
||||||
|
to_screen(u'ERROR: unable to write the new version')
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
bat = os.path.join(directory, 'youtube-dl-updater.bat')
|
||||||
|
b = open(bat, 'w')
|
||||||
|
b.write("""
|
||||||
|
echo Updating youtube-dl...
|
||||||
|
ping 127.0.0.1 -n 5 -w 1000 > NUL
|
||||||
|
move /Y "%s.new" "%s"
|
||||||
|
del "%s"
|
||||||
|
\n""" %(exe, exe, bat))
|
||||||
|
b.close()
|
||||||
|
|
||||||
|
os.startfile(bat)
|
||||||
|
except (IOError, OSError) as err:
|
||||||
|
if verbose: to_screen(compat_str(traceback.format_exc()))
|
||||||
|
to_screen(u'ERROR: unable to overwrite current version')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Zip unix package
|
||||||
|
elif isinstance(globals().get('__loader__'), zipimporter):
|
||||||
|
try:
|
||||||
|
urlh = compat_urllib_request.urlopen(version['bin'][0])
|
||||||
|
newcontent = urlh.read()
|
||||||
|
urlh.close()
|
||||||
|
except (IOError, OSError) as err:
|
||||||
|
if verbose: to_screen(compat_str(traceback.format_exc()))
|
||||||
|
to_screen(u'ERROR: unable to download latest version')
|
||||||
|
return
|
||||||
|
|
||||||
|
newcontent_hash = hashlib.sha256(newcontent).hexdigest()
|
||||||
|
if newcontent_hash != version['bin'][1]:
|
||||||
|
to_screen(u'ERROR: the downloaded file hash does not match. Aborting.')
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(filename, 'wb') as outf:
|
||||||
|
outf.write(newcontent)
|
||||||
|
except (IOError, OSError) as err:
|
||||||
|
if verbose: to_screen(compat_str(traceback.format_exc()))
|
||||||
|
to_screen(u'ERROR: unable to overwrite current version')
|
||||||
|
return
|
||||||
|
|
||||||
|
to_screen(u'Updated youtube-dl. Restart youtube-dl to use the new version.')
|
||||||
|
|
||||||
|
def get_notes(versions, fromVersion):
|
||||||
|
notes = []
|
||||||
|
for v,vdata in sorted(versions.items()):
|
||||||
|
if v > fromVersion:
|
||||||
|
notes.extend(vdata.get('notes', []))
|
||||||
|
return notes
|
||||||
|
|
||||||
|
def print_notes(to_screen, versions, fromVersion=__version__):
|
||||||
|
notes = get_notes(versions, fromVersion)
|
||||||
|
if notes:
|
||||||
|
to_screen(u'PLEASE NOTE:')
|
||||||
|
for note in notes:
|
||||||
|
to_screen(note)
|
@@ -1,22 +1,161 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import errno
|
||||||
import gzip
|
import gzip
|
||||||
import htmlentitydefs
|
import io
|
||||||
import HTMLParser
|
import json
|
||||||
import locale
|
import locale
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import traceback
|
||||||
import zlib
|
import zlib
|
||||||
import urllib2
|
|
||||||
import email.utils
|
import email.utils
|
||||||
import json
|
import socket
|
||||||
|
import datetime
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import cStringIO as StringIO
|
import urllib.request as compat_urllib_request
|
||||||
|
except ImportError: # Python 2
|
||||||
|
import urllib2 as compat_urllib_request
|
||||||
|
|
||||||
|
try:
|
||||||
|
import urllib.error as compat_urllib_error
|
||||||
|
except ImportError: # Python 2
|
||||||
|
import urllib2 as compat_urllib_error
|
||||||
|
|
||||||
|
try:
|
||||||
|
import urllib.parse as compat_urllib_parse
|
||||||
|
except ImportError: # Python 2
|
||||||
|
import urllib as compat_urllib_parse
|
||||||
|
|
||||||
|
try:
|
||||||
|
from urllib.parse import urlparse as compat_urllib_parse_urlparse
|
||||||
|
except ImportError: # Python 2
|
||||||
|
from urlparse import urlparse as compat_urllib_parse_urlparse
|
||||||
|
|
||||||
|
try:
|
||||||
|
import http.cookiejar as compat_cookiejar
|
||||||
|
except ImportError: # Python 2
|
||||||
|
import cookielib as compat_cookiejar
|
||||||
|
|
||||||
|
try:
|
||||||
|
import html.entities as compat_html_entities
|
||||||
|
except ImportError: # Python 2
|
||||||
|
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:
|
||||||
|
import http.client as compat_http_client
|
||||||
|
except ImportError: # Python 2
|
||||||
|
import httplib as compat_http_client
|
||||||
|
|
||||||
|
try:
|
||||||
|
from subprocess import DEVNULL
|
||||||
|
compat_subprocess_get_DEVNULL = lambda: DEVNULL
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import StringIO
|
compat_subprocess_get_DEVNULL = lambda: open(os.path.devnull, 'w')
|
||||||
|
|
||||||
|
try:
|
||||||
|
from urllib.parse import parse_qs as compat_parse_qs
|
||||||
|
except ImportError: # Python 2
|
||||||
|
# HACK: The following is the correct parse_qs implementation from cpython 3's stdlib.
|
||||||
|
# Python 2's version is apparently totally broken
|
||||||
|
def _unquote(string, encoding='utf-8', errors='replace'):
|
||||||
|
if string == '':
|
||||||
|
return string
|
||||||
|
res = string.split('%')
|
||||||
|
if len(res) == 1:
|
||||||
|
return string
|
||||||
|
if encoding is None:
|
||||||
|
encoding = 'utf-8'
|
||||||
|
if errors is None:
|
||||||
|
errors = 'replace'
|
||||||
|
# pct_sequence: contiguous sequence of percent-encoded bytes, decoded
|
||||||
|
pct_sequence = b''
|
||||||
|
string = res[0]
|
||||||
|
for item in res[1:]:
|
||||||
|
try:
|
||||||
|
if not item:
|
||||||
|
raise ValueError
|
||||||
|
pct_sequence += item[:2].decode('hex')
|
||||||
|
rest = item[2:]
|
||||||
|
if not rest:
|
||||||
|
# This segment was just a single percent-encoded character.
|
||||||
|
# May be part of a sequence of code units, so delay decoding.
|
||||||
|
# (Stored in pct_sequence).
|
||||||
|
continue
|
||||||
|
except ValueError:
|
||||||
|
rest = '%' + item
|
||||||
|
# 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
|
||||||
|
|
||||||
|
def _parse_qsl(qs, keep_blank_values=False, strict_parsing=False,
|
||||||
|
encoding='utf-8', errors='replace'):
|
||||||
|
qs, _coerce_result = qs, unicode
|
||||||
|
pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
|
||||||
|
r = []
|
||||||
|
for name_value in pairs:
|
||||||
|
if not name_value and not strict_parsing:
|
||||||
|
continue
|
||||||
|
nv = name_value.split('=', 1)
|
||||||
|
if len(nv) != 2:
|
||||||
|
if strict_parsing:
|
||||||
|
raise ValueError("bad query field: %r" % (name_value,))
|
||||||
|
# Handle case of a control-name with no equal sign
|
||||||
|
if keep_blank_values:
|
||||||
|
nv.append('')
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
if len(nv[1]) or keep_blank_values:
|
||||||
|
name = nv[0].replace('+', ' ')
|
||||||
|
name = _unquote(name, encoding=encoding, errors=errors)
|
||||||
|
name = _coerce_result(name)
|
||||||
|
value = nv[1].replace('+', ' ')
|
||||||
|
value = _unquote(value, encoding=encoding, errors=errors)
|
||||||
|
value = _coerce_result(value)
|
||||||
|
r.append((name, value))
|
||||||
|
return r
|
||||||
|
|
||||||
|
def compat_parse_qs(qs, keep_blank_values=False, strict_parsing=False,
|
||||||
|
encoding='utf-8', errors='replace'):
|
||||||
|
parsed_result = {}
|
||||||
|
pairs = _parse_qsl(qs, keep_blank_values, strict_parsing,
|
||||||
|
encoding=encoding, errors=errors)
|
||||||
|
for name, value in pairs:
|
||||||
|
if name in parsed_result:
|
||||||
|
parsed_result[name].append(value)
|
||||||
|
else:
|
||||||
|
parsed_result[name] = [value]
|
||||||
|
return parsed_result
|
||||||
|
|
||||||
|
try:
|
||||||
|
compat_str = unicode # Python 2
|
||||||
|
except NameError:
|
||||||
|
compat_str = str
|
||||||
|
|
||||||
|
try:
|
||||||
|
compat_chr = unichr # Python 2
|
||||||
|
except NameError:
|
||||||
|
compat_chr = chr
|
||||||
|
|
||||||
|
def compat_ord(c):
|
||||||
|
if type(c) is int: return c
|
||||||
|
else: return ord(c)
|
||||||
|
|
||||||
|
# This is not clearly defined otherwise
|
||||||
|
compiled_regex_type = type(re.compile(''))
|
||||||
|
|
||||||
std_headers = {
|
std_headers = {
|
||||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0',
|
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0',
|
||||||
@@ -26,30 +165,41 @@ std_headers = {
|
|||||||
'Accept-Language': 'en-us,en;q=0.5',
|
'Accept-Language': 'en-us,en;q=0.5',
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
|
||||||
compat_str = unicode # Python 2
|
|
||||||
except NameError:
|
|
||||||
compat_str = str
|
|
||||||
|
|
||||||
def preferredencoding():
|
def preferredencoding():
|
||||||
"""Get preferred encoding.
|
"""Get preferred encoding.
|
||||||
|
|
||||||
Returns the best encoding scheme for the system, based on
|
Returns the best encoding scheme for the system, based on
|
||||||
locale.getpreferredencoding() and some further tweaks.
|
locale.getpreferredencoding() and some further tweaks.
|
||||||
"""
|
"""
|
||||||
def yield_preferredencoding():
|
|
||||||
try:
|
try:
|
||||||
pref = locale.getpreferredencoding()
|
pref = locale.getpreferredencoding()
|
||||||
u'TEST'.encode(pref)
|
u'TEST'.encode(pref)
|
||||||
except:
|
except:
|
||||||
pref = 'UTF-8'
|
pref = 'UTF-8'
|
||||||
while True:
|
|
||||||
yield pref
|
|
||||||
return yield_preferredencoding().next()
|
|
||||||
|
|
||||||
|
return pref
|
||||||
|
|
||||||
|
if sys.version_info < (3,0):
|
||||||
|
def compat_print(s):
|
||||||
|
print(s.encode(preferredencoding(), 'xmlcharrefreplace'))
|
||||||
|
else:
|
||||||
|
def compat_print(s):
|
||||||
|
assert type(s) == type(u'')
|
||||||
|
print(s)
|
||||||
|
|
||||||
|
# In Python 2.x, json.dump expects a bytestream.
|
||||||
|
# In Python 3.x, it writes to a character stream
|
||||||
|
if sys.version_info < (3,0):
|
||||||
|
def write_json_file(obj, fn):
|
||||||
|
with open(fn, 'wb') as f:
|
||||||
|
json.dump(obj, f)
|
||||||
|
else:
|
||||||
|
def write_json_file(obj, fn):
|
||||||
|
with open(fn, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(obj, f)
|
||||||
|
|
||||||
def htmlentity_transform(matchobj):
|
def htmlentity_transform(matchobj):
|
||||||
"""Transforms an HTML entity to a Unicode character.
|
"""Transforms an HTML entity to a character.
|
||||||
|
|
||||||
This function receives a match object and is intended to be used with
|
This function receives a match object and is intended to be used with
|
||||||
the re.sub() function.
|
the re.sub() function.
|
||||||
@@ -57,11 +207,10 @@ def htmlentity_transform(matchobj):
|
|||||||
entity = matchobj.group(1)
|
entity = matchobj.group(1)
|
||||||
|
|
||||||
# Known non-numeric HTML entity
|
# Known non-numeric HTML entity
|
||||||
if entity in htmlentitydefs.name2codepoint:
|
if entity in compat_html_entities.name2codepoint:
|
||||||
return unichr(htmlentitydefs.name2codepoint[entity])
|
return compat_chr(compat_html_entities.name2codepoint[entity])
|
||||||
|
|
||||||
# Unicode character
|
mobj = re.match(u'(?u)#(x?\\d+)', entity)
|
||||||
mobj = re.match(ur'(?u)#(x?\d+)', entity)
|
|
||||||
if mobj is not None:
|
if mobj is not None:
|
||||||
numstr = mobj.group(1)
|
numstr = mobj.group(1)
|
||||||
if numstr.startswith(u'x'):
|
if numstr.startswith(u'x'):
|
||||||
@@ -69,27 +218,28 @@ def htmlentity_transform(matchobj):
|
|||||||
numstr = u'0%s' % numstr
|
numstr = u'0%s' % numstr
|
||||||
else:
|
else:
|
||||||
base = 10
|
base = 10
|
||||||
return unichr(long(numstr, base))
|
return compat_chr(int(numstr, base))
|
||||||
|
|
||||||
# Unknown entity in name, return its literal representation
|
# Unknown entity in name, return its literal representation
|
||||||
return (u'&%s;' % entity)
|
return (u'&%s;' % entity)
|
||||||
|
|
||||||
HTMLParser.locatestarttagend = re.compile(r"""<[a-zA-Z][-.a-zA-Z0-9:_]*(?:\s+(?:(?<=['"\s])[^\s/>][^\s/=>]*(?:\s*=+\s*(?:'[^']*'|"[^"]*"|(?!['"])[^>\s]*))?\s*)*)?\s*""", re.VERBOSE) # backport bugfix
|
compat_html_parser.locatestarttagend = re.compile(r"""<[a-zA-Z][-.a-zA-Z0-9:_]*(?:\s+(?:(?<=['"\s])[^\s/>][^\s/=>]*(?:\s*=+\s*(?:'[^']*'|"[^"]*"|(?!['"])[^>\s]*))?\s*)*)?\s*""", re.VERBOSE) # backport bugfix
|
||||||
class IDParser(HTMLParser.HTMLParser):
|
class AttrParser(compat_html_parser.HTMLParser):
|
||||||
"""Modified HTMLParser that isolates a tag with the specified id"""
|
"""Modified HTMLParser that isolates a tag with the specified attribute"""
|
||||||
def __init__(self, id):
|
def __init__(self, attribute, value):
|
||||||
self.id = id
|
self.attribute = attribute
|
||||||
|
self.value = value
|
||||||
self.result = None
|
self.result = None
|
||||||
self.started = False
|
self.started = False
|
||||||
self.depth = {}
|
self.depth = {}
|
||||||
self.html = None
|
self.html = None
|
||||||
self.watch_startpos = False
|
self.watch_startpos = False
|
||||||
self.error_count = 0
|
self.error_count = 0
|
||||||
HTMLParser.HTMLParser.__init__(self)
|
compat_html_parser.HTMLParser.__init__(self)
|
||||||
|
|
||||||
def error(self, message):
|
def error(self, message):
|
||||||
if self.error_count > 10 or self.started:
|
if self.error_count > 10 or self.started:
|
||||||
raise HTMLParser.HTMLParseError(message, self.getpos())
|
raise compat_html_parser.HTMLParseError(message, self.getpos())
|
||||||
self.rawdata = '\n'.join(self.html.split('\n')[self.getpos()[0]:]) # skip one line
|
self.rawdata = '\n'.join(self.html.split('\n')[self.getpos()[0]:]) # skip one line
|
||||||
self.error_count += 1
|
self.error_count += 1
|
||||||
self.goahead(1)
|
self.goahead(1)
|
||||||
@@ -103,7 +253,7 @@ class IDParser(HTMLParser.HTMLParser):
|
|||||||
attrs = dict(attrs)
|
attrs = dict(attrs)
|
||||||
if self.started:
|
if self.started:
|
||||||
self.find_startpos(None)
|
self.find_startpos(None)
|
||||||
if 'id' in attrs and attrs['id'] == self.id:
|
if self.attribute in attrs and attrs[self.attribute] == self.value:
|
||||||
self.result = [tag]
|
self.result = [tag]
|
||||||
self.started = True
|
self.started = True
|
||||||
self.watch_startpos = True
|
self.watch_startpos = True
|
||||||
@@ -128,8 +278,10 @@ class IDParser(HTMLParser.HTMLParser):
|
|||||||
handle_decl = handle_pi = unknown_decl = find_startpos
|
handle_decl = handle_pi = unknown_decl = find_startpos
|
||||||
|
|
||||||
def get_result(self):
|
def get_result(self):
|
||||||
if self.result == None: return None
|
if self.result is None:
|
||||||
if len(self.result) != 3: return None
|
return None
|
||||||
|
if len(self.result) != 3:
|
||||||
|
return None
|
||||||
lines = self.html.split('\n')
|
lines = self.html.split('\n')
|
||||||
lines = lines[self.result[1][0]-1:self.result[2][0]]
|
lines = lines[self.result[1][0]-1:self.result[2][0]]
|
||||||
lines[0] = lines[0][self.result[1][1]:]
|
lines[0] = lines[0][self.result[1][1]:]
|
||||||
@@ -137,13 +289,23 @@ class IDParser(HTMLParser.HTMLParser):
|
|||||||
lines[-1] = lines[-1][:self.result[2][1]-self.result[1][1]]
|
lines[-1] = lines[-1][:self.result[2][1]-self.result[1][1]]
|
||||||
lines[-1] = lines[-1][:self.result[2][1]]
|
lines[-1] = lines[-1][:self.result[2][1]]
|
||||||
return '\n'.join(lines).strip()
|
return '\n'.join(lines).strip()
|
||||||
|
# Hack for https://github.com/rg3/youtube-dl/issues/662
|
||||||
|
if sys.version_info < (2, 7, 3):
|
||||||
|
AttrParser.parse_endtag = (lambda self, i:
|
||||||
|
i + len("</scr'+'ipt>")
|
||||||
|
if self.rawdata[i:].startswith("</scr'+'ipt>")
|
||||||
|
else compat_html_parser.HTMLParser.parse_endtag(self, i))
|
||||||
|
|
||||||
def get_element_by_id(id, html):
|
def get_element_by_id(id, html):
|
||||||
"""Return the content of the tag with the specified id in the passed HTML document"""
|
"""Return the content of the tag with the specified ID in the passed HTML document"""
|
||||||
parser = IDParser(id)
|
return get_element_by_attribute("id", id, html)
|
||||||
|
|
||||||
|
def get_element_by_attribute(attribute, value, html):
|
||||||
|
"""Return the content of the tag with the specified attribute in the passed HTML document"""
|
||||||
|
parser = AttrParser(attribute, value)
|
||||||
try:
|
try:
|
||||||
parser.loads(html)
|
parser.loads(html)
|
||||||
except HTMLParser.HTMLParseError:
|
except compat_html_parser.HTMLParseError:
|
||||||
pass
|
pass
|
||||||
return parser.get_result()
|
return parser.get_result()
|
||||||
|
|
||||||
@@ -152,12 +314,13 @@ def clean_html(html):
|
|||||||
"""Clean an HTML snippet into a readable string"""
|
"""Clean an HTML snippet into a readable string"""
|
||||||
# Newline vs <br />
|
# Newline vs <br />
|
||||||
html = html.replace('\n', ' ')
|
html = html.replace('\n', ' ')
|
||||||
html = re.sub('\s*<\s*br\s*/?\s*>\s*', '\n', html)
|
html = re.sub(r'\s*<\s*br\s*/?\s*>\s*', '\n', html)
|
||||||
|
html = re.sub(r'<\s*/\s*p\s*>\s*<\s*p[^>]*>', '\n', html)
|
||||||
# Strip html tags
|
# Strip html tags
|
||||||
html = re.sub('<.*?>', '', html)
|
html = re.sub('<.*?>', '', html)
|
||||||
# Replace html entities
|
# Replace html entities
|
||||||
html = unescapeHTML(html)
|
html = unescapeHTML(html)
|
||||||
return html
|
return html.strip()
|
||||||
|
|
||||||
|
|
||||||
def sanitize_open(filename, open_mode):
|
def sanitize_open(filename, open_mode):
|
||||||
@@ -175,16 +338,24 @@ def sanitize_open(filename, open_mode):
|
|||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32':
|
||||||
import msvcrt
|
import msvcrt
|
||||||
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
|
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
|
||||||
return (sys.stdout, filename)
|
return (sys.stdout.buffer if hasattr(sys.stdout, 'buffer') else sys.stdout, filename)
|
||||||
stream = open(encodeFilename(filename), open_mode)
|
stream = open(encodeFilename(filename), open_mode)
|
||||||
return (stream, filename)
|
return (stream, filename)
|
||||||
except (IOError, OSError), err:
|
except (IOError, OSError) as err:
|
||||||
# In case of error, try to remove win32 forbidden chars
|
if err.errno in (errno.EACCES,):
|
||||||
filename = re.sub(ur'[/<>:"\|\?\*]', u'#', filename)
|
raise
|
||||||
|
|
||||||
|
# In case of error, try to remove win32 forbidden chars
|
||||||
|
alt_filename = os.path.join(
|
||||||
|
re.sub(u'[/<>:"\\|\\\\?\\*]', u'#', path_part)
|
||||||
|
for path_part in os.path.split(filename)
|
||||||
|
)
|
||||||
|
if alt_filename == filename:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
# An exception here should be caught in the caller
|
# An exception here should be caught in the caller
|
||||||
stream = open(encodeFilename(filename), open_mode)
|
stream = open(encodeFilename(filename), open_mode)
|
||||||
return (stream, filename)
|
return (stream, alt_filename)
|
||||||
|
|
||||||
|
|
||||||
def timeconvert(timestr):
|
def timeconvert(timestr):
|
||||||
@@ -195,9 +366,10 @@ def timeconvert(timestr):
|
|||||||
timestamp = email.utils.mktime_tz(timetuple)
|
timestamp = email.utils.mktime_tz(timetuple)
|
||||||
return timestamp
|
return timestamp
|
||||||
|
|
||||||
def sanitize_filename(s, restricted=False):
|
def sanitize_filename(s, restricted=False, is_id=False):
|
||||||
"""Sanitizes a string so it could be used as part of a filename.
|
"""Sanitizes a string so it could be used as part of a filename.
|
||||||
If restricted is set, use a stricter subset of allowed characters.
|
If restricted is set, use a stricter subset of allowed characters.
|
||||||
|
Set is_id if this is not an arbitrary string, but an ID that should be kept if possible
|
||||||
"""
|
"""
|
||||||
def replace_insane(char):
|
def replace_insane(char):
|
||||||
if char == '?' or ord(char) < 32 or ord(char) == 127:
|
if char == '?' or ord(char) < 32 or ord(char) == 127:
|
||||||
@@ -208,13 +380,14 @@ def sanitize_filename(s, restricted=False):
|
|||||||
return '_-' if restricted else ' -'
|
return '_-' if restricted else ' -'
|
||||||
elif char in '\\/|*<>':
|
elif char in '\\/|*<>':
|
||||||
return '_'
|
return '_'
|
||||||
if restricted and (char in '!&\'' or char.isspace()):
|
if restricted and (char in '!&\'()[]{}$;`^,#' or char.isspace()):
|
||||||
return '_'
|
return '_'
|
||||||
if restricted and ord(char) > 127:
|
if restricted and ord(char) > 127:
|
||||||
return '_'
|
return '_'
|
||||||
return char
|
return char
|
||||||
|
|
||||||
result = u''.join(map(replace_insane, s))
|
result = u''.join(map(replace_insane, s))
|
||||||
|
if not is_id:
|
||||||
while '__' in result:
|
while '__' in result:
|
||||||
result = result.replace('__', '_')
|
result = result.replace('__', '_')
|
||||||
result = result.strip('_')
|
result = result.strip('_')
|
||||||
@@ -235,27 +408,83 @@ def orderedSet(iterable):
|
|||||||
|
|
||||||
def unescapeHTML(s):
|
def unescapeHTML(s):
|
||||||
"""
|
"""
|
||||||
@param s a string (of type unicode)
|
@param s a string
|
||||||
"""
|
"""
|
||||||
assert type(s) == type(u'')
|
assert type(s) == type(u'')
|
||||||
|
|
||||||
result = re.sub(ur'(?u)&(.+?);', htmlentity_transform, s)
|
result = re.sub(u'(?u)&(.+?);', htmlentity_transform, s)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def encodeFilename(s):
|
def encodeFilename(s):
|
||||||
"""
|
"""
|
||||||
@param s The name of the file (of type unicode)
|
@param s The name of the file
|
||||||
"""
|
"""
|
||||||
|
|
||||||
assert type(s) == type(u'')
|
assert type(s) == type(u'')
|
||||||
|
|
||||||
|
# Python 3 has a Unicode API
|
||||||
|
if sys.version_info >= (3, 0):
|
||||||
|
return s
|
||||||
|
|
||||||
if sys.platform == 'win32' and sys.getwindowsversion()[0] >= 5:
|
if sys.platform == 'win32' and sys.getwindowsversion()[0] >= 5:
|
||||||
# Pass u'' directly to use Unicode APIs on Windows 2000 and up
|
# Pass u'' directly to use Unicode APIs on Windows 2000 and up
|
||||||
# (Detecting Windows NT 4 is tricky because 'major >= 4' would
|
# (Detecting Windows NT 4 is tricky because 'major >= 4' would
|
||||||
# match Windows 9x series as well. Besides, NT 4 is obsolete.)
|
# match Windows 9x series as well. Besides, NT 4 is obsolete.)
|
||||||
return s
|
return s
|
||||||
else:
|
else:
|
||||||
return s.encode(sys.getfilesystemencoding(), 'ignore')
|
encoding = sys.getfilesystemencoding()
|
||||||
|
if encoding is None:
|
||||||
|
encoding = 'utf-8'
|
||||||
|
return s.encode(encoding, 'ignore')
|
||||||
|
|
||||||
|
def decodeOption(optval):
|
||||||
|
if optval is None:
|
||||||
|
return optval
|
||||||
|
if isinstance(optval, bytes):
|
||||||
|
optval = optval.decode(preferredencoding())
|
||||||
|
|
||||||
|
assert isinstance(optval, compat_str)
|
||||||
|
return optval
|
||||||
|
|
||||||
|
def formatSeconds(secs):
|
||||||
|
if secs > 3600:
|
||||||
|
return '%d:%02d:%02d' % (secs // 3600, (secs % 3600) // 60, secs % 60)
|
||||||
|
elif secs > 60:
|
||||||
|
return '%d:%02d' % (secs // 60, secs % 60)
|
||||||
|
else:
|
||||||
|
return '%d' % secs
|
||||||
|
|
||||||
|
def make_HTTPS_handler(opts):
|
||||||
|
if sys.version_info < (3,2):
|
||||||
|
# Python's 2.x handler is very simplistic
|
||||||
|
return compat_urllib_request.HTTPSHandler()
|
||||||
|
else:
|
||||||
|
import ssl
|
||||||
|
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
|
||||||
|
context.set_default_verify_paths()
|
||||||
|
|
||||||
|
context.verify_mode = (ssl.CERT_NONE
|
||||||
|
if opts.no_check_certificate
|
||||||
|
else ssl.CERT_REQUIRED)
|
||||||
|
return compat_urllib_request.HTTPSHandler(context=context)
|
||||||
|
|
||||||
|
class ExtractorError(Exception):
|
||||||
|
"""Error during info extraction."""
|
||||||
|
def __init__(self, msg, tb=None):
|
||||||
|
""" tb, if given, is the original traceback (so that it can be printed out). """
|
||||||
|
|
||||||
|
if not sys.exc_info()[0] in (compat_urllib_error.URLError, socket.timeout, UnavailableVideoError):
|
||||||
|
msg = msg + u'; please report this issue on GitHub.'
|
||||||
|
super(ExtractorError, self).__init__(msg)
|
||||||
|
|
||||||
|
self.traceback = tb
|
||||||
|
self.exc_info = sys.exc_info() # preserve original exception
|
||||||
|
|
||||||
|
def format_traceback(self):
|
||||||
|
if self.traceback is None:
|
||||||
|
return None
|
||||||
|
return u''.join(traceback.format_tb(self.traceback))
|
||||||
|
|
||||||
|
|
||||||
class DownloadError(Exception):
|
class DownloadError(Exception):
|
||||||
"""Download Error exception.
|
"""Download Error exception.
|
||||||
@@ -264,7 +493,10 @@ class DownloadError(Exception):
|
|||||||
configured to continue on errors. They will contain the appropriate
|
configured to continue on errors. They will contain the appropriate
|
||||||
error message.
|
error message.
|
||||||
"""
|
"""
|
||||||
pass
|
def __init__(self, msg, exc_info=None):
|
||||||
|
""" exc_info, if given, is the original exception that caused the trouble (as returned by sys.exc_info()). """
|
||||||
|
super(DownloadError, self).__init__(msg)
|
||||||
|
self.exc_info = exc_info
|
||||||
|
|
||||||
|
|
||||||
class SameFileError(Exception):
|
class SameFileError(Exception):
|
||||||
@@ -282,7 +514,8 @@ class PostProcessingError(Exception):
|
|||||||
This exception may be raised by PostProcessor's .run() method to
|
This exception may be raised by PostProcessor's .run() method to
|
||||||
indicate an error in the postprocessing task.
|
indicate an error in the postprocessing task.
|
||||||
"""
|
"""
|
||||||
pass
|
def __init__(self, msg):
|
||||||
|
self.msg = msg
|
||||||
|
|
||||||
class MaxDownloadsReached(Exception):
|
class MaxDownloadsReached(Exception):
|
||||||
""" --max-downloads limit has been reached. """
|
""" --max-downloads limit has been reached. """
|
||||||
@@ -313,15 +546,7 @@ class ContentTooShortError(Exception):
|
|||||||
self.downloaded = downloaded
|
self.downloaded = downloaded
|
||||||
self.expected = expected
|
self.expected = expected
|
||||||
|
|
||||||
|
class YoutubeDLHandler(compat_urllib_request.HTTPHandler):
|
||||||
class Trouble(Exception):
|
|
||||||
"""Trouble helper exception
|
|
||||||
|
|
||||||
This is an exception to be handled with
|
|
||||||
FileDownloader.trouble
|
|
||||||
"""
|
|
||||||
|
|
||||||
class YoutubeDLHandler(urllib2.HTTPHandler):
|
|
||||||
"""Handler for HTTP requests and responses.
|
"""Handler for HTTP requests and responses.
|
||||||
|
|
||||||
This class, when installed with an OpenerDirector, automatically adds
|
This class, when installed with an OpenerDirector, automatically adds
|
||||||
@@ -348,33 +573,108 @@ class YoutubeDLHandler(urllib2.HTTPHandler):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def addinfourl_wrapper(stream, headers, url, code):
|
def addinfourl_wrapper(stream, headers, url, code):
|
||||||
if hasattr(urllib2.addinfourl, 'getcode'):
|
if hasattr(compat_urllib_request.addinfourl, 'getcode'):
|
||||||
return urllib2.addinfourl(stream, headers, url, code)
|
return compat_urllib_request.addinfourl(stream, headers, url, code)
|
||||||
ret = urllib2.addinfourl(stream, headers, url)
|
ret = compat_urllib_request.addinfourl(stream, headers, url)
|
||||||
ret.code = code
|
ret.code = code
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def http_request(self, req):
|
def http_request(self, req):
|
||||||
for h in std_headers:
|
for h,v in std_headers.items():
|
||||||
if h in req.headers:
|
if h in req.headers:
|
||||||
del req.headers[h]
|
del req.headers[h]
|
||||||
req.add_header(h, std_headers[h])
|
req.add_header(h, v)
|
||||||
if 'Youtubedl-no-compression' in req.headers:
|
if 'Youtubedl-no-compression' in req.headers:
|
||||||
if 'Accept-encoding' in req.headers:
|
if 'Accept-encoding' in req.headers:
|
||||||
del req.headers['Accept-encoding']
|
del req.headers['Accept-encoding']
|
||||||
del req.headers['Youtubedl-no-compression']
|
del req.headers['Youtubedl-no-compression']
|
||||||
|
if 'Youtubedl-user-agent' in req.headers:
|
||||||
|
if 'User-agent' in req.headers:
|
||||||
|
del req.headers['User-agent']
|
||||||
|
req.headers['User-agent'] = req.headers['Youtubedl-user-agent']
|
||||||
|
del req.headers['Youtubedl-user-agent']
|
||||||
return req
|
return req
|
||||||
|
|
||||||
def http_response(self, req, resp):
|
def http_response(self, req, resp):
|
||||||
old_resp = resp
|
old_resp = resp
|
||||||
# gzip
|
# gzip
|
||||||
if resp.headers.get('Content-encoding', '') == 'gzip':
|
if resp.headers.get('Content-encoding', '') == 'gzip':
|
||||||
gz = gzip.GzipFile(fileobj=StringIO.StringIO(resp.read()), mode='r')
|
gz = gzip.GzipFile(fileobj=io.BytesIO(resp.read()), mode='r')
|
||||||
resp = self.addinfourl_wrapper(gz, old_resp.headers, old_resp.url, old_resp.code)
|
resp = self.addinfourl_wrapper(gz, old_resp.headers, old_resp.url, old_resp.code)
|
||||||
resp.msg = old_resp.msg
|
resp.msg = old_resp.msg
|
||||||
# deflate
|
# deflate
|
||||||
if resp.headers.get('Content-encoding', '') == 'deflate':
|
if resp.headers.get('Content-encoding', '') == 'deflate':
|
||||||
gz = StringIO.StringIO(self.deflate(resp.read()))
|
gz = io.BytesIO(self.deflate(resp.read()))
|
||||||
resp = self.addinfourl_wrapper(gz, old_resp.headers, old_resp.url, old_resp.code)
|
resp = self.addinfourl_wrapper(gz, old_resp.headers, old_resp.url, old_resp.code)
|
||||||
resp.msg = old_resp.msg
|
resp.msg = old_resp.msg
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
https_request = http_request
|
||||||
|
https_response = http_response
|
||||||
|
|
||||||
|
def unified_strdate(date_str):
|
||||||
|
"""Return a string with the date in the format YYYYMMDD"""
|
||||||
|
upload_date = None
|
||||||
|
#Replace commas
|
||||||
|
date_str = date_str.replace(',',' ')
|
||||||
|
# %z (UTC offset) is only supported in python>=3.2
|
||||||
|
date_str = re.sub(r' (\+|-)[\d]*$', '', date_str)
|
||||||
|
format_expressions = ['%d %B %Y', '%B %d %Y', '%b %d %Y', '%Y-%m-%d', '%d/%m/%Y', '%Y/%m/%d %H:%M:%S']
|
||||||
|
for expression in format_expressions:
|
||||||
|
try:
|
||||||
|
upload_date = datetime.datetime.strptime(date_str, expression).strftime('%Y%m%d')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return upload_date
|
||||||
|
|
||||||
|
def date_from_str(date_str):
|
||||||
|
"""
|
||||||
|
Return a datetime object from a string in the format YYYYMMDD or
|
||||||
|
(now|today)[+-][0-9](day|week|month|year)(s)?"""
|
||||||
|
today = datetime.date.today()
|
||||||
|
if date_str == 'now'or date_str == 'today':
|
||||||
|
return today
|
||||||
|
match = re.match('(now|today)(?P<sign>[+-])(?P<time>\d+)(?P<unit>day|week|month|year)(s)?', date_str)
|
||||||
|
if match is not None:
|
||||||
|
sign = match.group('sign')
|
||||||
|
time = int(match.group('time'))
|
||||||
|
if sign == '-':
|
||||||
|
time = -time
|
||||||
|
unit = match.group('unit')
|
||||||
|
#A bad aproximation?
|
||||||
|
if unit == 'month':
|
||||||
|
unit = 'day'
|
||||||
|
time *= 30
|
||||||
|
elif unit == 'year':
|
||||||
|
unit = 'day'
|
||||||
|
time *= 365
|
||||||
|
unit += 's'
|
||||||
|
delta = datetime.timedelta(**{unit: time})
|
||||||
|
return today + delta
|
||||||
|
return datetime.datetime.strptime(date_str, "%Y%m%d").date()
|
||||||
|
|
||||||
|
class DateRange(object):
|
||||||
|
"""Represents a time interval between two dates"""
|
||||||
|
def __init__(self, start=None, end=None):
|
||||||
|
"""start and end must be strings in the format accepted by date"""
|
||||||
|
if start is not None:
|
||||||
|
self.start = date_from_str(start)
|
||||||
|
else:
|
||||||
|
self.start = datetime.datetime.min.date()
|
||||||
|
if end is not None:
|
||||||
|
self.end = date_from_str(end)
|
||||||
|
else:
|
||||||
|
self.end = datetime.datetime.max.date()
|
||||||
|
if self.start > self.end:
|
||||||
|
raise ValueError('Date range: "%s" , the start date must be before the end date' % self)
|
||||||
|
@classmethod
|
||||||
|
def day(cls, day):
|
||||||
|
"""Returns a range that only contains the given day"""
|
||||||
|
return cls(day,day)
|
||||||
|
def __contains__(self, date):
|
||||||
|
"""Check if the date is in the range"""
|
||||||
|
if not isinstance(date, datetime.date):
|
||||||
|
date = date_from_str(date)
|
||||||
|
return self.start <= date <= self.end
|
||||||
|
def __str__(self):
|
||||||
|
return '%s - %s' % ( self.start.isoformat(), self.end.isoformat())
|
||||||
|
2
youtube_dl/version.py
Normal file
2
youtube_dl/version.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
__version__ = '2013.06.21'
|
Reference in New Issue
Block a user