Compare commits
895 Commits
extjs4-por
...
archive/1.
Author | SHA1 | Date | |
---|---|---|---|
e050905b29 | |||
6c3442e7e7 | |||
993abbc6a6 | |||
a2fcebe15c | |||
b8e5ebe822 | |||
e33a8fbea4 | |||
bcc7a74725 | |||
ffb8d9f8c3 | |||
396417bcd0 | |||
b13da8a42a | |||
415979e2f7 | |||
5f0694deb2 | |||
6d14be18b0 | |||
65fac156eb | |||
956f2ad574 | |||
275c93657f | |||
38d7b7cdfd | |||
7661127b9d | |||
a6e8ac8725 | |||
d91584b700 | |||
5427cbb73a | |||
d977915f32 | |||
a86b6f0f8f | |||
3dfe6af1ee | |||
1f315a9ef0 | |||
08c03d7678 | |||
dd08cb29e5 | |||
909176e9aa | |||
85eeadcfca | |||
f870741d9d | |||
cc69c9c85b | |||
41acade01a | |||
9bec5142c7 | |||
0f1f62ec62 | |||
318ab17986 | |||
25150f13af | |||
7cde3efb94 | |||
7f01dc909e | |||
10ebf9b0b0 | |||
3ba5443c76 | |||
c39f00fa0b | |||
3962c41a55 | |||
42ba9086d0 | |||
2d4dec669e | |||
bcf0fe4a61 | |||
1dc4c465c7 | |||
b52de1549e | |||
8a3f15e5c0 | |||
8565eccb3d | |||
30eaf775c2 | |||
ffb1316f09 | |||
bd80ad62a0 | |||
78851becf2 | |||
af76abb038 | |||
bf01b53bda | |||
8a48ec0126 | |||
c3a02e5291 | |||
3c1995476d | |||
48cedf635f | |||
0b4627be8a | |||
739537f860 | |||
df88c82265 | |||
5394ac5604 | |||
f739269dfd | |||
f57ee74ee2 | |||
798f5e2deb | |||
a7fe4d4510 | |||
6c73105a73 | |||
e66be42c81 | |||
2263463114 | |||
454c7be364 | |||
85fdacc0e7 | |||
869dbab459 | |||
852b51f224 | |||
492ad07965 | |||
904a51835b | |||
d38b8fc45c | |||
5f92810f76 | |||
34e12fcb38 | |||
f769afd3ac | |||
e1d78c3de6 | |||
15a4023208 | |||
cbb7415a18 | |||
1a11e085b2 | |||
fcb65940d9 | |||
aa10e084a4 | |||
b2be4aba53 | |||
a1e66a4dc1 | |||
6240243251 | |||
ad58fca1f9 | |||
f221ae53eb | |||
5590c31ace | |||
4e5754b285 | |||
90a22af5e5 | |||
77f8449c0c | |||
be7ad16a3f | |||
e28954f63e | |||
52e60ac5b0 | |||
6ffe5cd2a4 | |||
9038357d78 | |||
d56f6cb4f1 | |||
5d301a4b33 | |||
e65a7ff2ea | |||
1bdc99ded7 | |||
dd34492e16 | |||
9f3b2f3167 | |||
0260e34189 | |||
5464cf674a | |||
a58ce30e7b | |||
83cecc0c09 | |||
00757af149 | |||
639eefcf1d | |||
69a1f5f210 | |||
0a74812eeb | |||
cf437b6a33 | |||
0ab7ebd017 | |||
34e92b9f12 | |||
86b1b75fb8 | |||
4b9dcf377c | |||
560318a5a7 | |||
244ae878c9 | |||
f9b7892976 | |||
5f5b6fad0b | |||
5c545c5e0b | |||
20088a5c70 | |||
099a4eb8c6 | |||
ad7e519fb2 | |||
df57c7f924 | |||
7315255831 | |||
eab7850ed6 | |||
542e028977 | |||
f131194b75 | |||
d7e6afb01e | |||
e1dcf378c3 | |||
697c22a46c | |||
7ca704be72 | |||
72d381a3b6 | |||
59c2520e0d | |||
58d385241f | |||
58059300bd | |||
e4f2a450d6 | |||
64bba77807 | |||
a13b4270b5 | |||
52c8fde461 | |||
0a01aa28b0 | |||
bfb202086d | |||
6032c25813 | |||
6cbb2fa5e1 | |||
cdf301601f | |||
1b974d1061 | |||
602a913fa3 | |||
6a8f24e973 | |||
fde46885e9 | |||
7223a51ba5 | |||
8ac65d77e0 | |||
65ebcf5384 | |||
53caeb4565 | |||
3b1cb0f58e | |||
41ac46c7fe | |||
8e3d737adc | |||
7ef9e3dbe0 | |||
78fcf1781a | |||
2b08ed06af | |||
0cdab04a64 | |||
84aca3c009 | |||
9662ccf486 | |||
83719e8404 | |||
04d90903a6 | |||
f599b883cf | |||
bef71e60b3 | |||
acf4fc4193 | |||
123dd8f011 | |||
0516e3df45 | |||
0c750084dc | |||
907109b8bc | |||
630aa730d5 | |||
16faa26124 | |||
ebabd20c98 | |||
d40dfcd53c | |||
6ab951caee | |||
52e0993fa3 | |||
d7bb5dfa8b | |||
7c3d44c42e | |||
dd6e7ec490 | |||
2c1a863ffb | |||
40382002f6 | |||
05b4cb5546 | |||
75dca80ac4 | |||
cc56764ee9 | |||
53f485d87e | |||
f95cfb42c3 | |||
26f5be1760 | |||
d3f47097c1 | |||
7a2092d3c4 | |||
3e632600c6 | |||
4df58b51ff | |||
4fc8032c88 | |||
5e1874eb8d | |||
810391316c | |||
47daef1e47 | |||
294ad9fae1 | |||
f1fe593fd6 | |||
c982e8de67 | |||
33339b1dc6 | |||
ecf5af1e16 | |||
4e77c46694 | |||
3f8526160d | |||
82fb3408ee | |||
223c9319c7 | |||
c61c2d8c3a | |||
8bdf1e9044 | |||
98dcc3f26e | |||
e2b0ceae1d | |||
5dba838533 | |||
54eb28a097 | |||
4bd4c78969 | |||
1990bdcb52 | |||
70bf274974 | |||
a4fb8e769b | |||
56bbc90c5b | |||
2c54c696a1 | |||
ddde10ec99 | |||
fed5221503 | |||
c0650f88d1 | |||
5d5edd2639 | |||
f77440efcb | |||
74aa924625 | |||
0338fc6f2d | |||
12118c5454 | |||
1bdb072ac8 | |||
ade26794ec | |||
8c4a08bb87 | |||
34650f4b3c | |||
59fa974b3b | |||
98e8418087 | |||
176064b520 | |||
c5ad5589df | |||
9987d335a0 | |||
d6b44afb99 | |||
7597ba9343 | |||
41f1ad9f5f | |||
29d3e72f49 | |||
d04af1e392 | |||
c620ddcba0 | |||
e8aee7327b | |||
018330c92c | |||
58c048f1b0 | |||
9f75d4597e | |||
ffc48d3810 | |||
c38b00dd07 | |||
479c96745c | |||
63807899a0 | |||
a3806b6d7a | |||
7bd87d1a82 | |||
8bf18d6694 | |||
06ee112344 | |||
3cc43f63a0 | |||
27bfa5a649 | |||
5057e2caab | |||
686fb31844 | |||
03d5654a16 | |||
83f0d72601 | |||
19093e03ae | |||
d28de71995 | |||
f107485871 | |||
984691f74c | |||
8ba8e24fab | |||
7c9433fd8b | |||
821f395d8b | |||
e9ba3972ad | |||
7fbe163c24 | |||
e4118048eb | |||
bb67721002 | |||
9856f97323 | |||
e2e37a83de | |||
61c125420b | |||
36a78d8f21 | |||
3f4cfd5c88 | |||
cc85f00588 | |||
72b58d19c1 | |||
bbf17772ac | |||
2cbcb35c9e | |||
7ef7cc41b6 | |||
7c6d1f30ff | |||
8911600155 | |||
c58221c866 | |||
92eb9feae4 | |||
b9311d4b57 | |||
b0d78da13b | |||
12365241a5 | |||
8ba5e78610 | |||
1bfaf74ff9 | |||
e362b36a59 | |||
a0ae3ebfce | |||
08d7b9fba3 | |||
95ce85ec78 | |||
93dee4c764 | |||
dcd85e64c0 | |||
58e8a0e562 | |||
bfa3564ba3 | |||
669d10e6be | |||
3d1b47bca6 | |||
2fde78f236 | |||
444740709a | |||
d7d91aec0b | |||
e865180e83 | |||
782b39c90d | |||
7e71995e55 | |||
1dd078f4f1 | |||
0045ec0cf1 | |||
5ecc580463 | |||
70047c367a | |||
b65918d616 | |||
3ca886ac7a | |||
74208c27f8 | |||
7cc8243849 | |||
c741e127b9 | |||
b5ea33e506 | |||
ffe0c168bb | |||
b8c345ff26 | |||
9286d43ba8 | |||
23b0f58eb4 | |||
ccd2e5e41d | |||
62a1602d5a | |||
b50c848054 | |||
e4d94ea528 | |||
fc4ca861da | |||
74d59eb6ec | |||
ce4b4ef48e | |||
2e0b0d9474 | |||
0e4101a9a3 | |||
ba4ca111c6 | |||
827b53792b | |||
6e75a194c3 | |||
a3a9cae9f7 | |||
f447bd5cea | |||
f79785abf2 | |||
a2423ba536 | |||
4307699bbf | |||
e7a2a8dcee | |||
67450f43b3 | |||
10ccd9aaaf | |||
16568404e7 | |||
901f5a84bf | |||
8ffa80c2a2 | |||
0f16faabcb | |||
508b5aed8f | |||
f2b2f98fac | |||
42e33d452e | |||
ea75cfb6d7 | |||
9155191b0a | |||
0571799a07 | |||
aaf4aa5226 | |||
3e98ef5f89 | |||
c398ef57a5 | |||
68b413af5f | |||
3f79d36489 | |||
582c6735d0 | |||
5d43c2ac94 | |||
845b95d88f | |||
66437531e8 | |||
666bb09513 | |||
7d88cb1850 | |||
d825be4901 | |||
135c98634a | |||
ecb457c043 | |||
cd8ab2805c | |||
b7ca968fae | |||
8f71b8d5c6 | |||
8a72187998 | |||
d9f1210fe7 | |||
0a9fe242e7 | |||
e53413902c | |||
bd500c30de | |||
b8b27bdaf2 | |||
ad8b757387 | |||
fd8dc457fa | |||
69509f446e | |||
db502253c9 | |||
8d25a5e691 | |||
3c85cc4b03 | |||
03abe32054 | |||
7695c4e566 | |||
a64fe05890 | |||
762ad783e2 | |||
aa969fd830 | |||
e5f56e2fdd | |||
610cd7dd33 | |||
bb7b529c29 | |||
43390b850a | |||
8c2189f161 | |||
37ea2854a2 | |||
7b9bca0957 | |||
ec3d698a14 | |||
3309f59333 | |||
68990b4a3f | |||
17cac01673 | |||
18eb885983 | |||
cb661f2595 | |||
fc14b00868 | |||
725d06f439 | |||
3569e18712 | |||
42118932f2 | |||
c480132fa2 | |||
313a04f9a6 | |||
d1f2776463 | |||
dac524ccf3 | |||
bafd0209af | |||
0a6c2ee147 | |||
85cdbec10d | |||
f083a3b67b | |||
d08ea7da81 | |||
483c439e38 | |||
914413c88f | |||
438dff177c | |||
196458399e | |||
1d243d5967 | |||
418037dd43 | |||
900907a545 | |||
b3a721b539 | |||
d783b8ead7 | |||
5572f61022 | |||
fe6e9ec467 | |||
675a64e213 | |||
931b8aec71 | |||
f19caf69e0 | |||
56a24f16f2 | |||
275c939b95 | |||
8238c63156 | |||
c19718b66a | |||
eac52dec73 | |||
124daaf8b2 | |||
5d6fa23011 | |||
0da64f7db4 | |||
b9030dfb8b | |||
3528549430 | |||
39a04aae20 | |||
72a2ace0a1 | |||
4240345daf | |||
cb9867a26f | |||
07e166b94a | |||
3c7f492451 | |||
aeca5dd1e7 | |||
c194f6bbe4 | |||
4d77241603 | |||
a2a45f1a0b | |||
217087a2fe | |||
bb89a355e5 | |||
954c0e2d4d | |||
9ce738e39a | |||
911d583ff0 | |||
07bc1322dd | |||
1058435700 | |||
0676aaf918 | |||
5d0dace131 | |||
be75d5021b | |||
b0029517eb | |||
2ae936df44 | |||
1f5cfd16e0 | |||
b5b0db6c60 | |||
1f660653ff | |||
7020e5ca90 | |||
00ebaae67a | |||
eb773bba2c | |||
e10cfb8f8c | |||
fcf26bad45 | |||
ec373c0ec9 | |||
933f80c01c | |||
89634137b9 | |||
e7dada6afc | |||
8db789ffe2 | |||
dd3aab1cef | |||
54769fe190 | |||
fa7edd0bad | |||
2b865273f6 | |||
403ad26111 | |||
6bc3968ba4 | |||
bbeb11b1e7 | |||
e4840d6b37 | |||
88db73e244 | |||
a359374547 | |||
c5e328c3bd | |||
bc425b392a | |||
4feb816380 | |||
ff95d9720a | |||
7847362dbb | |||
7b1e8862b4 | |||
ed883125fd | |||
9799c64505 | |||
d1f788ebe3 | |||
3744bdad69 | |||
ea75828f25 | |||
966fc6f64f | |||
5ff0d61b52 | |||
f5956f01e7 | |||
717db367e8 | |||
e0efe3885a | |||
836da50f78 | |||
85b1753a28 | |||
eb6959fb98 | |||
30c142ac4d | |||
08a75bd9f9 | |||
754a5a7f8a | |||
863fd7d2b7 | |||
56f2283e3e | |||
272d2005e0 | |||
375ee2dd1c | |||
98dcc8e3b1 | |||
e91458662f | |||
2600785cbb | |||
e5576dff1e | |||
4c648bc09f | |||
17d9739abb | |||
c1bf52e8d9 | |||
d60b436fcb | |||
2e0e0fb6b5 | |||
fe4f732c12 | |||
25d930b307 | |||
083c7fbb32 | |||
81fc47d080 | |||
e1745443bf | |||
649f933316 | |||
8b46ed8bdb | |||
62aa339fd8 | |||
96d8f10080 | |||
3d8e3f4add | |||
08eb6002f8 | |||
0015c9af86 | |||
4365e1ff39 | |||
0b6af77d57 | |||
6b2320d4b6 | |||
a5742f892d | |||
01adb882ea | |||
9fd6d3d418 | |||
d4834bc6c7 | |||
e4eda24e8f | |||
39f648684e | |||
56f8e213e3 | |||
cb86828fa6 | |||
5cffc8f7e0 | |||
97912a28e4 | |||
9d2b0101d6 | |||
e49b7d5c23 | |||
78a0ffd437 | |||
dee8c30968 | |||
a34f224201 | |||
7a3c4440dc | |||
ce0c6d99f2 | |||
d96fae520d | |||
d3c3d64cd4 | |||
b530658e20 | |||
eb70a7a6dc | |||
370035ffc5 | |||
0941377fac | |||
1a8aa4b920 | |||
1c8327034d | |||
9ed155c456 | |||
3d813ea1f8 | |||
0f962aeda5 | |||
3a0b6f8a6d | |||
cade8ee784 | |||
0d27032c06 | |||
3b8bbb2e77 | |||
9c9064b246 | |||
d1a3cbebbe | |||
79f8af688a | |||
bbba60f34c | |||
a2d313383c | |||
e336cd64b4 | |||
eff3577505 | |||
2504b2520a | |||
d8560f5c25 | |||
51802f7c54 | |||
a3538c8937 | |||
714b7f3c55 | |||
f0c4a4c766 | |||
683e4be529 | |||
93a860f2a1 | |||
9c4cd86492 | |||
fc6f9ebc3b | |||
603ca1b855 | |||
3b89595d38 | |||
448478485a | |||
4234583fc7 | |||
bdd9bd11b5 | |||
8dbdb02967 | |||
0555fbeb9d | |||
3126407d74 | |||
2d09035dc4 | |||
ed229d2ace | |||
9e0d173115 | |||
81d4b00ade | |||
1a9be0e9a4 | |||
28fc325db9 | |||
eb309813ea | |||
2e7bd90bda | |||
f59eca4405 | |||
fa209dfd5f | |||
8e8717c867 | |||
8644bc219a | |||
7c276f3133 | |||
ce1aca54b5 | |||
54642720e4 | |||
18ebf5b912 | |||
ff087d133c | |||
5d4c8241ea | |||
fbc664fa14 | |||
cc130c0085 | |||
87802aa965 | |||
678be3ce15 | |||
52f89270e6 | |||
1a9ae62669 | |||
e7b5be6a60 | |||
7e51c82705 | |||
d6e619c413 | |||
ba1cc6ef1f | |||
b5a0f32826 | |||
2cceb3a349 | |||
6ad3a770af | |||
f7c21fd87b | |||
01465f583f | |||
d3b0df5788 | |||
4f3c753fc1 | |||
6a873c524e | |||
010b6dd4af | |||
00900fef1c | |||
d2e1d66f43 | |||
c95ca18b37 | |||
34f81634e5 | |||
2cb77d17ce | |||
4c32aa14d0 | |||
5d9120b667 | |||
8a3bad9fc2 | |||
2005691312 | |||
47c9cccd74 | |||
2186fdb870 | |||
5b1e43735b | |||
e54f6c84d6 | |||
30d91f17dc | |||
af058bbdc7 | |||
7232dc4b01 | |||
263b10ffd2 | |||
36d5ff5040 | |||
85d4602949 | |||
a75fa41c42 | |||
e75ae7c81e | |||
fad24e93f3 | |||
94d53ac368 | |||
52bf08dfd1 | |||
532973c3d1 | |||
254efa88e5 | |||
583248f558 | |||
69956ad1db | |||
1e274cfb48 | |||
b0e38d7bde | |||
4d643f2cba | |||
09a56ae03c | |||
c2b5d29c6a | |||
a3d2b41b54 | |||
da679371b7 | |||
83283cdcf3 | |||
e180e2af88 | |||
14c3655ba1 | |||
f330469bc9 | |||
c81fbf788d | |||
a35b2497f3 | |||
8fe299dc21 | |||
e66f0cb503 | |||
5e129b3c64 | |||
2d40d2b224 | |||
ce0234f0ef | |||
c225eae189 | |||
afa941df2e | |||
49cbcf1f9c | |||
3c3b68e2cc | |||
b93477c41e | |||
766c48e3ca | |||
1f73476dc3 | |||
5bfb98f9a9 | |||
d07b53f665 | |||
449be00e33 | |||
655af15695 | |||
a549eac063 | |||
6fa2728ddc | |||
9060de9b70 | |||
57ac902d50 | |||
05578e0c75 | |||
a0d4141afd | |||
493d0ac690 | |||
fe9fe7977c | |||
e579a78d26 | |||
bf96475840 | |||
93c49495b2 | |||
54ae8a4482 | |||
d658c8fe47 | |||
58134925a2 | |||
244583ef97 | |||
0b821640bb | |||
612ed4123f | |||
91b9eac075 | |||
5be93cd5d8 | |||
816f3ff6d2 | |||
2e62140d2c | |||
2381b1ae28 | |||
6a131f021e | |||
871d9ac4b0 | |||
0d665d772d | |||
aef2a83f25 | |||
e6cd4d17ee | |||
b9c49f27fa | |||
e2118b6bb2 | |||
d7ba74f01d | |||
bc9abc8bc9 | |||
00ad550a52 | |||
884bfd777e | |||
e7db1b285f | |||
ee3a17bf37 | |||
292ffb35ac | |||
fd458fbe64 | |||
b9fdb5a65f | |||
36f92231a6 | |||
6cb584d53d | |||
2eb1a51f6b | |||
84804d37cc | |||
1da7a518b5 | |||
808d9bfba8 | |||
53b4a06fd1 | |||
4490cd371a | |||
7d36a4fa51 | |||
6f844a86d2 | |||
03689a805b | |||
1d0857964e | |||
fd3a33af03 | |||
2354eeca7b | |||
ca0003a7af | |||
2c615e468b | |||
3794773e95 | |||
1731fd641b | |||
c379b6c3b2 | |||
e1896d2ace | |||
5f168f3a25 | |||
8346b4bb77 | |||
39bbe76436 | |||
3bd28208d1 | |||
ffebfb9cdf | |||
93091fbe23 | |||
efd2762255 | |||
4870d34a52 | |||
26410ca9c1 | |||
c1477e45cb | |||
25e58bc8a2 | |||
4d2a0b1856 | |||
82fbbad385 | |||
29a306e378 | |||
5cfe3b5601 | |||
3f458e46dd | |||
1067eb7f98 | |||
dca27a4cf9 | |||
b0ceae8d28 | |||
dc0bf3bc88 | |||
3b9d7ff9c3 | |||
a165d5d746 | |||
cc02ebea6a | |||
41ffee5d8a | |||
14a89b3f8a | |||
6f0c2af58a | |||
84cccabf19 | |||
7fb483adde | |||
28ce7a70a0 | |||
14565977fa | |||
e4420ef354 | |||
02ad0b93ab | |||
6d2a001635 | |||
2a3eb0578c | |||
60fac28217 | |||
59e01e7ecf | |||
4c52ee4229 | |||
8428524793 | |||
21c8d02d9a | |||
0c687c7684 | |||
78f9efefd9 | |||
6b228ce31f | |||
40ce4ec731 | |||
c029c312e4 | |||
16c38cd027 | |||
e23a6b852a | |||
90e4de54e9 | |||
c1505bea3a | |||
6235e832fe | |||
a71f14c47e | |||
ed3b23b0fc | |||
6402634ec1 | |||
3e68733cfd | |||
f847a7dc4e | |||
c7954c20eb | |||
dc7ed11601 | |||
d898def9ec | |||
3e2f6c4060 | |||
321a22a6f0 | |||
b4774af2f3 | |||
d0fd709c74 | |||
e24212b3f8 | |||
f8f72af6dc | |||
b9caa4eeeb | |||
6c3b216b40 | |||
eaad867885 | |||
f6b9f67df8 | |||
24fe3f7fd5 | |||
da2fb41a3a | |||
f8d7f22167 | |||
b75abc70e5 | |||
2d821bd79a | |||
12d9a7a5bd | |||
c118fa36a9 | |||
82c91cdc51 | |||
5501094214 | |||
b41aa808be | |||
b9336889f5 | |||
995f5387eb | |||
38958d3c4f | |||
b45e019f08 | |||
d93fcf6eea | |||
a2d75a5274 | |||
8797c3ce1b | |||
79749cca03 | |||
5fd8628761 | |||
0e80b3ea0a | |||
aa61d33ee2 | |||
13f29a77dd | |||
97453d1411 | |||
62d02091b3 | |||
161ad0ff0d | |||
7f323ec0fc | |||
05aebbb575 | |||
de85e1dcdc | |||
1ce480ff23 | |||
007a9912d2 | |||
d793b9e6b8 | |||
72ec926c1a | |||
0d431ae7db | |||
f1e0e3be15 | |||
e8bf5eb592 | |||
d9a2c4db72 | |||
8fb7277a82 | |||
35128cf18f | |||
6ff1da2391 | |||
4614188c62 | |||
80297b8e45 | |||
9173a9cfdd | |||
571d1079f6 | |||
0497c407e1 | |||
8b58c960f3 | |||
b615ebe1b8 | |||
3ed8279219 | |||
f2944bdeef | |||
0fcd90ee2c | |||
26460808e7 | |||
7aba1af0b2 | |||
4d2b7df49d | |||
bd775d0d40 | |||
7fb3c3c04c | |||
19c27ee8c5 | |||
d69b8e1099 | |||
88daf82cb0 | |||
99c1a61383 | |||
2e55769c18 | |||
259d2633e7 | |||
8e5aab660c | |||
fc96e9d02c | |||
821d403a6c | |||
5e0d988ef0 | |||
925ac42f7c | |||
1ac72b81b6 | |||
3417caf1d2 | |||
1bcfc91c35 | |||
6ee0e5b6be | |||
58a74202e1 | |||
a4c6f4e8c9 | |||
60f3d32de7 | |||
b3eed8a1f0 | |||
37137d9b54 | |||
4fb14b581d | |||
98da4d0291 | |||
f0c06f4bc5 | |||
63d701305c | |||
99396afa0c | |||
6231dbd1ca | |||
8f021c7f06 | |||
6bb4559d18 | |||
7c9eea0361 | |||
15247507d4 | |||
10de8d5475 | |||
e304c1f719 | |||
48d3e89d84 | |||
44f9e17a09 |
27
.gitattributes
vendored
@ -1,9 +1,24 @@
|
||||
/libtorrent export-ignore
|
||||
/win32 export-ignore
|
||||
docs/build export-ignore
|
||||
docs/source export-ignore
|
||||
/tests export-ignore
|
||||
deluge/scripts export-ignore
|
||||
/libtorrent/ export-ignore
|
||||
/win32/ export-ignore
|
||||
/osx/ export-ignore
|
||||
docs/build/ export-ignore
|
||||
docs/source/ export-ignore
|
||||
/tests/ export-ignore
|
||||
deluge/scripts/ export-ignore
|
||||
setup.cfg export-ignore
|
||||
check_glade.sh export-ignore
|
||||
createicons.sh export-ignore
|
||||
create_potfiles_in.py export-ignore
|
||||
gettextize.sh export-ignore
|
||||
deluge/i18n/deluge.pot export-ignore
|
||||
deluge/ui/web/css/*-debug.css export-ignore
|
||||
deluge/ui/web/js/*-debug.js export-ignore
|
||||
deluge/ui/web/js/deluge-all/ export-ignore
|
||||
deluge/ui/web/js/ext-extensions/ export-ignore
|
||||
deluge/ui/web/gen_gettext.py export-ignore
|
||||
deluge/ui/web/build export-ignore
|
||||
deluge/ui/web/docs/ export-ignore
|
||||
|
||||
.gitattributes export-ignore
|
||||
.gitmodules export-ignore
|
||||
.gitignore export-ignore
|
||||
|
4
.gitignore
vendored
@ -7,3 +7,7 @@ dist
|
||||
*.pyc
|
||||
*.tar.*
|
||||
_trial_temp
|
||||
deluge/i18n/*/
|
||||
*.desktop
|
||||
.build_data*
|
||||
osx/app
|
||||
|
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
||||
[submodule "libtorrent"]
|
||||
path = libtorrent
|
||||
url = git://deluge-torrent.org/libtorrent
|
797
AUTHORS
Normal file
@ -0,0 +1,797 @@
|
||||
Authors:
|
||||
* Andrew Resch ('andar') <andrewresch@gmail.com>
|
||||
* Damien Churchill ('damoxc') <damoxc@gmail.com>
|
||||
|
||||
Main Developers:
|
||||
* Andrew Resch
|
||||
* Damien Churchill
|
||||
* John Garland ('johnnyg') <johnnybg+deluge@gmail.com>
|
||||
* Calum Lind ('cas') <calumlind+deluge@gmail.com>
|
||||
|
||||
libtorrent (http://www.libtorrent.org):
|
||||
* Arvid Norberg
|
||||
|
||||
Contributors (and Past Developers):
|
||||
* Zach Tibbitts <zach@collegegeek.org>
|
||||
* Alon Zakai ('Kripken') <kripkensteiner@gmail.com>
|
||||
* Marcos Mobley ('markybob') <markybob@gmail.com>
|
||||
* Alex Dedul
|
||||
* Sadrul Habib Chowdhury
|
||||
* Ido Abramovich <ido.deluge@gmail.com>
|
||||
* Martijn Voncken <mvoncken@gmail.com>
|
||||
* Mark Stahler ('kramed') <markstahler@gmail.com>
|
||||
* Pedro Algarvio ('s0undt3ch') <ufs@ufsoft.org>
|
||||
* Cristian Greco ('cgreco') <cristian@regolo.cc>
|
||||
* Chase Sterling ('gazpachoKing') <chase.sterling@gmail.com>
|
||||
|
||||
Plugin Developers:
|
||||
* Autoadd : Chase Sterling
|
||||
* Blocklist : John Garland
|
||||
* Execute : Damien Churchill
|
||||
* Extractor : Andrew Resch
|
||||
* Label : Martijn Voncken
|
||||
* Notifications : Pedro Algarvio
|
||||
* Scheduler : Andrew Resch
|
||||
* Webui : Damien Churchill
|
||||
|
||||
Images Authors:
|
||||
|
||||
* files: deluge/data/pixmaps/*.svg, *.png
|
||||
deluge/ui/web/icons/active.png, alert.png, all.png, checking.png, dht.png,
|
||||
downloading.png, inactive.png, queued.png, seeding.png, traffic.png
|
||||
exceptions: deluge/data/pixmaps/deluge.svg and derivatives
|
||||
copyright: Andrew Resch
|
||||
license: GPLv3
|
||||
|
||||
* files: deluge/data/pixmaps/deluge.svg and derivatives
|
||||
deluge/ui/web/icons/apple-pre-*.png, deluge*.png
|
||||
deluge/ui/web/images/deluge*.png
|
||||
copyright: Andrew Wedderburn
|
||||
license: GPLv3
|
||||
|
||||
* files: deluge/plugins/blocklist/blocklist/data/*.png
|
||||
deluge/data/pixmaps/tracker_warning16.png, tracker_all16.png, lock48.png
|
||||
copyright: Gnome Icon Theme
|
||||
license: GPLv2
|
||||
url: http://ftp.acc.umu.se/pub/GNOME/sources/gnome-icon-theme
|
||||
|
||||
* files: deluge/data/pixmaps/magnet.png
|
||||
copyright: Woothemes
|
||||
license: Freeware
|
||||
icon pack: WP Woothemes Ultimate
|
||||
url: http://www.woothemes.com/
|
||||
|
||||
* files: deluge/data/pixmaps/flags/*.png
|
||||
copyright: Mark James <mjames@gmail.com>
|
||||
license: Public Domain
|
||||
url: http://famfamfam.com/lab/icons/flags/
|
||||
|
||||
* files: deluge/ui/web/icons/*.png
|
||||
exceptions: apple-pre-*.png, active.png, alert.png, all.png, deluge.png, dht.png,
|
||||
downloading.png, inactive.png, queued.png, seeding.png, traffic.png
|
||||
copyright: Yusuke Kamiyamane <p@yusukekamiyamane.com>
|
||||
license: Creative Commons Attribution 3.0 License
|
||||
url: http://p.yusukekamiyamane.com/
|
||||
|
||||
* files: deluge/ui/web/images/spinner.gif, spinner-split.gif
|
||||
copyright: Steven Chim
|
||||
license: BSD license
|
||||
url: http://members.upc.nl/j.chim/ext/spinner2/ext-spinner.html
|
||||
|
||||
Translation Contributors:
|
||||
* files: deluge/i18n/*.po
|
||||
|
||||
Aaron Wang Shi
|
||||
abbigss
|
||||
ABCdatos
|
||||
Abcx
|
||||
Actam
|
||||
Adam
|
||||
adaminikisi
|
||||
adi_oporanu
|
||||
Adrian Goll
|
||||
afby
|
||||
Ahmades
|
||||
Ahmad Farghal
|
||||
Ahmad Gharbeia أحمد غربية
|
||||
akira
|
||||
Aki Sivula
|
||||
Alan Pepelko
|
||||
Alberto
|
||||
Alberto Ferrer
|
||||
alcatr4z
|
||||
AlckO
|
||||
Aleksej Korgenkov
|
||||
Alessio Treglia
|
||||
Alexander Ilyashov
|
||||
Alexander Matveev
|
||||
Alexander Saltykov
|
||||
Alexander Taubenkorb
|
||||
Alexander Telenga
|
||||
Alexander Yurtsev
|
||||
Alexandre Martani
|
||||
Alexandre Rosenfeld
|
||||
Alexandre Sapata Carbonell
|
||||
Alexey Osipov
|
||||
Alin Claudiu Radut
|
||||
allah
|
||||
AlSim
|
||||
Alvaro Carrillanca P.
|
||||
A.Matveev
|
||||
Andras Hipsag
|
||||
András Kárász
|
||||
Andrea Ratto
|
||||
Andreas Johansson
|
||||
Andreas Str
|
||||
André F. Oliveira
|
||||
AndreiF
|
||||
andrewh
|
||||
Angel Guzman Maeso
|
||||
Aníbal Deboni Neto
|
||||
animarval
|
||||
Antonio Cono
|
||||
antoniojreyes
|
||||
Anton Shestakov
|
||||
Anton Yakutovich
|
||||
antou
|
||||
Arkadiusz Kalinowski
|
||||
Artin
|
||||
artir
|
||||
Astur
|
||||
Athanasios Lefteris
|
||||
Athmane MOKRAOUI (ButterflyOfFire)
|
||||
Augusta Carla Klug
|
||||
Avoledo Marco
|
||||
axaard
|
||||
AxelRafn
|
||||
Axezium
|
||||
Ayont
|
||||
b3rx
|
||||
Bae Taegil
|
||||
Bajusz Tamás
|
||||
Balaam's Miracle
|
||||
Ballestein
|
||||
Bent Ole Fosse
|
||||
berto89
|
||||
bigx
|
||||
Bjorn Inge Berg
|
||||
blackbird
|
||||
Blackeyed
|
||||
blackmx
|
||||
BlueSky
|
||||
Blutheo
|
||||
bmhm
|
||||
bob00work
|
||||
boenki
|
||||
Bogdan Bădic-Spătariu
|
||||
bonpu
|
||||
Boone
|
||||
boss01
|
||||
Branislav Jovanović
|
||||
bronze
|
||||
brownie
|
||||
Brus46
|
||||
bumper
|
||||
butely
|
||||
BXCracer
|
||||
c0nfidencal
|
||||
Can Kaya
|
||||
Carlos Alexandro Becker
|
||||
cassianoleal
|
||||
Cédric.h
|
||||
César Rubén
|
||||
chaoswizard
|
||||
Chen Tao
|
||||
chicha
|
||||
Chien Cheng Wei
|
||||
Christian Kopac
|
||||
Christian Widell
|
||||
Christoffer Brodd-Reijer
|
||||
christooss
|
||||
CityAceE
|
||||
Clopy
|
||||
Clusty
|
||||
cnu
|
||||
Commandant
|
||||
Constantinos Koniaris
|
||||
Coolmax
|
||||
cosmix
|
||||
Costin Chirvasuta
|
||||
CoVaLiDiTy
|
||||
cow_2001
|
||||
Crispin Kirchner
|
||||
crom
|
||||
Cruster
|
||||
Cybolic
|
||||
Dan Bishop
|
||||
Danek
|
||||
Dani
|
||||
Daniel Demarco
|
||||
Daniel Ferreira
|
||||
Daniel Frank
|
||||
Daniel Holm
|
||||
Daniel Høyer Iversen
|
||||
Daniel Marynicz
|
||||
Daniel Nylander
|
||||
Daniel Patriche
|
||||
Daniel Schildt
|
||||
Daniil Sorokin
|
||||
Dante Díaz
|
||||
Daria Michalska
|
||||
DarkenCZ
|
||||
Darren
|
||||
Daspah
|
||||
David Eurenius
|
||||
davidhjelm
|
||||
David Machakhelidze
|
||||
Dawid Dziurdzia
|
||||
Daya Adianto
|
||||
dcruz
|
||||
Deady
|
||||
Dereck Wonnacott
|
||||
Devgru
|
||||
Devid Antonio FiloniDevilDogTG
|
||||
di0rz`
|
||||
Dialecti Valsamou
|
||||
Diego Medeiros
|
||||
Dkzoffy
|
||||
Dmitrij D. Czarkoff
|
||||
Dmitriy Geels
|
||||
Dmitry Olyenyov
|
||||
Dominik Kozaczko
|
||||
Dominik Lübben
|
||||
doomster
|
||||
Dorota Król
|
||||
Doyen Philippe
|
||||
Dread Knight
|
||||
DreamSonic
|
||||
duan
|
||||
Duong Thanh An
|
||||
DvoglavaZver
|
||||
dwori
|
||||
dylansmrjones
|
||||
Ebuntor
|
||||
Edgar Alejandro Jarquin Flores
|
||||
Eetu
|
||||
ekerazha
|
||||
Elias Julkunen
|
||||
elparia
|
||||
Emberke
|
||||
Emiliano Goday Caneda
|
||||
EndelWar
|
||||
eng.essam
|
||||
enubuntu
|
||||
ercangun
|
||||
Erdal Ronahi
|
||||
ergin üresin
|
||||
Eric
|
||||
Éric Lassauge
|
||||
Erlend Finvåg
|
||||
Errdil
|
||||
ethan shalev
|
||||
Evgeni Spasov
|
||||
ezekielnin
|
||||
Fabian Ordelmans
|
||||
Fabio Mazanatti
|
||||
Fábio Nogueira
|
||||
FaCuZ
|
||||
Felipe Lerena
|
||||
Fernando Pereira
|
||||
fjetland
|
||||
Florian Schäfer
|
||||
FoBoS
|
||||
Folke
|
||||
Force
|
||||
fosk
|
||||
fragarray
|
||||
freddeg
|
||||
Frédéric Perrin
|
||||
Fredrik Kilegran
|
||||
FreeAtMind
|
||||
Fulvio Ciucci
|
||||
Gabor Kelemen
|
||||
Galatsanos Panagiotis
|
||||
Gaussian
|
||||
gdevitis
|
||||
Georg Brzyk
|
||||
George Dumitrescu
|
||||
Georgi Arabadjiev
|
||||
Georg Sieber
|
||||
Gerd Radecke
|
||||
Germán Heusdens
|
||||
Gianni Vialetto
|
||||
Gigih Aji Ibrahim
|
||||
Giorgio Wicklein
|
||||
Giovanni Rapagnani
|
||||
Giuseppe
|
||||
gl
|
||||
glen
|
||||
granjerox
|
||||
Green Fish
|
||||
greentea
|
||||
Greyhound
|
||||
G. U.
|
||||
Guillaume BENOIT
|
||||
Guillaume Pelletier
|
||||
Gustavo Henrique Klug
|
||||
gutocarvalho
|
||||
Guybrush88
|
||||
Hans Rødtang
|
||||
HardDisk
|
||||
Hargas Gábor
|
||||
Heitor Thury Barreiros Barbosa
|
||||
helios91940
|
||||
helix84
|
||||
Helton Rodrigues
|
||||
Hendrik Luup
|
||||
Henrique Ferreiro
|
||||
Henry Goury-Laffont
|
||||
Hezy Amiel
|
||||
hidro
|
||||
hoball
|
||||
hokten
|
||||
Holmsss
|
||||
hristo.num
|
||||
Hubert Życiński
|
||||
Hyo
|
||||
Iarwain
|
||||
ibe
|
||||
ibear
|
||||
Id2ndR
|
||||
Igor Zubarev
|
||||
IKON (Ion)
|
||||
imen
|
||||
Ionuț Jula
|
||||
Isabelle STEVANT
|
||||
István Nyitrai
|
||||
Ivan Petrovic
|
||||
Ivan Prignano
|
||||
IvaSerge
|
||||
jackmc
|
||||
Jacks0nxD
|
||||
Jack Shen
|
||||
Jacky Yeung
|
||||
Jacques Stadler
|
||||
Janek Thomaschewski
|
||||
Jan Kaláb
|
||||
Jan Niklas Hasse
|
||||
Jasper Groenewegen
|
||||
Javi Rodríguez
|
||||
Jayasimha (ಜಯಸಿಂಹ)
|
||||
jeannich
|
||||
Jeff Bailes
|
||||
Jesse Zilstorff
|
||||
Joan Duran
|
||||
João Santos
|
||||
Joar Bagge
|
||||
Joe Anderson
|
||||
Joel Calado
|
||||
Johan Linde
|
||||
John Garland
|
||||
Jojan
|
||||
jollyr0ger
|
||||
Jonas Bo Grimsgaard
|
||||
Jonas Granqvist
|
||||
Jonas Slivka
|
||||
Jonathan Zeppettini
|
||||
Jørgen
|
||||
Jørgen Tellnes
|
||||
josé
|
||||
José Geraldo Gouvêa
|
||||
José Iván León Islas
|
||||
José Lou C.
|
||||
Jose Sun
|
||||
Jr.
|
||||
Jukka Kauppinen
|
||||
Julián Alarcón
|
||||
julietgolf
|
||||
Jusic
|
||||
Justzupi
|
||||
Kaarel
|
||||
Kai Thomsen
|
||||
Kalman Tarnay
|
||||
Kamil Páral
|
||||
Kane_F
|
||||
kaotiks@gmail.com
|
||||
Kateikyoushii
|
||||
kaxhinaz
|
||||
Kazuhiro NISHIYAMA
|
||||
Kerberos
|
||||
Keresztes Ákos
|
||||
kevintyk
|
||||
kiersie
|
||||
Kimbo^
|
||||
Kim Lübbe
|
||||
kitzOgen
|
||||
Kjetil Rydland
|
||||
kluon
|
||||
kmikz
|
||||
Knedlyk
|
||||
koleoptero
|
||||
Kőrösi Krisztián
|
||||
Kouta
|
||||
Krakatos
|
||||
Krešo Kunjas
|
||||
kripken
|
||||
Kristaps
|
||||
Kristian Øllegaard
|
||||
Kristoffer Egil Bonarjee
|
||||
Krzysztof Janowski
|
||||
Krzysztof Zawada
|
||||
Larry Wei Liu
|
||||
laughterwym
|
||||
Laur Mõtus
|
||||
lazka
|
||||
leandrud
|
||||
lê bình
|
||||
Le Coz Florent
|
||||
Leo
|
||||
liorda
|
||||
LKRaider
|
||||
LoLo_SaG
|
||||
Long Tran
|
||||
Lorenz
|
||||
Low Kian Seong
|
||||
Luca Andrea Rossi
|
||||
Luca Ferretti
|
||||
Lucky LIX
|
||||
Luis Gomes
|
||||
Luis Reis
|
||||
Łukasz Wyszyński
|
||||
luojie-dune
|
||||
maaark
|
||||
Maciej Chojnacki
|
||||
Maciej Meller
|
||||
Mads Peter Rommedahl
|
||||
Major Kong
|
||||
Malaki
|
||||
malde
|
||||
Malte Lenz
|
||||
Mantas Kriaučiūnas
|
||||
Mara Sorella
|
||||
Marcin
|
||||
Marcin Falkiewicz
|
||||
marcobra
|
||||
Marco da Silva
|
||||
Marco de Moulin
|
||||
Marco Rodrigues
|
||||
Marcos
|
||||
Marcos Escalier
|
||||
Marcos Mobley
|
||||
Marcus Ekstrom
|
||||
Marek Dębowski
|
||||
Mário Buči
|
||||
Mario Munda
|
||||
Marius Andersen
|
||||
Marius Hudea
|
||||
Marius Mihai
|
||||
Mariusz Cielecki
|
||||
Mark Krapivner
|
||||
marko-markovic
|
||||
Markus Brummer
|
||||
Markus Sutter
|
||||
Martin
|
||||
Martin Dybdal
|
||||
Martin Iglesias
|
||||
Martin Lettner
|
||||
Martin Pihl
|
||||
Masoud Kalali
|
||||
mat02
|
||||
Matej Urbančič
|
||||
Mathias-K
|
||||
Mathieu Arès
|
||||
Mathieu D. (MatToufoutu)
|
||||
Mathijs
|
||||
Matrik
|
||||
Matteo Renzulli
|
||||
Matteo Settenvini
|
||||
Matthew Gadd
|
||||
Matthias Benkard
|
||||
Matthias Mailänder
|
||||
Mattias Ohlsson
|
||||
Mauro de Carvalho
|
||||
Max Molchanov
|
||||
Me
|
||||
MercuryCC
|
||||
Mert Bozkurt
|
||||
Mert Dirik
|
||||
MFX
|
||||
mhietar
|
||||
mibtha
|
||||
Michael Budde
|
||||
Michael Kaliszka
|
||||
Michalis Makaronides
|
||||
Michał Tokarczyk
|
||||
Miguel Pires da Rosa
|
||||
Mihai Capotă
|
||||
Miika Metsälä
|
||||
Mikael Fernblad
|
||||
Mike Sierra
|
||||
mikhalek
|
||||
Milan Prvulović
|
||||
Milo Casagrande
|
||||
Mindaugas
|
||||
Miroslav Matejaš
|
||||
misel
|
||||
mithras
|
||||
Mitja Pagon
|
||||
M.Kitchen
|
||||
Mohamed Magdy
|
||||
moonkey
|
||||
MrBlonde
|
||||
muczy
|
||||
Münir Ekinci
|
||||
Mustafa Temizel
|
||||
mvoncken
|
||||
Mytonn
|
||||
NagyMarton
|
||||
neaion
|
||||
Neil Lin
|
||||
Nemo
|
||||
Nerijus Arlauskas
|
||||
Nicklas Larsson
|
||||
Nicolaj Wyke
|
||||
Nicola Piovesan
|
||||
Nicolas Sabatier
|
||||
Nicolas Velin
|
||||
Nightfall
|
||||
NiKoB
|
||||
Nikolai M. Riabov
|
||||
Niko_Thien
|
||||
niska
|
||||
Nithir
|
||||
noisemonkey
|
||||
nomemohes
|
||||
nosense
|
||||
null
|
||||
Nuno Estêvão
|
||||
Nuno Santos
|
||||
nxxs
|
||||
nyo
|
||||
obo
|
||||
Ojan
|
||||
Olav Andreas Lindekleiv
|
||||
oldbeggar
|
||||
Olivier FAURAX
|
||||
orphe
|
||||
osantana
|
||||
Osman Tosun
|
||||
OssiR
|
||||
otypoks
|
||||
ounn
|
||||
Oz123
|
||||
Özgür BASKIN
|
||||
Pablo Carmona A.
|
||||
Pablo Ledesma
|
||||
Pablo Navarro Castillo
|
||||
Paco Molinero
|
||||
Pål-Eivind Johnsen
|
||||
pano
|
||||
Paolo Naldini
|
||||
Paracelsus
|
||||
Patryk13_03
|
||||
Patryk Skorupa
|
||||
PattogoTehen
|
||||
Paul Lange
|
||||
Pavcio
|
||||
Paweł Wysocki
|
||||
Pedro Brites Moita
|
||||
Pedro Clemente Pereira Neto
|
||||
Pekka "PEXI" Niemistö
|
||||
Penegal
|
||||
Penzo
|
||||
perdido
|
||||
Peter Kotrcka
|
||||
Peter Skov
|
||||
Peter Van den Bosch
|
||||
Petter Eklund
|
||||
Petter Viklund
|
||||
phatsphere
|
||||
Phenomen
|
||||
Philipi
|
||||
Philippides Homer
|
||||
phoenix
|
||||
pidi
|
||||
Pierre Quillery
|
||||
Pierre Rudloff
|
||||
Pierre Slamich
|
||||
Pietrao
|
||||
Piotr Strębski
|
||||
Piotr Wicijowski
|
||||
Pittmann Tamás
|
||||
Playmolas
|
||||
Prescott
|
||||
Prescott_SK
|
||||
pronull
|
||||
Przemysław Kulczycki
|
||||
Pumy
|
||||
pushpika
|
||||
PY
|
||||
qubicllj
|
||||
r21vo
|
||||
Rafał Barański
|
||||
rainofchaos
|
||||
Rajbir
|
||||
ras0ir
|
||||
Rat
|
||||
rd1381
|
||||
Renato
|
||||
Rene Hennig
|
||||
Rene Pärts
|
||||
Ricardo Duarte
|
||||
Richard
|
||||
Robert Hrovat
|
||||
Roberth Sjonøy
|
||||
Robert Lundmark
|
||||
Robin Jakobsson
|
||||
Robin Kåveland
|
||||
Rodrigo Donado
|
||||
Roel Groeneveld
|
||||
rohmaru
|
||||
Rolf Christensen
|
||||
Rolf Leggewie
|
||||
Roni Kantis
|
||||
Ronmi
|
||||
Rostislav Raykov
|
||||
royto
|
||||
RuiAmaro
|
||||
Rui Araújo
|
||||
Rui Moura
|
||||
Rune Svendsen
|
||||
Rusna
|
||||
Rytis
|
||||
Sabirov Mikhail
|
||||
salseeg
|
||||
Sami Koskinen
|
||||
Samir van de Sand
|
||||
Samuel Arroyo Acuña
|
||||
Samuel R. C. Vale
|
||||
Sanel
|
||||
Santi
|
||||
Santi Martínez Cantelli
|
||||
Sardan
|
||||
Sargate Kanogan
|
||||
Sarmad Jari
|
||||
Saša Bodiroža
|
||||
sat0shi
|
||||
Saulius Pranckevičius
|
||||
Savvas Radevic
|
||||
Sebastian Krauß
|
||||
Sebastián Porta
|
||||
Sedir
|
||||
Sefa Denizoğlu
|
||||
sekolands
|
||||
Selim Suerkan
|
||||
semsomi
|
||||
Sergii Golovatiuk
|
||||
setarcos
|
||||
Sheki
|
||||
Shironeko
|
||||
Shlomil
|
||||
silfiriel
|
||||
Simone Tolotti
|
||||
Simone Vendemia
|
||||
sirkubador
|
||||
Sławomir Więch
|
||||
slip
|
||||
slyon
|
||||
smoke
|
||||
Sonja
|
||||
spectral
|
||||
spin_555
|
||||
spitf1r3
|
||||
Spiziuz
|
||||
Spyros Theodoritsis
|
||||
SqUe
|
||||
Squigly
|
||||
srtck
|
||||
Stefan Horning
|
||||
Stefano Maggiolo
|
||||
Stefano Roberto Soleti
|
||||
steinberger
|
||||
Stéphane Travostino
|
||||
Stephan Klein
|
||||
Steven De Winter
|
||||
Stevie
|
||||
Stian24
|
||||
stylius
|
||||
Sukarn Maini
|
||||
Sunjae Park
|
||||
Susana Pereira
|
||||
szymon siglowy
|
||||
takercena
|
||||
TAS
|
||||
Taygeto
|
||||
temy4
|
||||
texxxxxx
|
||||
thamood
|
||||
Thanos Chatziathanassiou
|
||||
Tharawut Paripaiboon
|
||||
Theodoor
|
||||
Théophane Anestis
|
||||
Thor Marius K. Høgås
|
||||
Tiago Silva
|
||||
Tiago Sousa
|
||||
Tikkel
|
||||
tim__b
|
||||
Tim Bordemann
|
||||
Tim Fuchs
|
||||
Tim Kornhammar
|
||||
Timo
|
||||
Timo Jyrinki
|
||||
Timothy Babych
|
||||
TitkosRejtozo
|
||||
Tom
|
||||
Tomas Gustavsson
|
||||
Tomas Valentukevičius
|
||||
Tomasz Dominikowski
|
||||
Tomislav Plavčić
|
||||
Tom Mannerhagen
|
||||
Tommy Mikkelsen
|
||||
Tom Verdaat
|
||||
Tony Manco
|
||||
Tor Erling H. Opsahl
|
||||
Toudi
|
||||
tqm_z
|
||||
Trapanator
|
||||
Tribaal
|
||||
Triton
|
||||
TuniX12
|
||||
Tuomo Sipola
|
||||
turbojugend_gr
|
||||
Turtle.net
|
||||
twilight
|
||||
tymmej
|
||||
Ulrik
|
||||
Umarzuki Mochlis
|
||||
unikob
|
||||
Vadim Gusev
|
||||
Vagi
|
||||
Valentin Bora
|
||||
Valmantas Palikša
|
||||
VASKITTU
|
||||
Vassilis Skoullis
|
||||
vetal17
|
||||
vicedo
|
||||
viki
|
||||
villads hamann
|
||||
Vincent Garibal
|
||||
Vincent Ortalda
|
||||
vinchi007
|
||||
Vinícius de Figueiredo Silva
|
||||
Vinzenz Vietzke
|
||||
virtoo
|
||||
virtual_spirit
|
||||
Vitor Caike
|
||||
Vitor Lamas Gatti
|
||||
Vladimir Lazic
|
||||
Vladimir Sharshov
|
||||
Wanderlust
|
||||
Wander Nauta
|
||||
Ward De Ridder
|
||||
WebCrusader
|
||||
webdr
|
||||
Wentao Tang
|
||||
wilana
|
||||
Wilfredo Ernesto Guerrero Campos
|
||||
Wim Champagne
|
||||
World Sucks
|
||||
Xabi Ezpeleta
|
||||
Xavi de Moner
|
||||
XavierToo
|
||||
XChesser
|
||||
Xiaodong Xu
|
||||
xyb
|
||||
Yaron
|
||||
Yasen Pramatarov
|
||||
YesPoX
|
||||
Yuren Ju
|
||||
Yves MATHIEU
|
||||
zekopeko
|
||||
zhuqin
|
||||
Zissan
|
||||
Γιάννης Κατσαμπίρης
|
||||
Артём Попов
|
||||
Миша
|
||||
Шаймарданов Максим
|
||||
蔡查理
|
613
ChangeLog
@ -1,4 +1,595 @@
|
||||
=== Deluge 1.3.0 (In Development) ===
|
||||
=== Deluge 1.3.16 (unreleased) ===
|
||||
|
||||
==== Core ====
|
||||
* Fix saving copy of torrent file for magnet links.
|
||||
|
||||
=== Deluge 1.3.15 (12 May 2017) ===
|
||||
|
||||
==== Core ====
|
||||
* #2991: Fix issues with displaying libtorrent single proxy.
|
||||
* #3008: Fix libtorrent 1.2 trackers crashing Deluge UIs.
|
||||
* #2990: Fix error in torrent priorities causing file priority mismatch in UIs.
|
||||
|
||||
==== GtkUI ====
|
||||
* #3012: Configure gtkrc to use consistent button ordering on Windows.
|
||||
* Fix column sort state not saved in Thinclient mode.
|
||||
* #2786: Fix connection manager error with malformed ip.
|
||||
* #2866: Rename SystemTray/Indicator 'Pause/Resume All' to 'Pause/Resume Session'.
|
||||
* #2991: Workaround lt single proxy by greying out unused proxy types.
|
||||
|
||||
==== WebUI ====
|
||||
* Security Fix: Check render template files exist otherwise raise 404.
|
||||
|
||||
==== Notification Plugin ====
|
||||
* #2913: Fix webui passing string for int port value.
|
||||
|
||||
==== AutoAdd Plugin ====
|
||||
* Add WebUI preferences page detailing lack of configuration via WebUI.
|
||||
|
||||
==== Label Plugin ====
|
||||
* Add WebUI preferences page detailing how to configure plugin.
|
||||
|
||||
|
||||
=== Deluge 1.3.14 (6 March 2017) ===
|
||||
|
||||
==== Core ====
|
||||
* #2889: Fixed 'Too many files open' errors.
|
||||
* #2861: Added support for python-geoip for use with libtorrent 1.1.
|
||||
* #2149: Fixed a single proxy entry being overwritten resulting in no proxy set.
|
||||
|
||||
==== UI ====
|
||||
* Added tracker_status translation to UIs.
|
||||
|
||||
==== GtkUI ====
|
||||
* #2901: Strip whitespace from infohash before checks.
|
||||
* Add missed feature autofill infohash entry from clipboard.
|
||||
|
||||
==== WebUI ====
|
||||
* #1908: Backport bind interface option for server.
|
||||
* Security: Fixed WebUI CSRF Vulnerability.
|
||||
|
||||
==== ConsoleUI ====
|
||||
* [#2948] [Console] Fix decode error comparing non-ascii (str) torrent name.
|
||||
|
||||
==== AutoAdd Plugin ====
|
||||
* Fixes for splitting magnets from file.
|
||||
* Remove duplicate magnet extension when splitting.
|
||||
|
||||
|
||||
=== Deluge 1.3.13 (20 July 2016) ===
|
||||
==== Core ====
|
||||
* Increase RSA key size from 1024 to 2048 and use SHA256 digest.
|
||||
* Fixed empty error message from certain trackers.
|
||||
* Fixed torrent ending up displaying the wrong state.
|
||||
* #1032: Force a torrent into Error state if the resume data is rejected.
|
||||
* Workaround unwanted tracker announce when force rechecking paused torrent.
|
||||
* #2703: Stop moving torrent files if target files exist to prevent unintended clobbering of data.
|
||||
* #1330: Fixed the pausing and resuming of the Deluge session so torrents return to previous state.
|
||||
* #2765: Add support for TLS SNI in httpdownloader.
|
||||
* #2790: Ensure base32 magnet hash is uppercase to fix lowercase magnets uris.
|
||||
|
||||
==== Daemon ====
|
||||
* New command-line option to restict selected config key to read-only.
|
||||
* Allow use of uppercase log level to match UIs.
|
||||
|
||||
==== UI ====
|
||||
* #2832: Fixed error with blank lines in auth file.
|
||||
|
||||
==== GtkUI ====
|
||||
* Fixed installing plugin from a non-ascii directory.
|
||||
* Error'd torrents no longer display a progress percentage.
|
||||
* #2753: Fixed the 'Added' column showing the wrong date.
|
||||
* #2435: Prevent the user from changing tracker selection when editing trackers.
|
||||
* Fixed showing the wrong connected status with hostname in the Connection Manager.
|
||||
* #2754: Fixed the progress column to sort by progress and state correctly.
|
||||
* #2696: Fixed incorrect Move Completed folder shown in Options tab.
|
||||
* #2783: Sorting for name column is now case insensitive.
|
||||
* #2795: Reduce height of Add Torrent Dialog to help with smaller screeen resoltuions.
|
||||
* OSX: Fixed empty scrolling status (systray) menu.
|
||||
* OSX: Fixed starting deluged from connection manager.
|
||||
* #2093: Windows OS: Fixed opening non-ascii torrent files.
|
||||
* #2855: Fixed adding UDP trackers to trackers dialog.
|
||||
|
||||
==== WebUI ====
|
||||
* #2782: Fixed HTTPS negotiating incorrect cipher.
|
||||
* #2485: Fixed the broken Options context menu.
|
||||
* #2705: Fixed the hostlist config file not being created.
|
||||
* #2293: Fixed plugin's js code not loading when using the WebUI plugin.
|
||||
|
||||
==== Console ====
|
||||
* Fixed adding non-ascii torrent in non-interactive mode.
|
||||
* #2796: Add time_added to info sort keys.
|
||||
* #2815: Fixed 'add' cmd path inconsistency on Windows.
|
||||
|
||||
==== OSX Packaging ====
|
||||
* Source .py files no longer included in Deluge.app.
|
||||
|
||||
==== Windows OS Packaging ====
|
||||
* #2777: Updated MSVC SP1 check to latest release CLID.
|
||||
|
||||
==== Blocklist Plugin ====
|
||||
* #2729: Fixed plugin lockup with empty url.
|
||||
|
||||
==== Scheduler Plugin ====
|
||||
* Fixed corrupt plugin prefences page on OSX.
|
||||
* Fixed error accidentally introduced in 1.3.12.
|
||||
|
||||
==== Notification Plugin ====
|
||||
* #2402: Fixed the popup to show the actual count of files finished.
|
||||
* #2857: Fixed issue with SMTP port entry not updating in GTKUI.
|
||||
|
||||
==== AutoAdd Plugin ====
|
||||
* Fixed watch dir not accepting uppercase file extension.
|
||||
|
||||
==== Extractor Plugin ====
|
||||
* Ignore the remaining rar part files to prevent spawning useless processes.
|
||||
* #2785: Fixed only an empty folder when extracting rar files.
|
||||
|
||||
==== Execute Plugin ====
|
||||
* #2784: Windows OS: Escape ampersand in torrent args.
|
||||
|
||||
=== Deluge 1.3.12 (13 September 2015) ===
|
||||
==== GtkUI ====
|
||||
* #2731: Fix potential AttributeError in is_on_active_workspace
|
||||
|
||||
==== Core ====
|
||||
* Include fix for Twisted 15.0 URI class rename
|
||||
* #2233: Fix AttributeError in set_trackers with lt 1.0
|
||||
* Enable lt extension bindings again for versions >=0.16.7 (this disables Tracker Exchange by default)
|
||||
* Backport atomic fastresume and state file saving fixes as another attempt to prevent data loss on unclean exits
|
||||
|
||||
==== WebUI ====
|
||||
* Fixed i18n issue in Connection Manager which left users unable to connect
|
||||
* #2295: Increase cookie lifespan for display settings
|
||||
|
||||
==== Console ====
|
||||
* #2333: Fixed 'set and then get' in config command
|
||||
|
||||
==== Scheduler Plugin ====
|
||||
* Show current speed limit in statusbar
|
||||
|
||||
==== Win32 Packaging ====
|
||||
* #2736: Added version info to the properties of Deluge exes
|
||||
* #2734: Added a 256x256 to deluge.ico
|
||||
* #2325: Fixed the uninstaller deleting non-deluge files
|
||||
* Include pillow module to enable resizing of tracker icons
|
||||
|
||||
=== Deluge 1.3.11 (30 November 2014) ===
|
||||
==== GtkUI ====
|
||||
* Fixed ImportError for users with Twisted < 10
|
||||
* #2698: Fixed column issue when disabling a plugin
|
||||
|
||||
==== Core ====
|
||||
* Fixed cache issue with libtorrent 0.16 on Windows
|
||||
* #2555: Disabled use of SSLv3 protocol for DelugeRPC
|
||||
|
||||
==== WebUI ====
|
||||
* Modify SSL Context to allow >= TLSv1 protocol
|
||||
* #2588: Fixed Size column to show total_wanted instead of total_size
|
||||
|
||||
=== Deluge 1.3.10 (15 October 2014) ===
|
||||
==== GtkUI ====
|
||||
* #2256: Indexes are not updated correctly when removing column
|
||||
* Fix bug in the torrentview when Plugins added a column
|
||||
|
||||
==== WebUI ====
|
||||
* Security update for POODLE vulnerability
|
||||
|
||||
=== Deluge 1.3.9 (4 October 2014) ===
|
||||
==== GtkUI ====
|
||||
* #2514: Fix every torrent is displayed twice in classic mode
|
||||
|
||||
=== Deluge 1.3.8 (4 October 2014) ===
|
||||
==== Core ====
|
||||
* #1126 & #2322: Emit FinishedEvent after moving storage complete
|
||||
* Fixes to mitigate fastresume corruption
|
||||
|
||||
==== GtkUI ====
|
||||
* #2335: Fix application startup failing with 'cannot acquire lock' error
|
||||
* #2497: Fix the Queued Torrents 'Clear' button not properly clearing the list of torrent
|
||||
* #2496: Fix updating core_config before setting default options
|
||||
* #2493: Fix TypeError if active workspace is None
|
||||
* LP:#1168858: Nautilus window opens behind current window
|
||||
* Fix showing the open_folder menuitem
|
||||
* Suppress unimportant gnome warnings
|
||||
* Optimized the updating of the torrent view
|
||||
* Fixed Indicator icon label issue
|
||||
* Fix listview error with new config
|
||||
|
||||
==== WebUI ====
|
||||
* Ensure values are updated from config upon showing a plugin page
|
||||
|
||||
==== Extractor ====
|
||||
* Add WebUI plugin page
|
||||
* Find 7-zip application path on Windows via registry
|
||||
|
||||
==== Execute ====
|
||||
* #1290: Add a TorrentRemoved event option
|
||||
|
||||
==== Scheduler ====
|
||||
* #2238: Fix an 'undefined this.scheduleCells' error in javascript console
|
||||
|
||||
==== Notifications ====
|
||||
* #1310: Add WebUI plugin page
|
||||
|
||||
==== Blocklist ====
|
||||
* #2478: Add WebUI plugin page
|
||||
|
||||
==== Console ====
|
||||
* #2470: Fix console parsing args
|
||||
|
||||
=== Deluge 1.3.7 (9 July 2014) ===
|
||||
==== Core ====
|
||||
* #2324: Encryption level set by Deluge did not match libtorrent values
|
||||
* #2303: Torrent state was not updated until after emitting TorrentFinishedEvent
|
||||
* Fix twisted 13.1 compatability
|
||||
* #2343: Fix error if listen interface is whitespace
|
||||
* #2082: Validate ip address for listen_interface entry
|
||||
* #1490: Increase the Alertmanager interval to 0.3s
|
||||
* Prevent private flagged torrents auto-merging trackers
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix issue with Plugins that add Tab to torrentdetails
|
||||
* Fix the scalable icon install directory
|
||||
* #2335: Fix IPC lockfile issue preventing start of deluge-gtk
|
||||
* #2365: Fix hiding Progress column generating TypeError
|
||||
* #2371: Add StartupWMClass to desktop file
|
||||
* #2372: Fix the Ratio column not retaining position
|
||||
* #2369: Fix bypassing the password dialog when showing/quitting
|
||||
|
||||
==== WebUI ====
|
||||
* #2374: Fix right-click selection issue
|
||||
* #2310: Fix unicode password support
|
||||
* #2418: Fix WebUI error when adding non-ascii torrent
|
||||
|
||||
==== Windows OS ====
|
||||
* Allow silent uninstall for Windows package
|
||||
* #2367: Fix DelugeStart theme not showing Private Flag as ticked/checked
|
||||
* #2315: Potential fix for lost window issue
|
||||
|
||||
==== Extractor ====
|
||||
* #2290: Fix dotted filenames being rejected
|
||||
|
||||
=== Deluge 1.3.6 (25 Feburary 2013) ===
|
||||
==== Core ====
|
||||
* Catch & log KeyError when removing a torrent from the queued torrents set
|
||||
* Fix moving/renaming torrents issues when using libtorrent 0.16
|
||||
* Make sure queue order is preserved when restarting
|
||||
* #2160: Disable use of python bindings for libtorrent extensions and replace with session flag
|
||||
* #2163: Fix unable add torrent file with empty (0:) encoding tag
|
||||
* #2201: Fix error in authmanager if auth file has extra newlines
|
||||
* #2109: Fix the Proxy settings not being cleared by setting None
|
||||
* #2110: Fix accepting magnet uris with xt param anywhere within them
|
||||
* #2204: Fix daemon shutdown hang with large numbers of torrents
|
||||
* Fix prioritize first/last pieces option for magnet links
|
||||
|
||||
==== Client ====
|
||||
* Fix keyerrors after removing torrents from UIs
|
||||
|
||||
==== GtkUI ====
|
||||
* Add move completed option to add torrent dialog
|
||||
* Prevent jitter in torrent view
|
||||
* Fix torrent creation with non-ascii characters
|
||||
* Fix #2100 : Add option not to bring main window to front when adding torrents through ipcinterface
|
||||
* Add Quit Dialog when toggling classic mode in preferences and only show connection manager when not in classic mode.
|
||||
* #2169: Fix 'Download Location' in the Add Torrent Dialog not set correctly when folder typed into Other->Location field
|
||||
* #2171: Fix the Add Peer dialog not responding if empty or invalid values entered
|
||||
* #2104: Fix no title set for the appindicator
|
||||
* #2086: Fix submenus and icons for appindicator
|
||||
* #2146: Fix missing translations in View|Tabs submenu
|
||||
* Fix torrent names on libtorrent 0.16 on windows
|
||||
* #2147: Fix missing translations for plugin preferences page
|
||||
* #1474: Fix the on_show_prefs hook not being executed immediatly after enabling a plugin
|
||||
* #1946: Fix ReactorNotRestartable error when set as startup application
|
||||
* #2130: Fix same name can be given to different files in Add Torrent dialog
|
||||
* #2129: Fix empty filename able to be set in AddTorrent dialog
|
||||
* #2228: Fix Apply-To-All in AddTorrent Dialog copying file renames to other torrents
|
||||
* #2260: Fix the Add Torrent dialog also bringing the main window to active workspace
|
||||
* Fix showing exception error to user in Classic Mode with no libtorrent installed
|
||||
|
||||
==== Console ====
|
||||
* LP#1004793: Enable use of connect command in non-interactive mode
|
||||
* Ensure console commands are executed in order
|
||||
* #2065: Fix crash with missing closing quote
|
||||
* #1397: Add support for -s STATE in info command
|
||||
|
||||
==== WebUI ====
|
||||
* Add move completed option to add torrent dialog
|
||||
* #2112: Fix world readable tmp directory in json_api
|
||||
* #2069: Fix login window layout problem when using larger than default font size
|
||||
* #1890: Fix columns in files and peers view could use some spacing
|
||||
* #2103: Fix sorting by name is case-sensitive [sedulous]
|
||||
* #2120: Fix manually entered values not being saved in spinners
|
||||
* #2212: Fix unable to scroll in proxy preferences page
|
||||
* Fix autoconnecting to the default host
|
||||
* #2046: Fix plugins not enabling properly until after refreshing page
|
||||
* #2125: Fix plugin methods not being available when enabled until restart
|
||||
* #2085: Fix not showing torrents in sidebar for categories other than 'All' in classic mode
|
||||
* #2232: Fix flag icon path in Peers Tab missing deluge.config.base
|
||||
* Fix submenus closing upon mouse click
|
||||
* Add failed login log message, including IP address, to enable use with fail2ban
|
||||
* #2261: Fix Proxy settings not being saved in preferences
|
||||
|
||||
==== Windows OS ====
|
||||
* Hide the cmd windows when running deluged.exe or deluge-web.exe
|
||||
* Add deluged-debug.exe and deluge-web-debug.exe that still show the cmd window
|
||||
* Add gtk locale files to fix untranslated text
|
||||
* Fix the Open Folder option not working with non-ascii paths
|
||||
* Fix the daemon starting with config dir containing spaces
|
||||
* Fix Windows tray submenu items requiring right-click instead of left-click
|
||||
* Fix issue with adding some torrents with illegal characters via url in gtk client
|
||||
* #2240: Fix freespace issue with large capacity drives
|
||||
|
||||
==== OS X ====
|
||||
* Fix Open File/Folder option
|
||||
* Add OS X Menu for GTK Quartz
|
||||
|
||||
==== Execute ====
|
||||
* Fix execute plugin not working with unicode torrent names
|
||||
|
||||
==== Extractor ====
|
||||
* Add Windows support, using 7-zip
|
||||
* Added support for more extensions
|
||||
* Disabled extracting 'Move Completed' torrents due to race condition
|
||||
|
||||
=== Deluge 1.3.5 (09 April 2012) ===
|
||||
==== Core ====
|
||||
* Fix not properly detecting when torrent is at end of queue
|
||||
* #2049: Preserve order when moving multiple torrents in the queue
|
||||
|
||||
==== GtkUI ====
|
||||
* Modified fix for #1957, keyerror with non-acsii columns
|
||||
* Fix translation of items in Sidebar and Torrent Menu
|
||||
* #2052: Fix translation of Progress bar text
|
||||
* #2071: Fix KeyError in gtkui when file priority set to value '3'
|
||||
* #2064: Fix files treeview height in Create Dialog
|
||||
* Fix missing semi-colon in deluge.desktop
|
||||
* Disable setting file priorities for seeding torrents
|
||||
* Bring MainWindow to front when opening another instance
|
||||
|
||||
==== WebUI ====
|
||||
* #2050: Fix 'Up Speed' column not sorting
|
||||
* Hide unused Infohash button in WebUI
|
||||
|
||||
==== Label ====
|
||||
* Disable unusable items for 'All' in sidebar menu
|
||||
* Fix items for translation
|
||||
|
||||
==== Console ====
|
||||
* Fix prefixed space for tab completing commands
|
||||
* Fix missing trailing space for command options with tab complete
|
||||
|
||||
==== Blocklist ====
|
||||
* Use (documented) formatdate over format_date_time
|
||||
|
||||
=== Deluge 1.3.4 (03 March 2012) ===
|
||||
==== Core ====
|
||||
* #1921: Free disk space reporting incorrectly in FreeBSD
|
||||
* #1964: Fix unhandled UnpicklingErrors
|
||||
* #1967: Fix unhandled IndexError when trying to open a non-json conf file
|
||||
* Fix setting daemon listen interface from command line
|
||||
* #2021: Fix share ratio limit not obeyed for seeded torrents added to session
|
||||
* Add optparse custom version to prevent unnecessary loading of libtorrent
|
||||
* #1554: Fix seeding on share ratio failing for partially selected torrents
|
||||
* Add proper process title naming in ps, top etc. (Depends: setproctitle)
|
||||
|
||||
==== GtkUI ====
|
||||
* #1918: Fix Drag'n'Drop not working in Windows
|
||||
* #1941: Increase maximum Cache Size to 999999 (15GiB)
|
||||
* #1940: File & folder renaming issue when using Add Torrent dialog in Windows
|
||||
* LP#821577: Fix UnpicklingError when external selection dragged onto Files Tab
|
||||
* #1934: Fix Unicode error in AddTorrent Dialog
|
||||
* #1957: Fix keyerror when adding columns for non-latin languages
|
||||
* #1969: Fix menu item 'Quit & Shutdown' still available when not connected to daemon
|
||||
* #1895: Fix Files Tab showing wrong files due to torrent_info race condition
|
||||
* #2010: Move speed text in titlebar to the beginning
|
||||
* #2032: Wait for client to shutdown/disconnect before stopping reactor
|
||||
* Fix compatibility with Python 2.5
|
||||
* Fix collapsed treeview in Create Torrent dialog
|
||||
* Ignore unmaximise event when window isn't visible
|
||||
* #1976: Fixed text entry with trailing newline characters causing issues for Move Storage
|
||||
|
||||
==== WebUI ====
|
||||
* Fix Webui files-tab menu setting wrong priority
|
||||
* Update to ExtJS 3.4.0
|
||||
* #1960: Fix statustab showing total_payload_download for upload as well
|
||||
* Remove uneeded Titlebar to save space
|
||||
* Fix clipped Browse button in WebUI
|
||||
* #1915: Fix being unable to stop the status bar from autohiding
|
||||
* Fix password box focus issue in Firefox
|
||||
* Fix plugin uploads from behind a reverse proxy
|
||||
* #2010: Move speed text in titlebar to the beginning
|
||||
* #1936: Fix Referenced before assignment error in json_api
|
||||
* Changes are now applied when clicking OK in Preferences
|
||||
* Added Download,Uploaded,Down Limit, Up Limit & Seeder/Peeds columns
|
||||
* Add magnet uri support to Add Url
|
||||
* Add keymaps for torrents - Ctrl-A (select all) and Delete
|
||||
* #2037: Fix 'Add Torrents' torrents list not scrolling
|
||||
* #2038: Fix Chrome 17 disconnecting from webui
|
||||
|
||||
==== Console ====
|
||||
* #1953: Fix flickering on every update
|
||||
* #1954: Fix 'invalid literal for float' when setting listen interface
|
||||
* #1945: Fix UnicodeDecodeError when using non-ascii chars in info
|
||||
|
||||
==== Label ====
|
||||
* #1961: Add missing 'All' filter option
|
||||
* #2035: Fix label options dialog in webui
|
||||
* #2036: Fix newly added labels not being sorted in torrent right click menu
|
||||
|
||||
==== Notification ====
|
||||
* #1905: Fix no email sent to second email address
|
||||
* #1898: Fix email notifications not including date/time they were sent
|
||||
|
||||
==== Scheduler ====
|
||||
* Add plugin page for WebUi
|
||||
|
||||
==== Execute ====
|
||||
* Commands now run scripts asynchronous to prevent Deluge from hanging
|
||||
|
||||
==== AutoAdd ====
|
||||
* Added watch folder support for '.magnet' text file containing single or multiple magnet uris
|
||||
* Fix glade object issue when re-enabling plugin in same session
|
||||
* Fix plugin not showing as enabled in webui
|
||||
|
||||
=== Deluge 1.3.3 (22 July 2011) ===
|
||||
==== Core ====
|
||||
* Properly show the 'Checking Resume Data' state instead of just 7
|
||||
* #1788: Added ability to use XDG_DOWNLOAD_DIR as default download folder
|
||||
* Fix path error with torrent files prefixed with 'file://' from Firefox
|
||||
* #1869: Fix setting the disk io read/write to bypass OS cache in Windows
|
||||
* #1504: Fix win32 running deluged as not logged in user via runas or service
|
||||
* #890: If added torrent already exists, append extra trackers to it
|
||||
* #1338: Fix Seeds and Peers totals not updating
|
||||
* #1239: Fix translated Tracker Error text not counted in sidebar Error status
|
||||
* Fix httpdownloader error with existing filename
|
||||
* #1505: Add libtorrent info to version output
|
||||
* #1637 Fix UnicodeDecodeError from 'deluge-* --help' with non-english languages
|
||||
* #1714 Fix handling of backslashes when renaming files/folders
|
||||
|
||||
==== GtkUI ====
|
||||
* Show the checking icon for torrents in the 'Checking Resume Data' state
|
||||
* #1195: Fix right-click selecting issue when switching between folders and files
|
||||
* Add F2 key shortcut for renaming filenames in the Files Tab
|
||||
* Increase max piece size to 16 MiB in create torrent dialog
|
||||
* #1475: Fix save and restore Preferences dialog size from config
|
||||
* Add search as you type to the torrent view
|
||||
* #1456: Fix no ETA showing with multiple files
|
||||
* #1560: Fix FilesTab Progress value sorting by int instead of float
|
||||
* #1263: Fix not remembering column widths
|
||||
* #948: New Release Dialog now shows the server version
|
||||
* Fix peers in PeersTab showing non-zero download rate when seeding
|
||||
|
||||
==== AutoAdd ====
|
||||
* #1861: Fix AutoAdd Warning (column number is a boolean)
|
||||
|
||||
==== Label ====
|
||||
* #1246: Fix losing Labels upon restart
|
||||
|
||||
==== Execute ====
|
||||
* #1477: Fix ignore Added events from state file on startup
|
||||
|
||||
==== ConsoleUI ====
|
||||
* #1258: Add support for urls and magnet uris in add command
|
||||
* #1801: Fix unhandled defered error and missing error message upon failed connect
|
||||
|
||||
=== Deluge 1.3.2 (24 May 2011) ===
|
||||
==== Core ====
|
||||
* #1527: Fix Converting unicode to unicode error in move_storage
|
||||
* #1373: Fix creating and moving non-ascii folder names in MS Windows
|
||||
* #1507: Fix temporary file race condition in core/core.py:add_torrent_url
|
||||
* Fix a bug that can occur when upgrading 1.1 config files
|
||||
* #1517: Fix isohunt urls not loading
|
||||
* Handle redirection when adding a torrent by url
|
||||
* #1614: Fix autoadd matching a directory called "torrent"
|
||||
* #1742: Fix failure in Event handler prevents further emissions
|
||||
|
||||
==== GtkUI ====
|
||||
* #1514: Added Indicator Applet
|
||||
* #1494: Add torrent columns Downloaded and Uploaded
|
||||
* #1308: Add torrent column Seeds/Peers ratio
|
||||
* #1646: Add torrent columns for per torrent upload and download speed limits
|
||||
* Add missing icons for Trackers filter
|
||||
* Fix inconsistancies in the text for translation
|
||||
* #1510: Fix cannot create a torrent with only non-zero tier trackers
|
||||
* #1513: Fix unhandled Twisted Error in test_listen_port
|
||||
* #690: Fix renaming folders does not remove old empty folders
|
||||
* #1336: Fix uneeded horizontal scrollbar showing in Files & Peers Tab
|
||||
* #1508: Fix TypeError in cell_data_queue() could not convert argument to correct param type
|
||||
* #1498: Fix double slashes appearing when renaming
|
||||
* #1283: Fix consistent icons for Files tab
|
||||
* #1282: Text for AutoManaged changed to 'On/Off' and localized
|
||||
* Fix Up/Down buttons in Edit Trackers Dialog
|
||||
* Add Key Shortcuts for main menu functions
|
||||
|
||||
==== WebUI ====
|
||||
* #1194: Fix infinite login prompt in web ui through reverse proxy
|
||||
* #1355: Fix slow changing states in webUI
|
||||
* #1536: Fix Edit Trackers window not scrolling and not being resizable
|
||||
* #1799: Fix Missing textbox for "Move completed" in torrent options
|
||||
* #1562: Fix Javascript error in Web UI when re-opening preferences
|
||||
* #1567: Fix js from plugins does not work with different 'base' setting
|
||||
* #1268: Fix torrent errors not displayed in webui
|
||||
* #1323: Fix filter panels not scrollable
|
||||
* Fix file uploads from behind a reverse proxy.
|
||||
* #1333: Fix peer list doesn't update automatically
|
||||
* #1537: Fix editing trackers list, trackers have to be reselected
|
||||
|
||||
==== ConsoleUI ====
|
||||
* #755: Fix can't set listen_ports through console UI
|
||||
* #1500: Fix Console crashes on command longer than terminal width
|
||||
* #1248: Fix deluge-console unicode support on redirected stdout
|
||||
* Fix for deluge-console not adding torrent files on MS Windows
|
||||
* #1450: Fix trailing white space in paths
|
||||
* Misc: Updated help text for deluge-console on MS Windows
|
||||
* #1484: Fix trying to access the screen object when not using interactive mode
|
||||
* #1548: Fix cli argument processing
|
||||
* #1856: Add --sort option to info command
|
||||
* #1857: Add seeding_time, active_time and tracker_status to info command
|
||||
|
||||
==== Scheduler ====
|
||||
* #1506: Fix max speed not restored on a yellow->green transition
|
||||
|
||||
=== Deluge 1.3.1 (31 October 2010) ===
|
||||
==== Core ====
|
||||
* #1369: Fix non-ascii config folders not working in windows
|
||||
|
||||
==== GtkUI ====
|
||||
* #1365: Fix sidebar not updating show/hide trackers
|
||||
* #1247: Fix hang on quit
|
||||
|
||||
==== WebUI ====
|
||||
* #1364: Fix preferences not saving when the web ui plugin is enabled in classic mode
|
||||
* #1377: Fix bug when enabling plugins
|
||||
* #1370: Fix issues with preferences
|
||||
* #1312: Fix deluge-web using 100% CPU
|
||||
|
||||
=== Deluge 1.3.0 (18 September 2010) ===
|
||||
==== Core ====
|
||||
* Fix issue where the save_timer is cancelled when it's not active
|
||||
* Fix unhandled exception when adding a torrent to the session
|
||||
* Moved xdg import so it is not called on Windows, where it is unused. fixes #1343
|
||||
* Fix key error after enabling a plugin that introduces a new status key
|
||||
* Ignore global stop ratio related settings in logic, so per torrent ones are used.
|
||||
* Ensure preferencesmanager only changes intended libtorrent session settings.
|
||||
* Fix issue when adding torrents without a 'session'. This can happen when a plugin adds a torrent, like how the AutoAdd plugin works. The user that adds this torrent will be an empty string.
|
||||
* Add TorrentFileCompleted event
|
||||
|
||||
==== GtkUI ====
|
||||
* Increase max piece size to 8 MiB in create torrent dialog (closes #1358)
|
||||
|
||||
==== Scheduler ====
|
||||
* Add max active downloading and seeding options to scheduler.
|
||||
* Fix scheduler so that it keeps current state, even after global settings change.
|
||||
|
||||
==== AutoAdd ====
|
||||
* AutoAdd plugin can now recover when one of the watchfolders has an unhandled exception.
|
||||
* Fix bug in AutoAdd plugin where watchdirs would not display in gtkui when first enabled.
|
||||
* Fix bugs with unicode torrents in AutoAdd plugin.
|
||||
|
||||
=== Deluge 1.3.0-rc2 (20 August 2010) ===
|
||||
==== Core ====
|
||||
* Fix tracker_icons failing on windows
|
||||
* Fix #1302 an uncaught exception in an state_changed event handler in SessionProxy was preventing the TorrentManager's stop method from properly saving all the resume data
|
||||
* Fix issue with SessionProxy not updating the torrent status correctly when get_torrent_status calls take place within the cache_expiry time
|
||||
|
||||
==== ConsoleUI ====
|
||||
* #1307: Fix not being able to add torrents
|
||||
* #1293: Fix not being able to add paths that contain backslashes
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix uncaught exception when closing deluge in classic mode
|
||||
|
||||
==== Execute ====
|
||||
* #1306: Fix always executing last event
|
||||
|
||||
==== Label ====
|
||||
* Fix being able to remove labels in web ui
|
||||
|
||||
==== WebUI ====
|
||||
* #1319: Fix shift selecting in file trees
|
||||
|
||||
=== Deluge 1.3.0-rc1 (08 May 2010) ===
|
||||
==== Core ====
|
||||
* Implement #1063 option to delete torrent file copy on torrent removal - patch from Ghent
|
||||
* Implement #457 progress bars for folders
|
||||
@ -7,15 +598,33 @@
|
||||
* #1112: Fix renaming files in add torrent dialog
|
||||
* #1247: Fix deluge-gtk from hanging on shutdown
|
||||
* #995: Rewrote tracker_icons
|
||||
* Add AutoAdd plugin
|
||||
* Add Notifications plugin
|
||||
|
||||
==== GtkUI ====
|
||||
* Use new SessionProxy class for caching torrent status client-side
|
||||
* Use torrent status diffs to reduce RPC traffic
|
||||
|
||||
==== Blocklist ====
|
||||
* Implement local blocklist support
|
||||
* #861: Pause transfers until blocklist is imported
|
||||
* Fix redirection not working with relative paths
|
||||
|
||||
==== Execute ====
|
||||
* Fix running commands with the TorrentAdded event
|
||||
* Fix the web interface
|
||||
|
||||
==== Label ====
|
||||
* Fix the web interface (#733)
|
||||
|
||||
==== Web ====
|
||||
* Migrate to ExtJS 3.1
|
||||
* Add gzip compression of HTTP data to the server
|
||||
* Improve the efficiency of the TorrentGrid
|
||||
* Improve the efficiency of the TorrentGrid with lots of torrents (#1026)
|
||||
* Add a base parameter to allow reverse proxying (#1076)
|
||||
* Fix showing all the peers in the details tab (#1054)
|
||||
* Fix uploading torrent files in Opera or IE (#1087)
|
||||
* Complete IE support
|
||||
|
||||
=== Deluge 1.2.0 - "Bursting like an infected kidney" (10 January 2010) ===
|
||||
==== Core ====
|
||||
|
17
DEPENDS
@ -6,25 +6,28 @@
|
||||
* simplejson (if python < 2.6)
|
||||
* setuptools
|
||||
* gettext
|
||||
* intltool
|
||||
* pyxdg
|
||||
* chardet
|
||||
* geoip-database (optional)
|
||||
* setproctitle (optional)
|
||||
* pillow (optional)
|
||||
* python-geoip (optional)
|
||||
|
||||
* libtorrent >= 0.14, or build the included version
|
||||
* libtorrent (rasterbar) >= 0.14
|
||||
|
||||
* If building included libtorrent::
|
||||
* If building libtorrent:
|
||||
* boost >= 1.34.1
|
||||
* openssl
|
||||
* zlib
|
||||
|
||||
=== UIs ===
|
||||
* chardet
|
||||
|
||||
=== Gtk ===
|
||||
* python-notify (libnotify python wrapper)
|
||||
* pygame
|
||||
* pygtk >= 2.12
|
||||
* librsvg
|
||||
* xdg-utils
|
||||
* python-notify (optional)
|
||||
* pygame (optional)
|
||||
* python-appindicator (optional)
|
||||
|
||||
=== Web ===
|
||||
* mako
|
||||
|
71
README
@ -1,4 +1,3 @@
|
||||
|
||||
==========================
|
||||
Deluge BitTorrent Client
|
||||
==========================
|
||||
@ -9,86 +8,50 @@ Authors:
|
||||
Andrew Resch
|
||||
Damien Churchill
|
||||
|
||||
For past developers and contributers see: http://dev.deluge-torrent.org/wiki/About
|
||||
|
||||
==========================
|
||||
License
|
||||
==========================
|
||||
Deluge is under the GNU GPLv3 license.
|
||||
Icon data/pixmaps/deluge.svg and derivatives in data/icons are copyright
|
||||
Andrew Wedderburn and are under the GNU GPLv3.
|
||||
All other icons in data/pixmaps are copyright Andrew Resch and are under
|
||||
the GNU GPLv3.
|
||||
|
||||
==========================
|
||||
Contact/Support:
|
||||
==========================
|
||||
|
||||
We have two options available for support:
|
||||
|
||||
Our Forum, at: http://forum.deluge-torrent.org
|
||||
or
|
||||
Our IRC Channel, at #deluge on Freenode: http://freenode.net
|
||||
For contributors and past developers see:
|
||||
AUTHORS
|
||||
|
||||
==========================
|
||||
Installation Instructions:
|
||||
==========================
|
||||
|
||||
For more detailed instructions see: http://dev.deluge-torrent.org/wiki/Installing/Source
|
||||
For detailed instructions see: http://dev.deluge-torrent.org/wiki/Installing/Source
|
||||
|
||||
See: DEPENDS for a full list of dependencies.
|
||||
Ensure build dependencies are installed, see DEPENDS for a full listing.
|
||||
|
||||
First, make sure you have the proper build dependencies installed. On a normal
|
||||
Debian or Ubuntu system:
|
||||
Build and install by running:
|
||||
|
||||
sudo apt-get install g++ make python-all-dev python-all python-dbus \
|
||||
python-gtk2 python-notify librsvg2-common python-xdg python-support \
|
||||
subversion libboost-dev libboost-python-dev \
|
||||
libboost-thread-dev libboost-date-time-dev libboost-filesystem-dev \
|
||||
libssl-dev zlib1g-dev python-setuptools \
|
||||
python-mako python-twisted-web python-chardet python-simplejson
|
||||
$ python setup.py build
|
||||
$ sudo python setup.py install
|
||||
|
||||
The names of the packages may vary depending on your OS / distro.
|
||||
==========================
|
||||
Contact/Support:
|
||||
==========================
|
||||
|
||||
Once you have the needed libraries installed, build and install by running:
|
||||
|
||||
$ python setup.py build
|
||||
$ sudo python setup.py install
|
||||
Forum: http://forum.deluge-torrent.org
|
||||
IRC Channel: #deluge on irc.freenode.net
|
||||
|
||||
==========================
|
||||
FAQ
|
||||
==========================
|
||||
|
||||
For the full FAQ see: http://dev.deluge-torrent.org/wiki/Faq
|
||||
|
||||
How to start the various user-interfaces
|
||||
|
||||
Gtk:
|
||||
deluge-gtk
|
||||
deluge or deluge-gtk
|
||||
Console:
|
||||
deluge-console
|
||||
Web:
|
||||
deluge-web
|
||||
Go to http://localhost:8112/ default-password = "deluge"
|
||||
|
||||
Why is deluge still listed in my system tray even after I close it ?
|
||||
|
||||
You closed the gtk user-interface but you did not close the daemon. Choose "Quit & Shutdown Daemon" to close both Daemon and gtk-ui.
|
||||
|
||||
|
||||
How do I start the daemon?
|
||||
|
||||
deluged
|
||||
|
||||
How do I start the daemon with logging to console?
|
||||
|
||||
deluged -d -L <log level>
|
||||
|
||||
I can't connect to the daemon from another machine
|
||||
|
||||
* Configure the daemon to allow remote connections
|
||||
* Add a user to the auth file located in the config folder: ~/.config/deluge/auth
|
||||
* Restart the daemon.
|
||||
See: http://dev.deluge-torrent.org/wiki/UserGuide/ThinClient
|
||||
|
||||
I upgraded from 0.5 and plugin x is missing
|
||||
|
||||
1.0 is a rewrite, all old 0.5 plugins have to be rewritten.
|
||||
|
||||
For the full FAQ see: http://dev.deluge-torrent.org/wiki/Faq
|
||||
|
21
check_glade.sh
Executable file
@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Fixes glade files which may have set gtk stock labels set to translatable
|
||||
for x in `find . -name '*.glade' |grep -v '.git\|build'` ; do \
|
||||
for y in gtk-add gtk-apply gtk-bold gtk-cancel gtk-cdrom gtk-clear \
|
||||
gtk-close gtk-color-picker gtk-connect gtk-convert gtk-copy gtk-cut \
|
||||
gtk-delete gtk-dialog-error gtk-dialog-info gtk-dialog-question \
|
||||
gtk-dialog-warning gtk-dnd gtk-dnd-multiple gtk-edit gtk-execute gtk-find \
|
||||
gtk-find-and-replace gtk-floppy gtk-goto-bottom gtk-goto-first \
|
||||
gtk-goto-last gtk-goto-top gtk-go-back gtk-go-down gtk-go-forward \
|
||||
gtk-go-up gtk-help gtk-home gtk-index gtk-italic gtk-jump-to \
|
||||
gtk-justify-center gtk-justify-fill gtk-justify-left gtk-missing-image \
|
||||
gtk-new gtk-no gtk-ok gtk-open gtk-paste gtk-preferences gtk-print \
|
||||
gtk-print-preview gtk-properties gtk-quit gtk-redo gtk-refresh \
|
||||
gtk-remove gtk-revert-to-saved gtk-save gtk-save-as gtk-select-color \
|
||||
gtk-select-font gtk-sort-descending gtk-spell-check gtk-stop \
|
||||
gtk-strikethrough gtk-undelete gtk-underline gtk-undo gtk-yes \
|
||||
gtk-zoom-100 gtk-zoom-fit gtk-zoom-in gtk-zoom-out; do \
|
||||
sed -i "s/<property\ name\=\"label\"\ translatable\=\"yes\">$y<\/property>/<property\ name\=\"label\"\ translatable\=\"no\">$y<\/property>/g" $x; \
|
||||
done;\
|
||||
done
|
17
create_potfiles_in.py
Normal file → Executable file
@ -1,17 +1,26 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
|
||||
import re
|
||||
import sys
|
||||
# Paths to exclude
|
||||
EXCLUSIONS = [
|
||||
"deluge/scripts"
|
||||
"deluge/scripts",
|
||||
"deluge/i18n",
|
||||
]
|
||||
|
||||
POTFILE_IN = "deluge/i18n/POTFILES.in"
|
||||
|
||||
print "Creating " + POTFILE_IN + " .."
|
||||
pattern = "deluge\/plugins\/.*\/build"
|
||||
compiled = re.compile(pattern)
|
||||
|
||||
sys.stdout.write("Creating " + POTFILE_IN + " ... ")
|
||||
sys.stdout.flush()
|
||||
to_translate = []
|
||||
for (dirpath, dirnames, filenames) in os.walk("deluge"):
|
||||
for filename in filenames:
|
||||
if os.path.splitext(filename)[1] in (".py", ".glade") and dirpath not in EXCLUSIONS:
|
||||
if os.path.splitext(filename)[1] in (".py", ".glade", ".in") \
|
||||
and dirpath not in EXCLUSIONS \
|
||||
and not compiled.match(dirpath):
|
||||
to_translate.append(os.path.join(dirpath, filename))
|
||||
|
||||
f = open(POTFILE_IN, "wb")
|
||||
|
@ -47,7 +47,7 @@ supports.
|
||||
|
||||
REQUIRED_VERSION = "0.14.9.0"
|
||||
|
||||
def check_version(LT):
|
||||
def check_version(lt):
|
||||
from deluge.common import VersionSplit
|
||||
if VersionSplit(lt.version) < VersionSplit(REQUIRED_VERSION):
|
||||
raise ImportError("This version of Deluge requires libtorrent >=%s!" % REQUIRED_VERSION)
|
||||
|
276
deluge/common.py
@ -17,9 +17,9 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with deluge. If not, write to:
|
||||
# The Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor
|
||||
# Boston, MA 02110-1301, USA.
|
||||
# The Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# In addition, as a special exception, the copyright holders give
|
||||
# permission to link the code of portions of this program with the OpenSSL
|
||||
@ -36,17 +36,26 @@
|
||||
|
||||
"""Common functions for various parts of Deluge to use."""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import os
|
||||
import time
|
||||
import subprocess
|
||||
import platform
|
||||
import sys
|
||||
import chardet
|
||||
import pkg_resources
|
||||
import gettext
|
||||
import locale
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
from deluge.error import *
|
||||
from deluge.log import LOG as log
|
||||
|
||||
# Do a little hack here just in case the user has json-py installed since it
|
||||
# has a different api
|
||||
if not hasattr(json, "dumps"):
|
||||
@ -62,29 +71,19 @@ if not hasattr(json, "dumps"):
|
||||
json.dump = dump
|
||||
json.load = load
|
||||
|
||||
import pkg_resources
|
||||
import xdg, xdg.BaseDirectory
|
||||
import gettext
|
||||
import locale
|
||||
|
||||
# Initialize gettext
|
||||
try:
|
||||
if hasattr(locale, "bindtextdomain"):
|
||||
locale.bindtextdomain("deluge", pkg_resources.resource_filename("deluge", "i18n"))
|
||||
if hasattr(locale, "textdomain"):
|
||||
locale.textdomain("deluge")
|
||||
gettext.bindtextdomain("deluge", pkg_resources.resource_filename("deluge", "i18n"))
|
||||
gettext.textdomain("deluge")
|
||||
gettext.install("deluge", pkg_resources.resource_filename("deluge", "i18n"))
|
||||
gettext.install("deluge", pkg_resources.resource_filename("deluge", "i18n"), unicode=True)
|
||||
except Exception, e:
|
||||
from deluge.log import LOG as log
|
||||
log.error("Unable to initialize gettext/locale!")
|
||||
log.exception(e)
|
||||
import __builtin__
|
||||
__builtin__.__dict__["_"] = lambda x: x
|
||||
|
||||
from deluge.error import *
|
||||
|
||||
LT_TORRENT_STATE = {
|
||||
"Queued": 0,
|
||||
"Checking": 1,
|
||||
@ -104,7 +103,6 @@ LT_TORRENT_STATE = {
|
||||
7: "Checking Resume Data"
|
||||
}
|
||||
|
||||
|
||||
TORRENT_STATE = [
|
||||
"Allocating",
|
||||
"Checking",
|
||||
@ -119,11 +117,15 @@ FILE_PRIORITY = {
|
||||
0: "Do Not Download",
|
||||
1: "Normal Priority",
|
||||
2: "High Priority",
|
||||
5: "Highest Priority",
|
||||
3: "High Priority",
|
||||
4: "High Priority",
|
||||
5: "High Priority",
|
||||
6: "High Priority",
|
||||
7: "Highest Priority",
|
||||
"Do Not Download": 0,
|
||||
"Normal Priority": 1,
|
||||
"High Priority": 2,
|
||||
"Highest Priority": 5
|
||||
"High Priority": 5,
|
||||
"Highest Priority": 7
|
||||
}
|
||||
|
||||
def get_version():
|
||||
@ -145,15 +147,27 @@ def get_default_config_dir(filename=None):
|
||||
|
||||
"""
|
||||
if windows_check():
|
||||
appDataPath = os.environ.get("APPDATA")
|
||||
if not appDataPath:
|
||||
import _winreg
|
||||
hkey = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders")
|
||||
appDataReg = _winreg.QueryValueEx(hkey, "AppData")
|
||||
appDataPath = appDataReg[0]
|
||||
_winreg.CloseKey(hkey)
|
||||
if filename:
|
||||
return os.path.join(os.environ.get("APPDATA"), "deluge", filename)
|
||||
return os.path.join(appDataPath, "deluge", filename)
|
||||
else:
|
||||
return os.path.join(os.environ.get("APPDATA"), "deluge")
|
||||
return os.path.join(appDataPath, "deluge")
|
||||
else:
|
||||
if filename:
|
||||
return os.path.join(xdg.BaseDirectory.save_config_path("deluge"), filename)
|
||||
else:
|
||||
return xdg.BaseDirectory.save_config_path("deluge")
|
||||
from xdg.BaseDirectory import save_config_path
|
||||
try:
|
||||
if filename:
|
||||
return os.path.join(save_config_path("deluge"), filename)
|
||||
else:
|
||||
return save_config_path("deluge")
|
||||
except OSError, e:
|
||||
log.error("Unable to use default config directory, exiting... (%s)", e)
|
||||
sys.exit(1)
|
||||
|
||||
def get_default_download_dir():
|
||||
"""
|
||||
@ -161,10 +175,21 @@ def get_default_download_dir():
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
if windows_check():
|
||||
return os.path.expanduser("~")
|
||||
else:
|
||||
return os.environ.get("HOME")
|
||||
download_dir = ""
|
||||
if not windows_check():
|
||||
from xdg.BaseDirectory import xdg_config_home
|
||||
try:
|
||||
with open(os.path.join(xdg_config_home, 'user-dirs.dirs'), 'r') as _file:
|
||||
for line in _file:
|
||||
if not line.startswith('#') and line.startswith('XDG_DOWNLOAD_DIR'):
|
||||
download_dir = os.path.expandvars(line.partition("=")[2].rstrip().strip('"'))
|
||||
break
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
if not download_dir:
|
||||
download_dir = os.path.join(os.path.expanduser("~"), 'Downloads')
|
||||
return download_dir
|
||||
|
||||
def windows_check():
|
||||
"""
|
||||
@ -209,18 +234,27 @@ def get_pixmap(fname):
|
||||
return pkg_resources.resource_filename("deluge", os.path.join("data", \
|
||||
"pixmaps", fname))
|
||||
|
||||
def open_file(path):
|
||||
def open_file(path, timestamp=None):
|
||||
"""
|
||||
Opens a file or folder using the system configured program
|
||||
|
||||
:param path: the path to the file or folder to open
|
||||
:type path: string
|
||||
:param timestamp: the timestamp of the event that requested to open
|
||||
:type timestamp: int
|
||||
|
||||
"""
|
||||
if windows_check():
|
||||
os.startfile("%s" % path)
|
||||
os.startfile(path.decode("utf8"))
|
||||
elif osx_check():
|
||||
subprocess.Popen(["open", "%s" % path])
|
||||
else:
|
||||
subprocess.Popen(["xdg-open", "%s" % path])
|
||||
if timestamp is None:
|
||||
timestamp = int(time.time())
|
||||
env = os.environ.copy()
|
||||
env["DESKTOP_STARTUP_ID"] = "%s-%u-%s-xdg_open_TIME%d" % \
|
||||
(os.path.basename(sys.argv[0]), os.getpid(), os.uname()[1], timestamp)
|
||||
subprocess.Popen(["xdg-open", "%s" % path], env=env)
|
||||
|
||||
def open_url_in_browser(url):
|
||||
"""
|
||||
@ -259,6 +293,30 @@ def fsize(fsize_b):
|
||||
fsize_gb = fsize_mb / 1024.0
|
||||
return "%.1f %s" % (fsize_gb, _("GiB"))
|
||||
|
||||
def fsize_short(fsize_b):
|
||||
"""
|
||||
Formats the bytes value into a string with K, M or G units
|
||||
|
||||
:param fsize_b: the filesize in bytes
|
||||
:type fsize_b: int
|
||||
:returns: formatted string in K, M or G units
|
||||
:rtype: string
|
||||
|
||||
**Usage**
|
||||
|
||||
>>> fsize(112245)
|
||||
'109.6 K'
|
||||
|
||||
"""
|
||||
fsize_kb = fsize_b / 1024.0
|
||||
if fsize_kb < 1024:
|
||||
return "%.1f %s" % (fsize_kb, _("K"))
|
||||
fsize_mb = fsize_kb / 1024.0
|
||||
if fsize_mb < 1024:
|
||||
return "%.1f %s" % (fsize_mb, _("M"))
|
||||
fsize_gb = fsize_mb / 1024.0
|
||||
return "%.1f %s" % (fsize_gb, _("G"))
|
||||
|
||||
def fpcnt(dec):
|
||||
"""
|
||||
Formats a string to display a percentage with two decimal places
|
||||
@ -291,7 +349,14 @@ def fspeed(bps):
|
||||
'42.1 KiB/s'
|
||||
|
||||
"""
|
||||
return '%s/s' % (fsize(bps))
|
||||
fspeed_kb = bps / 1024.0
|
||||
if fspeed_kb < 1024:
|
||||
return "%.1f %s" % (fspeed_kb, _("KiB/s"))
|
||||
fspeed_mb = fspeed_kb / 1024.0
|
||||
if fspeed_mb < 1024:
|
||||
return "%.1f %s" % (fspeed_mb, _("MiB/s"))
|
||||
fspeed_gb = fspeed_mb / 1024.0
|
||||
return "%.1f %s" % (fspeed_gb, _("GiB/s"))
|
||||
|
||||
def fpeer(num_peers, total_peers):
|
||||
"""
|
||||
@ -402,7 +467,9 @@ def is_magnet(uri):
|
||||
True
|
||||
|
||||
"""
|
||||
if uri[:20] == "magnet:?xt=urn:btih:":
|
||||
magnet_scheme = 'magnet:?'
|
||||
xt_param = 'xt=urn:btih:'
|
||||
if uri.startswith(magnet_scheme) and xt_param in uri:
|
||||
return True
|
||||
return False
|
||||
|
||||
@ -466,16 +533,15 @@ def free_space(path):
|
||||
:raises InvalidPathError: if the path is not valid
|
||||
|
||||
"""
|
||||
if not os.path.exists(path):
|
||||
if not path or not os.path.exists(path):
|
||||
raise InvalidPathError("%s is not a valid path" % path)
|
||||
|
||||
if windows_check():
|
||||
import win32file
|
||||
sectors, bytes, free, total = map(long, win32file.GetDiskFreeSpace(path))
|
||||
return (free * sectors * bytes)
|
||||
from win32file import GetDiskFreeSpaceEx
|
||||
return GetDiskFreeSpaceEx(path)[0]
|
||||
else:
|
||||
disk_data = os.statvfs(path)
|
||||
block_size = disk_data.f_bsize
|
||||
disk_data = os.statvfs(path.encode("utf8"))
|
||||
block_size = disk_data.f_frsize
|
||||
return disk_data.f_bavail * block_size
|
||||
|
||||
def is_ip(ip):
|
||||
@ -496,15 +562,23 @@ def is_ip(ip):
|
||||
import socket
|
||||
#first we test ipv4
|
||||
try:
|
||||
if socket.inet_pton(socket.AF_INET, "%s" % (ip)):
|
||||
return True
|
||||
if windows_check():
|
||||
if socket.inet_aton("%s" % (ip)):
|
||||
return True
|
||||
else:
|
||||
if socket.inet_pton(socket.AF_INET, "%s" % (ip)):
|
||||
return True
|
||||
except socket.error:
|
||||
if not socket.has_ipv6:
|
||||
return False
|
||||
#now test ipv6
|
||||
try:
|
||||
if socket.inet_pton(socket.AF_INET6, "%s" % (ip)):
|
||||
if windows_check():
|
||||
log.warning("ipv6 check unavailable on windows")
|
||||
return True
|
||||
else:
|
||||
if socket.inet_pton(socket.AF_INET6, "%s" % (ip)):
|
||||
return True
|
||||
except socket.error:
|
||||
return False
|
||||
|
||||
@ -526,7 +600,7 @@ def path_join(*parts):
|
||||
path += '/' + part
|
||||
return path
|
||||
|
||||
XML_ESCAPES = (
|
||||
XML_ESCAPES = (
|
||||
('&', '&'),
|
||||
('<', '<'),
|
||||
('>', '>'),
|
||||
@ -535,9 +609,9 @@ XML_ESCAPES = (
|
||||
)
|
||||
|
||||
def xml_decode(string):
|
||||
"""
|
||||
"""
|
||||
Unescape a string that was previously encoded for use within xml.
|
||||
|
||||
|
||||
:param string: The string to escape
|
||||
:type string: string
|
||||
:returns: The unescaped version of the string.
|
||||
@ -548,9 +622,9 @@ def xml_decode(string):
|
||||
return string
|
||||
|
||||
def xml_encode(string):
|
||||
"""
|
||||
"""
|
||||
Escape a string for use within an xml element or attribute.
|
||||
|
||||
|
||||
:param string: The string to escape
|
||||
:type string: string
|
||||
:returns: An escaped version of the string.
|
||||
@ -560,6 +634,59 @@ def xml_encode(string):
|
||||
string = string.replace(char, escape)
|
||||
return string
|
||||
|
||||
def decode_string(s, encoding="utf8"):
|
||||
"""
|
||||
Decodes a string and return unicode. If it cannot decode using
|
||||
`:param:encoding` then it will try latin1, and if that fails,
|
||||
try to detect the string encoding. If that fails, decode with
|
||||
ignore.
|
||||
|
||||
:param s: string to decode
|
||||
:type s: string
|
||||
:keyword encoding: the encoding to use in the decoding
|
||||
:type encoding: string
|
||||
:returns: s converted to unicode
|
||||
:rtype: unicode
|
||||
|
||||
"""
|
||||
if not s:
|
||||
return u''
|
||||
elif isinstance(s, unicode):
|
||||
return s
|
||||
|
||||
encodings = [lambda: ("utf8", 'strict'),
|
||||
lambda: ("iso-8859-1", 'strict'),
|
||||
lambda: (chardet.detect(s)["encoding"], 'strict'),
|
||||
lambda: (encoding, 'ignore')]
|
||||
|
||||
if not encoding is "utf8":
|
||||
encodings.insert(0, lambda: (encoding, 'strict'))
|
||||
|
||||
for l in encodings:
|
||||
try:
|
||||
return s.decode(*l())
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
return u''
|
||||
|
||||
def utf8_encoded(s, encoding="utf8"):
|
||||
"""
|
||||
Returns a utf8 encoded string of s
|
||||
|
||||
:param s: (unicode) string to (re-)encode
|
||||
:type s: basestring
|
||||
:keyword encoding: the encoding to use in the decoding
|
||||
:type encoding: string
|
||||
:returns: a utf8 encoded string of s
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
if isinstance(s, str):
|
||||
s = decode_string(s, encoding).encode("utf8")
|
||||
elif isinstance(s, unicode):
|
||||
s = s.encode("utf8")
|
||||
return s
|
||||
|
||||
class VersionSplit(object):
|
||||
"""
|
||||
Used for comparing version numbers.
|
||||
@ -570,13 +697,15 @@ class VersionSplit(object):
|
||||
"""
|
||||
def __init__(self, ver):
|
||||
ver = ver.lower()
|
||||
vs = ver.split("_") if "_" in ver else ver.split("-")
|
||||
self.version = [int(x) for x in vs[0].split(".")]
|
||||
vs = ver.replace("_", "-").split("-")
|
||||
self.version = [int(x) for x in vs[0].split(".") if x.isdigit()]
|
||||
self.suffix = None
|
||||
self.dev = False
|
||||
if len(vs) > 1:
|
||||
for s in ("rc", "alpha", "beta", "dev"):
|
||||
if s in vs[1][:len(s)]:
|
||||
self.suffix = vs[1]
|
||||
if vs[1].startswith(("rc", "alpha", "beta")):
|
||||
self.suffix = vs[1]
|
||||
if vs[-1] == 'dev':
|
||||
self.dev = True
|
||||
|
||||
def __cmp__(self, ver):
|
||||
"""
|
||||
@ -587,19 +716,32 @@ class VersionSplit(object):
|
||||
|
||||
"""
|
||||
|
||||
if self.version > ver.version or (self.suffix and self.suffix[:3] == "dev"):
|
||||
return 1
|
||||
if self.version < ver.version:
|
||||
return -1
|
||||
# If there is no suffix we use z because we want final
|
||||
# to appear after alpha, beta, and rc alphabetically.
|
||||
v1 = [self.version, self.suffix or 'z', self.dev]
|
||||
v2 = [ver.version, ver.suffix or 'z', ver.dev]
|
||||
return cmp(v1, v2)
|
||||
|
||||
if self.version == ver.version:
|
||||
if self.suffix == ver.suffix:
|
||||
return 0
|
||||
if self.suffix is None:
|
||||
return 1
|
||||
if ver.suffix is None:
|
||||
return -1
|
||||
if self.suffix < ver.suffix:
|
||||
return -1
|
||||
if self.suffix > ver.suffix:
|
||||
return 1
|
||||
def win32_unicode_argv():
|
||||
""" Gets sys.argv as list of unicode objects on any platform."""
|
||||
if windows_check():
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on Windows, with the
|
||||
# underlying Windows API instead replacing multi-byte characters with '?'.
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
get_cmd_linew = cdll.kernel32.GetCommandLineW
|
||||
get_cmd_linew.argtypes = []
|
||||
get_cmd_linew.restype = LPCWSTR
|
||||
|
||||
cmdline_to_argvw = windll.shell32.CommandLineToArgvW
|
||||
cmdline_to_argvw.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
cmdline_to_argvw.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = get_cmd_linew()
|
||||
argc = c_int(0)
|
||||
argv = cmdline_to_argvw(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in xrange(start, argc.value)]
|
||||
|
@ -96,6 +96,10 @@ class Component(object):
|
||||
self._component_stopping_deferred = None
|
||||
_ComponentRegistry.register(self)
|
||||
|
||||
def __del__(self):
|
||||
if _ComponentRegistry:
|
||||
_ComponentRegistry.deregister(self._component_name)
|
||||
|
||||
def _component_start_timer(self):
|
||||
if hasattr(self, "update"):
|
||||
self._component_timer = LoopingCall(self.update)
|
||||
@ -139,11 +143,18 @@ class Component(object):
|
||||
self._component_timer.stop()
|
||||
return True
|
||||
|
||||
def on_stop_fail(result):
|
||||
self._component_state = "Started"
|
||||
self._component_stopping_deferred = None
|
||||
log.error(result)
|
||||
return result
|
||||
|
||||
if self._component_state != "Stopped" and self._component_state != "Stopping":
|
||||
if hasattr(self, "stop"):
|
||||
self._component_state = "Stopping"
|
||||
d = maybeDeferred(self.stop)
|
||||
d.addCallback(on_stop)
|
||||
d.addErrback(on_stop_fail)
|
||||
self._component_stopping_deferred = d
|
||||
else:
|
||||
d = maybeDeferred(on_stop, None)
|
||||
@ -192,6 +203,18 @@ class Component(object):
|
||||
d.addCallback(on_stop)
|
||||
return d
|
||||
|
||||
def start(self):
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
pass
|
||||
|
||||
def update(self):
|
||||
pass
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
class ComponentRegistry(object):
|
||||
"""
|
||||
The ComponentRegistry holds a list of currently registered
|
||||
|
@ -45,9 +45,9 @@ The format of the config file is two json encoded dicts:
|
||||
<version dict>
|
||||
<content dict>
|
||||
|
||||
The version dict contains two keys: file and format. The format version is
|
||||
controlled by the Config class. It should only be changed when anything below
|
||||
it is changed directly by the Config class. An example of this would be if we
|
||||
The version dict contains two keys: file and format. The format version is
|
||||
controlled by the Config class. It should only be changed when anything below
|
||||
it is changed directly by the Config class. An example of this would be if we
|
||||
changed the serializer for the content to something different.
|
||||
|
||||
The config file version is changed by the 'owner' of the config file. This is
|
||||
@ -67,6 +67,8 @@ version as this will be done internally.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import cPickle as pickle
|
||||
import shutil
|
||||
import os
|
||||
@ -93,13 +95,13 @@ def prop(func):
|
||||
def find_json_objects(s):
|
||||
"""
|
||||
Find json objects in a string.
|
||||
|
||||
|
||||
:param s: the string to find json objects in
|
||||
:type s: string
|
||||
|
||||
|
||||
:returns: a list of tuples containing start and end locations of json objects in the string `s`
|
||||
:rtype: [(start, end), ...]
|
||||
|
||||
|
||||
"""
|
||||
objects = []
|
||||
opens = 0
|
||||
@ -109,8 +111,13 @@ def find_json_objects(s):
|
||||
if start < 0:
|
||||
return []
|
||||
|
||||
quoted = False
|
||||
for index, c in enumerate(s[offset:]):
|
||||
if c == "{":
|
||||
if c == '"':
|
||||
quoted = not quoted
|
||||
elif quoted:
|
||||
continue
|
||||
elif c == "{":
|
||||
opens += 1
|
||||
elif c == "}":
|
||||
opens -= 1
|
||||
@ -119,8 +126,8 @@ def find_json_objects(s):
|
||||
start = index + offset + 1
|
||||
|
||||
return objects
|
||||
|
||||
|
||||
|
||||
|
||||
class Config(object):
|
||||
"""
|
||||
This class is used to access/create/modify config files
|
||||
@ -146,7 +153,8 @@ class Config(object):
|
||||
self._save_timer = None
|
||||
|
||||
if defaults:
|
||||
self.__config = dict(defaults)
|
||||
for key, value in defaults.iteritems():
|
||||
self.set_item(key, value)
|
||||
|
||||
# Load the config from file in the config_dir
|
||||
if config_dir:
|
||||
@ -187,6 +195,10 @@ what is currently in the config and it could not convert the value
|
||||
5
|
||||
|
||||
"""
|
||||
if isinstance(value, basestring):
|
||||
value = deluge.common.utf8_encoded(value)
|
||||
|
||||
|
||||
if not self.__config.has_key(key):
|
||||
self.__config[key] = value
|
||||
log.debug("Setting '%s' to %s of %s", key, value, type(value))
|
||||
@ -200,7 +212,10 @@ what is currently in the config and it could not convert the value
|
||||
|
||||
if value is not None and oldtype != type(None) and oldtype != newtype:
|
||||
try:
|
||||
value = oldtype(value)
|
||||
if oldtype == unicode:
|
||||
value = oldtype(value, "utf8")
|
||||
else:
|
||||
value = oldtype(value)
|
||||
except ValueError:
|
||||
log.warning("Type '%s' invalid for '%s'", newtype, key)
|
||||
raise
|
||||
@ -250,7 +265,13 @@ what is currently in the config and it could not convert the value
|
||||
5
|
||||
|
||||
"""
|
||||
return self.__config[key]
|
||||
if isinstance(self.__config[key], str):
|
||||
try:
|
||||
return self.__config[key].decode("utf8")
|
||||
except UnicodeDecodeError:
|
||||
return self.__config[key]
|
||||
else:
|
||||
return self.__config[key]
|
||||
|
||||
def register_change_callback(self, callback):
|
||||
"""
|
||||
@ -342,27 +363,28 @@ what is currently in the config and it could not convert the value
|
||||
filename = self.__config_file
|
||||
|
||||
try:
|
||||
data = open(filename, "rb").read()
|
||||
with open(filename, "rb") as _file:
|
||||
data = _file.read()
|
||||
except IOError, e:
|
||||
log.warning("Unable to open config file %s: %s", filename, e)
|
||||
return
|
||||
|
||||
objects = find_json_objects(data)
|
||||
|
||||
|
||||
if not len(objects):
|
||||
# No json objects found, try depickling it
|
||||
try:
|
||||
self.__config.update(pickle.loads(data))
|
||||
except Exception, e:
|
||||
log.exception(e)
|
||||
log.warning("Unable to load config file: %s", filename)
|
||||
log.warning("Unable to load config file: %s", filename)
|
||||
elif len(objects) == 1:
|
||||
start, end = objects[0]
|
||||
try:
|
||||
self.__config.update(json.loads(data[start:end]))
|
||||
except Exception, e:
|
||||
log.exception(e)
|
||||
log.warning("Unable to load config file: %s", filename)
|
||||
log.warning("Unable to load config file: %s", filename)
|
||||
elif len(objects) == 2:
|
||||
try:
|
||||
start, end = objects[0]
|
||||
@ -371,8 +393,8 @@ what is currently in the config and it could not convert the value
|
||||
self.__config.update(json.loads(data[start:end]))
|
||||
except Exception, e:
|
||||
log.exception(e)
|
||||
log.warning("Unable to load config file: %s", filename)
|
||||
|
||||
log.warning("Unable to load config file: %s", filename)
|
||||
|
||||
log.debug("Config %s version: %s.%s loaded: %s", filename,
|
||||
self.__version["format"], self.__version["file"], self.__config)
|
||||
|
||||
@ -390,32 +412,31 @@ what is currently in the config and it could not convert the value
|
||||
# Check to see if the current config differs from the one on disk
|
||||
# We will only write a new config file if there is a difference
|
||||
try:
|
||||
data = open(filename, "rb").read()
|
||||
with open(filename, "rb") as _file:
|
||||
data = _file.read()
|
||||
objects = find_json_objects(data)
|
||||
start, end = objects[0]
|
||||
version = json.loads(data[start:end])
|
||||
start, end = objects[1]
|
||||
loaded_data = json.loads(data[start:end])
|
||||
|
||||
if self.__config == loaded_data and self.__version == version:
|
||||
# The config has not changed so lets just return
|
||||
self._save_timer.cancel()
|
||||
return
|
||||
except Exception, e:
|
||||
log.warning("Unable to open config file: %s", filename)
|
||||
|
||||
|
||||
if self._save_timer and self._save_timer.active():
|
||||
self._save_timer.cancel()
|
||||
return True
|
||||
except (IOError, IndexError), e:
|
||||
log.warning("Unable to open config file: %s because: %s", filename, e)
|
||||
|
||||
# Save the new config and make sure it's written to disk
|
||||
try:
|
||||
log.debug("Saving new config file %s", filename + ".new")
|
||||
f = open(filename + ".new", "wb")
|
||||
json.dump(self.__version, f, indent=2)
|
||||
json.dump(self.__version, f, indent=2)
|
||||
json.dump(self.__config, f, indent=2)
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
f.close()
|
||||
except Exception, e:
|
||||
except IOError, e:
|
||||
log.error("Error writing new config file: %s", e)
|
||||
return False
|
||||
|
||||
@ -424,7 +445,7 @@ what is currently in the config and it could not convert the value
|
||||
log.debug("Backing up old config file to %s~", filename)
|
||||
shutil.move(filename, filename + "~")
|
||||
except Exception, e:
|
||||
log.error("Error backing up old config..")
|
||||
log.warning("Unable to backup old config...")
|
||||
|
||||
# The new config file has been written successfully, so let's move it over
|
||||
# the existing one.
|
||||
|
@ -51,7 +51,7 @@ from deluge.log import LOG as log
|
||||
class AlertManager(component.Component):
|
||||
def __init__(self):
|
||||
log.debug("AlertManager initialized..")
|
||||
component.Component.__init__(self, "AlertManager", interval=0.05)
|
||||
component.Component.__init__(self, "AlertManager", interval=0.3)
|
||||
self.session = component.get("Core").session
|
||||
|
||||
self.session.set_alert_mask(
|
||||
@ -74,7 +74,8 @@ class AlertManager(component.Component):
|
||||
|
||||
def stop(self):
|
||||
for dc in self.delayed_calls:
|
||||
dc.cancel()
|
||||
if dc.active():
|
||||
dc.cancel()
|
||||
self.delayed_calls = []
|
||||
|
||||
def register_handler(self, alert_type, handler):
|
||||
|
@ -33,6 +33,8 @@
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import os
|
||||
import random
|
||||
import stat
|
||||
@ -118,13 +120,14 @@ class AuthManager(component.Component):
|
||||
f = [localclient]
|
||||
else:
|
||||
# Load the auth file into a dictionary: {username: password, ...}
|
||||
f = open(auth_file, "r").readlines()
|
||||
with open(auth_file, "r") as _file:
|
||||
f = _file.readlines()
|
||||
|
||||
for line in f:
|
||||
if line.startswith("#"):
|
||||
# This is a comment line
|
||||
continue
|
||||
line = line.strip()
|
||||
if line.startswith("#") or not line:
|
||||
# This line is a comment or empty
|
||||
continue
|
||||
try:
|
||||
lsplit = line.split(":")
|
||||
except Exception, e:
|
||||
@ -143,4 +146,5 @@ class AuthManager(component.Component):
|
||||
self.__auth[username.strip()] = (password.strip(), level)
|
||||
|
||||
if "localclient" not in self.__auth:
|
||||
open(auth_file, "a").write(self.__create_localclient_account())
|
||||
with open(auth_file, "a") as _file:
|
||||
_file.write(self.__create_localclient_account())
|
||||
|
@ -74,12 +74,12 @@ class AutoAdd(component.Component):
|
||||
return
|
||||
|
||||
for filename in os.listdir(self.config["autoadd_location"]):
|
||||
if filename.split(".")[-1] == "torrent":
|
||||
try:
|
||||
filepath = os.path.join(self.config["autoadd_location"], filename)
|
||||
except UnicodeDecodeError, e:
|
||||
log.error("Unable to auto add torrent due to inproper filename encoding: %s", e)
|
||||
continue
|
||||
try:
|
||||
filepath = os.path.join(self.config["autoadd_location"], filename)
|
||||
except UnicodeDecodeError, e:
|
||||
log.error("Unable to auto add torrent due to improper filename encoding: %s", e)
|
||||
continue
|
||||
if os.path.isfile(filepath) and filename.endswith(".torrent"):
|
||||
try:
|
||||
filedump = self.load_torrent(filepath)
|
||||
except (RuntimeError, Exception), e:
|
||||
|
@ -33,6 +33,8 @@
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
from deluge._libtorrent import lt
|
||||
|
||||
import os
|
||||
@ -42,11 +44,14 @@ import shutil
|
||||
import threading
|
||||
import pkg_resources
|
||||
import warnings
|
||||
import tempfile
|
||||
from urlparse import urljoin
|
||||
|
||||
|
||||
from twisted.internet import reactor, defer
|
||||
from twisted.internet.task import LoopingCall
|
||||
import twisted.web.client
|
||||
import twisted.web.error
|
||||
|
||||
from deluge.httpdownloader import download_file
|
||||
from deluge.log import LOG as log
|
||||
@ -69,19 +74,30 @@ from deluge.core.eventmanager import EventManager
|
||||
from deluge.core.rpcserver import export
|
||||
|
||||
class Core(component.Component):
|
||||
def __init__(self, listen_interface=None):
|
||||
def __init__(self, listen_interface=None, read_only_config_keys=None):
|
||||
log.debug("Core init..")
|
||||
component.Component.__init__(self, "Core")
|
||||
|
||||
# These keys will be dropped from the set_config() RPC and are
|
||||
# configurable from the command-line.
|
||||
self.read_only_config_keys = read_only_config_keys
|
||||
log.debug("read_only_config_keys: %s", read_only_config_keys)
|
||||
|
||||
# Start the libtorrent session
|
||||
log.info("Starting libtorrent %s session..", lt.version)
|
||||
|
||||
# Create the client fingerprint
|
||||
version = [int(value.split("-")[0]) for value in deluge.common.get_version().split(".")]
|
||||
version = deluge.common.VersionSplit(deluge.common.get_version()).version
|
||||
while len(version) < 4:
|
||||
version.append(0)
|
||||
|
||||
self.session = lt.session(lt.fingerprint("DE", *version), flags=0)
|
||||
# In libtorrent versions below 0.16.7.0 disable extension bindings due to GIL issue.
|
||||
# https://code.google.com/p/libtorrent/issues/detail?id=369
|
||||
if deluge.common.VersionSplit(lt.version) >= deluge.common.VersionSplit("0.16.7.0"):
|
||||
self.session = lt.session(lt.fingerprint("DE", *version), flags=0)
|
||||
else:
|
||||
# Setting session flags to 1 enables all libtorrent default plugins
|
||||
self.session = lt.session(lt.fingerprint("DE", *version), flags=1)
|
||||
|
||||
# Load the session state if available
|
||||
self.__load_session_state()
|
||||
@ -89,15 +105,27 @@ class Core(component.Component):
|
||||
# Set the user agent
|
||||
self.settings = lt.session_settings()
|
||||
self.settings.user_agent = "Deluge %s" % deluge.common.get_version()
|
||||
# Increase the alert queue size so that alerts don't get lost
|
||||
self.settings.alert_queue_size = 10000
|
||||
# Ignore buggy resume data timestamps checking #3044.
|
||||
self.settings.ignore_resume_timestamps = True
|
||||
|
||||
# Set session settings
|
||||
self.settings.send_redundant_have = True
|
||||
if deluge.common.windows_check() and lt.version_major == 0 and lt.version_minor <= 15:
|
||||
self.settings.disk_io_write_mode = \
|
||||
lt.io_buffer_mode_t.disable_os_cache
|
||||
self.settings.disk_io_read_mode = \
|
||||
lt.io_buffer_mode_t.disable_os_cache
|
||||
self.session.set_settings(self.settings)
|
||||
|
||||
# Load metadata extension
|
||||
self.session.add_extension(lt.create_metadata_plugin)
|
||||
self.session.add_extension(lt.create_ut_metadata_plugin)
|
||||
self.session.add_extension(lt.create_smart_ban_plugin)
|
||||
# In libtorrent versions below 0.16.7.0 disable extension bindings due to GIL issue.
|
||||
# https://code.google.com/p/libtorrent/issues/detail?id=369
|
||||
if deluge.common.VersionSplit(lt.version) >= deluge.common.VersionSplit("0.16.7.0"):
|
||||
self.session.add_extension("metadata_transfer")
|
||||
self.session.add_extension("ut_metadata")
|
||||
self.session.add_extension("smart_ban")
|
||||
|
||||
# Create the components
|
||||
self.eventmanager = EventManager()
|
||||
@ -112,6 +140,9 @@ class Core(component.Component):
|
||||
# New release check information
|
||||
self.new_release = None
|
||||
|
||||
# GeoIP instance with db loaded
|
||||
self.geoip_instance = None
|
||||
|
||||
# Get the core config
|
||||
self.config = deluge.configmanager.ConfigManager("core.conf")
|
||||
|
||||
@ -119,8 +150,11 @@ class Core(component.Component):
|
||||
# store the one in the config so we can restore it on shutdown
|
||||
self.__old_interface = None
|
||||
if listen_interface:
|
||||
self.__old_interface = self.config["listen_interface"]
|
||||
self.config["listen_interface"] = listen_interface
|
||||
if deluge.common.is_ip(listen_interface):
|
||||
self.__old_interface = self.config["listen_interface"]
|
||||
self.config["listen_interface"] = listen_interface
|
||||
else:
|
||||
log.error("Invalid listen interface (must be IP Address): %s", listen_interface)
|
||||
|
||||
def start(self):
|
||||
"""Starts the core"""
|
||||
@ -128,9 +162,12 @@ class Core(component.Component):
|
||||
self.__new_release = None
|
||||
|
||||
def stop(self):
|
||||
log.debug("Core stopping...")
|
||||
|
||||
# Save the DHT state if necessary
|
||||
if self.config["dht"]:
|
||||
self.save_dht_state()
|
||||
|
||||
# Save the libtorrent session state
|
||||
self.__save_session_state()
|
||||
|
||||
@ -147,16 +184,17 @@ class Core(component.Component):
|
||||
def __save_session_state(self):
|
||||
"""Saves the libtorrent session state"""
|
||||
try:
|
||||
open(deluge.configmanager.get_config_dir("session.state"), "wb").write(
|
||||
lt.bencode(self.session.state()))
|
||||
lt_data = open(deluge.configmanager.get_config_dir("session.state"), "wb")
|
||||
lt_data.write(lt.bencode(self.session.save_state()))
|
||||
lt_data.close()
|
||||
except Exception, e:
|
||||
log.warning("Failed to save lt state: %s", e)
|
||||
|
||||
def __load_session_state(self):
|
||||
"""Loads the libtorrent session state"""
|
||||
try:
|
||||
self.session.load_state(lt.bdecode(
|
||||
open(deluge.configmanager.get_config_dir("session.state"), "rb").read()))
|
||||
with open(deluge.configmanager.get_config_dir("session.state"), "rb") as _file:
|
||||
self.session.load_state(lt.bdecode(_file.read()))
|
||||
except Exception, e:
|
||||
log.warning("Failed to load lt state: %s", e)
|
||||
|
||||
@ -236,20 +274,35 @@ class Core(component.Component):
|
||||
:returns: a Deferred which returns the torrent_id as a str or None
|
||||
"""
|
||||
log.info("Attempting to add url %s", url)
|
||||
def on_get_file(filename):
|
||||
def on_download_success(filename):
|
||||
# We got the file, so add it to the session
|
||||
data = open(filename, "rb").read()
|
||||
f = open(filename, "rb")
|
||||
data = f.read()
|
||||
f.close()
|
||||
try:
|
||||
os.remove(filename)
|
||||
except Exception, e:
|
||||
log.warning("Couldn't remove temp file: %s", e)
|
||||
return self.add_torrent_file(filename, base64.encodestring(data), options)
|
||||
|
||||
def on_get_file_error(failure):
|
||||
# Log the error and pass the failure onto the client
|
||||
log.error("Error occured downloading torrent from %s", url)
|
||||
log.error("Reason: %s", failure.getErrorMessage())
|
||||
return failure
|
||||
def on_download_fail(failure):
|
||||
if failure.check(twisted.web.error.PageRedirect):
|
||||
new_url = urljoin(url, failure.getErrorMessage().split(" to ")[1])
|
||||
result = download_file(new_url, tempfile.mkstemp()[1], headers=headers, force_filename=True)
|
||||
result.addCallbacks(on_download_success, on_download_fail)
|
||||
elif failure.check(twisted.web.client.PartialDownloadError):
|
||||
result = download_file(url, tempfile.mkstemp()[1], headers=headers, force_filename=True,
|
||||
allow_compression=False)
|
||||
result.addCallbacks(on_download_success, on_download_fail)
|
||||
else:
|
||||
# Log the error and pass the failure onto the client
|
||||
log.error("Error occurred downloading torrent from %s", url)
|
||||
log.error("Reason: %s", failure.getErrorMessage())
|
||||
result = failure
|
||||
return result
|
||||
|
||||
d = download_file(url, url.split("/")[-1], headers=headers)
|
||||
d.addCallback(on_get_file)
|
||||
d.addErrback(on_get_file_error)
|
||||
d = download_file(url, tempfile.mkstemp()[1], headers=headers, force_filename=True)
|
||||
d.addCallbacks(on_download_success, on_download_fail)
|
||||
return d
|
||||
|
||||
@export
|
||||
@ -368,15 +421,18 @@ class Core(component.Component):
|
||||
@export
|
||||
def pause_all_torrents(self):
|
||||
"""Pause all torrents in the session"""
|
||||
for torrent in self.torrentmanager.torrents.values():
|
||||
torrent.pause()
|
||||
if not self.session.is_paused():
|
||||
self.session.pause()
|
||||
component.get("EventManager").emit(SessionPausedEvent())
|
||||
|
||||
@export
|
||||
def resume_all_torrents(self):
|
||||
"""Resume all torrents in the session"""
|
||||
for torrent in self.torrentmanager.torrents.values():
|
||||
torrent.resume()
|
||||
component.get("EventManager").emit(SessionResumedEvent())
|
||||
if self.session.is_paused():
|
||||
self.session.resume()
|
||||
for torrent_id in self.torrentmanager.torrents:
|
||||
self.torrentmanager[torrent_id].update_state()
|
||||
component.get("EventManager").emit(SessionResumedEvent())
|
||||
|
||||
@export
|
||||
def resume_torrent(self, torrent_ids):
|
||||
@ -387,11 +443,15 @@ class Core(component.Component):
|
||||
@export
|
||||
def get_torrent_status(self, torrent_id, keys, diff=False):
|
||||
# Build the status dictionary
|
||||
status = self.torrentmanager[torrent_id].get_status(keys, diff)
|
||||
try:
|
||||
status = self.torrentmanager[torrent_id].get_status(keys, diff)
|
||||
except KeyError:
|
||||
# Torrent was probaly removed meanwhile
|
||||
return {}
|
||||
|
||||
# Get the leftover fields and ask the plugin manager to fill them
|
||||
# Get any remaining keys from plugin manager or 'all' if no keys specified.
|
||||
leftover_fields = list(set(keys) - set(status.keys()))
|
||||
if len(leftover_fields) > 0:
|
||||
if len(leftover_fields) > 0 or len(keys) == 0:
|
||||
status.update(self.pluginmanager.get_status(torrent_id, leftover_fields))
|
||||
return status
|
||||
|
||||
@ -454,6 +514,8 @@ class Core(component.Component):
|
||||
"""Set the config with values from dictionary"""
|
||||
# Load all the values into the configuration
|
||||
for key in config.keys():
|
||||
if self.read_only_config_keys and key in self.read_only_config_keys:
|
||||
continue
|
||||
if isinstance(config[key], unicode) or isinstance(config[key], str):
|
||||
config[key] = config[key].encode("utf8")
|
||||
self.config[key] = config[key]
|
||||
@ -606,7 +668,9 @@ class Core(component.Component):
|
||||
if add_to_session:
|
||||
options = {}
|
||||
options["download_location"] = os.path.split(path)[0]
|
||||
self.add_torrent_file(os.path.split(target)[1], open(target, "rb").read(), options)
|
||||
with open(target, "rb") as _file:
|
||||
filedump = base64.encodestring(_file.read())
|
||||
self.add_torrent_file(os.path.split(target)[1], filedump, options)
|
||||
|
||||
@export
|
||||
def upload_plugin(self, filename, filedump):
|
||||
@ -679,7 +743,8 @@ class Core(component.Component):
|
||||
@export
|
||||
def queue_top(self, torrent_ids):
|
||||
log.debug("Attempting to queue %s to top", torrent_ids)
|
||||
for torrent_id in torrent_ids:
|
||||
# torrent_ids must be sorted in reverse before moving to preserve order
|
||||
for torrent_id in sorted(torrent_ids, key=self.torrentmanager.get_queue_position, reverse=True):
|
||||
try:
|
||||
# If the queue method returns True, then we should emit a signal
|
||||
if self.torrentmanager.queue_top(torrent_id):
|
||||
@ -690,35 +755,48 @@ class Core(component.Component):
|
||||
@export
|
||||
def queue_up(self, torrent_ids):
|
||||
log.debug("Attempting to queue %s to up", torrent_ids)
|
||||
torrents = ((self.torrentmanager.get_queue_position(torrent_id), torrent_id) for torrent_id in torrent_ids)
|
||||
torrent_moved = True
|
||||
prev_queue_position = None
|
||||
#torrent_ids must be sorted before moving.
|
||||
torrent_ids = list(torrent_ids)
|
||||
torrent_ids.sort(key = lambda id: self.torrentmanager.torrents[id].get_queue_position())
|
||||
for torrent_id in torrent_ids:
|
||||
try:
|
||||
# If the queue method returns True, then we should emit a signal
|
||||
if self.torrentmanager.queue_up(torrent_id):
|
||||
component.get("EventManager").emit(TorrentQueueChangedEvent())
|
||||
except KeyError:
|
||||
log.warning("torrent_id: %s does not exist in the queue", torrent_id)
|
||||
for queue_position, torrent_id in sorted(torrents):
|
||||
# Move the torrent if and only if there is space (by not moving it we preserve the order)
|
||||
if torrent_moved or queue_position - prev_queue_position > 1:
|
||||
try:
|
||||
torrent_moved = self.torrentmanager.queue_up(torrent_id)
|
||||
except KeyError:
|
||||
log.warning("torrent_id: %s does not exist in the queue", torrent_id)
|
||||
# If the torrent moved, then we should emit a signal
|
||||
if torrent_moved:
|
||||
component.get("EventManager").emit(TorrentQueueChangedEvent())
|
||||
else:
|
||||
prev_queue_position = queue_position
|
||||
|
||||
@export
|
||||
def queue_down(self, torrent_ids):
|
||||
log.debug("Attempting to queue %s to down", torrent_ids)
|
||||
torrents = ((self.torrentmanager.get_queue_position(torrent_id), torrent_id) for torrent_id in torrent_ids)
|
||||
torrent_moved = True
|
||||
prev_queue_position = None
|
||||
#torrent_ids must be sorted before moving.
|
||||
torrent_ids = list(torrent_ids)
|
||||
torrent_ids.sort(key = lambda id: -self.torrentmanager.torrents[id].get_queue_position())
|
||||
for torrent_id in torrent_ids:
|
||||
try:
|
||||
# If the queue method returns True, then we should emit a signal
|
||||
if self.torrentmanager.queue_down(torrent_id):
|
||||
component.get("EventManager").emit(TorrentQueueChangedEvent())
|
||||
except KeyError:
|
||||
log.warning("torrent_id: %s does not exist in the queue", torrent_id)
|
||||
for queue_position, torrent_id in sorted(torrents, reverse=True):
|
||||
# Move the torrent if and only if there is space (by not moving it we preserve the order)
|
||||
if torrent_moved or prev_queue_position - queue_position > 1:
|
||||
try:
|
||||
torrent_moved = self.torrentmanager.queue_down(torrent_id)
|
||||
except KeyError:
|
||||
log.warning("torrent_id: %s does not exist in the queue", torrent_id)
|
||||
# If the torrent moved, then we should emit a signal
|
||||
if torrent_moved:
|
||||
component.get("EventManager").emit(TorrentQueueChangedEvent())
|
||||
else:
|
||||
prev_queue_position = queue_position
|
||||
|
||||
@export
|
||||
def queue_bottom(self, torrent_ids):
|
||||
log.debug("Attempting to queue %s to bottom", torrent_ids)
|
||||
for torrent_id in torrent_ids:
|
||||
# torrent_ids must be sorted before moving to preserve order
|
||||
for torrent_id in sorted(torrent_ids, key=self.torrentmanager.get_queue_position):
|
||||
try:
|
||||
# If the queue method returns True, then we should emit a signal
|
||||
if self.torrentmanager.queue_bottom(torrent_id):
|
||||
@ -747,7 +825,11 @@ class Core(component.Component):
|
||||
def on_get_page(result):
|
||||
return bool(int(result))
|
||||
|
||||
def logError(failure):
|
||||
log.warning("Error testing listen port: %s", failure)
|
||||
|
||||
d.addCallback(on_get_page)
|
||||
d.addErrback(logError)
|
||||
|
||||
return d
|
||||
|
||||
@ -768,7 +850,10 @@ class Core(component.Component):
|
||||
"""
|
||||
if not path:
|
||||
path = self.config["download_location"]
|
||||
return deluge.common.free_space(path)
|
||||
try:
|
||||
return deluge.common.free_space(path)
|
||||
except InvalidPathError:
|
||||
return 0
|
||||
|
||||
@export
|
||||
def get_libtorrent_version(self):
|
||||
|
@ -32,6 +32,8 @@
|
||||
# statement from all source files in the program, then also delete it here.
|
||||
#
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import os
|
||||
import gettext
|
||||
import locale
|
||||
@ -52,7 +54,8 @@ class Daemon(object):
|
||||
if os.path.isfile(deluge.configmanager.get_config_dir("deluged.pid")):
|
||||
# Get the PID and the port of the supposedly running daemon
|
||||
try:
|
||||
(pid, port) = open(deluge.configmanager.get_config_dir("deluged.pid")).read().strip().split(";")
|
||||
with open(deluge.configmanager.get_config_dir("deluged.pid")) as _file:
|
||||
(pid, port) = _file.read().strip().split(";")
|
||||
pid = int(pid)
|
||||
port = int(port)
|
||||
except ValueError:
|
||||
@ -62,13 +65,8 @@ class Daemon(object):
|
||||
|
||||
def process_running(pid):
|
||||
if deluge.common.windows_check():
|
||||
# Do some fancy WMI junk to see if the PID exists in Windows
|
||||
from win32com.client import GetObject
|
||||
def get_proclist():
|
||||
WMI = GetObject('winmgmts:')
|
||||
processes = WMI.InstancesOf('Win32_Process')
|
||||
return [process.Properties_('ProcessID').Value for process in processes]
|
||||
return pid in get_proclist()
|
||||
import win32process
|
||||
return pid in win32process.EnumProcesses()
|
||||
else:
|
||||
# We can just use os.kill on UNIX to test if the process is running
|
||||
try:
|
||||
@ -110,7 +108,7 @@ class Daemon(object):
|
||||
|
||||
# Twisted catches signals to terminate, so just have it call the shutdown
|
||||
# method.
|
||||
reactor.addSystemEventTrigger("after", "shutdown", self.shutdown)
|
||||
reactor.addSystemEventTrigger("before", "shutdown", self._shutdown)
|
||||
|
||||
# Catch some Windows specific signals
|
||||
if deluge.common.windows_check():
|
||||
@ -120,7 +118,7 @@ class Daemon(object):
|
||||
def win_handler(ctrl_type):
|
||||
log.debug("ctrl_type: %s", ctrl_type)
|
||||
if ctrl_type == CTRL_CLOSE_EVENT or ctrl_type == CTRL_SHUTDOWN_EVENT:
|
||||
self.__shutdown()
|
||||
self._shutdown()
|
||||
return 1
|
||||
SetConsoleCtrlHandler(win_handler)
|
||||
|
||||
@ -133,9 +131,20 @@ class Daemon(object):
|
||||
if options and options.config:
|
||||
deluge.configmanager.set_config_dir(options.config)
|
||||
|
||||
if options and options.listen_interface:
|
||||
listen_interface = options.listen_interface
|
||||
else:
|
||||
listen_interface = ""
|
||||
|
||||
if options and options.read_only_config_keys:
|
||||
read_only_config_keys = options.read_only_config_keys.split(",")
|
||||
else:
|
||||
read_only_config_keys = []
|
||||
|
||||
from deluge.core.core import Core
|
||||
# Start the core as a thread and join it until it's done
|
||||
self.core = Core()
|
||||
self.core = Core(listen_interface=listen_interface,
|
||||
read_only_config_keys=read_only_config_keys)
|
||||
|
||||
port = self.core.config["daemon_port"]
|
||||
if options and options.port:
|
||||
@ -163,8 +172,8 @@ class Daemon(object):
|
||||
if not classic:
|
||||
# Write out a pid file all the time, we use this to see if a deluged is running
|
||||
# We also include the running port number to do an additional test
|
||||
open(deluge.configmanager.get_config_dir("deluged.pid"), "wb").write(
|
||||
"%s;%s\n" % (os.getpid(), port))
|
||||
with open(deluge.configmanager.get_config_dir("deluged.pid"), "wb") as _file:
|
||||
_file.write("%s;%s\n" % (os.getpid(), port))
|
||||
|
||||
component.start()
|
||||
try:
|
||||
@ -177,17 +186,16 @@ class Daemon(object):
|
||||
reactor.callLater(0, reactor.stop)
|
||||
|
||||
def _shutdown(self, *args, **kwargs):
|
||||
try:
|
||||
os.remove(deluge.configmanager.get_config_dir("deluged.pid"))
|
||||
except Exception, e:
|
||||
log.exception(e)
|
||||
log.error("Error removing deluged.pid!")
|
||||
if os.path.exists(deluge.configmanager.get_config_dir("deluged.pid")):
|
||||
try:
|
||||
os.remove(deluge.configmanager.get_config_dir("deluged.pid"))
|
||||
except Exception, e:
|
||||
log.exception(e)
|
||||
log.error("Error removing deluged.pid!")
|
||||
|
||||
component.shutdown()
|
||||
try:
|
||||
reactor.stop()
|
||||
except twisted.internet.error.ReactorNotRunning:
|
||||
log.debug("Tried to stop the reactor but it is not running..")
|
||||
log.info("Waiting for components to shutdown..")
|
||||
d = component.shutdown()
|
||||
return d
|
||||
|
||||
@export()
|
||||
def info(self):
|
||||
|
@ -53,7 +53,10 @@ class EventManager(component.Component):
|
||||
if event.name in self.handlers:
|
||||
for handler in self.handlers[event.name]:
|
||||
#log.debug("Running handler %s for event %s with args: %s", event.name, handler, event.args)
|
||||
handler(*event.args)
|
||||
try:
|
||||
handler(*event.args)
|
||||
except Exception, e:
|
||||
log.error("Event handler %s failed in %s with exception %s", event.name, handler, e)
|
||||
|
||||
def register_event_handler(self, event, handler):
|
||||
"""
|
||||
|
@ -126,12 +126,11 @@ class FilterManager(component.Component):
|
||||
|
||||
#sanitize input: filter-value must be a list of strings
|
||||
for key, value in filter_dict.items():
|
||||
if isinstance(value, str):
|
||||
filter_dict[key] = [value]
|
||||
if isinstance(value, basestring):
|
||||
filter_dict[key] = [value]
|
||||
|
||||
|
||||
if "id"in filter_dict: #optimized filter for id:
|
||||
torrent_ids = filter_dict["id"]
|
||||
if "id" in filter_dict: #optimized filter for id:
|
||||
torrent_ids = list(filter_dict["id"])
|
||||
del filter_dict["id"]
|
||||
else:
|
||||
torrent_ids = self.torrents.get_torrent_list()
|
||||
@ -170,7 +169,9 @@ class FilterManager(component.Component):
|
||||
for torrent_id in list(torrent_ids):
|
||||
status = status_func(torrent_id, filter_dict.keys()) #status={key:value}
|
||||
for field, values in filter_dict.iteritems():
|
||||
if (not status[field] in values) and torrent_id in torrent_ids:
|
||||
if field in status and status[field] in values:
|
||||
continue
|
||||
elif torrent_id in torrent_ids:
|
||||
torrent_ids.remove(torrent_id)
|
||||
|
||||
return torrent_ids
|
||||
@ -196,9 +197,8 @@ class FilterManager(component.Component):
|
||||
value = status[field]
|
||||
items[field][value] = items[field].get(value, 0) + 1
|
||||
|
||||
items["tracker_host"]["All"] = len(torrent_ids)
|
||||
|
||||
if "tracker_host" in items:
|
||||
items["tracker_host"]["All"] = len(torrent_ids)
|
||||
items["tracker_host"]["Error"] = len(tracker_error_filter(torrent_ids, ("Error",)))
|
||||
|
||||
if "state" in tree_keys and not show_zero_hits:
|
||||
|
@ -92,6 +92,8 @@ class PluginManager(deluge.pluginmanagerbase.PluginManagerBase,
|
||||
def get_status(self, torrent_id, fields):
|
||||
"""Return the value of status fields for the selected torrent_id."""
|
||||
status = {}
|
||||
if len(fields) == 0:
|
||||
fields = self.status_fields.keys()
|
||||
for field in fields:
|
||||
try:
|
||||
status[field] = self.status_fields[field](torrent_id)
|
||||
|
@ -33,6 +33,7 @@
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import os.path
|
||||
import threading
|
||||
@ -48,6 +49,12 @@ import deluge.common
|
||||
import deluge.component as component
|
||||
from deluge.log import LOG as log
|
||||
|
||||
try:
|
||||
import GeoIP
|
||||
except ImportError:
|
||||
GeoIP = None
|
||||
|
||||
|
||||
DEFAULT_PREFS = {
|
||||
"send_info": False,
|
||||
"info_sent": 0.0,
|
||||
@ -144,6 +151,8 @@ DEFAULT_PREFS = {
|
||||
}
|
||||
|
||||
class PreferencesManager(component.Component):
|
||||
LT_SINGLE_PROXY = deluge.common.VersionSplit(lt.version) >= deluge.common.VersionSplit("0.16.0.0")
|
||||
|
||||
def __init__(self):
|
||||
component.Component.__init__(self, "PreferencesManager")
|
||||
|
||||
@ -152,7 +161,6 @@ class PreferencesManager(component.Component):
|
||||
def start(self):
|
||||
self.core = component.get("Core")
|
||||
self.session = component.get("Core").session
|
||||
self.settings = component.get("Core").settings
|
||||
|
||||
# Register set functions in the Config
|
||||
self.config.register_set_function("torrentfiles_location",
|
||||
@ -229,10 +237,15 @@ class PreferencesManager(component.Component):
|
||||
self.config.register_change_callback(self._on_config_value_change)
|
||||
|
||||
def stop(self):
|
||||
if self.new_release_timer:
|
||||
if self.new_release_timer and self.new_release_timer.running:
|
||||
self.new_release_timer.stop()
|
||||
|
||||
# Config set functions
|
||||
def session_set_setting(self, key, value):
|
||||
settings = self.session.settings()
|
||||
setattr(settings, key, value)
|
||||
self.session.set_settings(settings)
|
||||
|
||||
def _on_config_value_change(self, key, value):
|
||||
component.get("EventManager").emit(ConfigValueChangedEvent(key, value))
|
||||
|
||||
@ -247,7 +260,7 @@ class PreferencesManager(component.Component):
|
||||
# Only set the listen ports if random_port is not true
|
||||
if self.config["random_port"] is not True:
|
||||
log.debug("listen port range set to %s-%s", value[0], value[1])
|
||||
self.session.listen_on(value[0], value[1], str(self.config["listen_interface"]))
|
||||
self.session.listen_on(value[0], value[1], str(self.config["listen_interface"]).strip())
|
||||
|
||||
def _on_set_listen_interface(self, key, value):
|
||||
# Call the random_port callback since it'll do what we need
|
||||
@ -269,13 +282,12 @@ class PreferencesManager(component.Component):
|
||||
# Set the listen ports
|
||||
log.debug("listen port range set to %s-%s", listen_ports[0],
|
||||
listen_ports[1])
|
||||
self.session.listen_on(listen_ports[0], listen_ports[1], str(self.config["listen_interface"]))
|
||||
self.session.listen_on(listen_ports[0], listen_ports[1], str(self.config["listen_interface"]).strip())
|
||||
|
||||
def _on_set_outgoing_ports(self, key, value):
|
||||
if not self.config["random_outgoing_ports"]:
|
||||
log.debug("outgoing port range set to %s-%s", value[0], value[1])
|
||||
self.settings.outgoing_ports = value[0], value[1]
|
||||
self.session.set_settings(self.settings)
|
||||
self.session_set_setting("outgoing_ports", (value[0], value[1]))
|
||||
|
||||
def _on_set_random_outgoing_ports(self, key, value):
|
||||
if value:
|
||||
@ -284,20 +296,19 @@ class PreferencesManager(component.Component):
|
||||
def _on_set_peer_tos(self, key, value):
|
||||
log.debug("setting peer_tos to: %s", value)
|
||||
try:
|
||||
self.settings.peer_tos = chr(int(value, 16))
|
||||
self.session_set_setting("peer_tos", chr(int(value, 16)))
|
||||
except ValueError, e:
|
||||
log.debug("Invalid tos byte: %s", e)
|
||||
return
|
||||
|
||||
self.session.set_settings(self.settings)
|
||||
|
||||
def _on_set_dht(self, key, value):
|
||||
log.debug("dht value set to %s", value)
|
||||
state_file = deluge.configmanager.get_config_dir("dht.state")
|
||||
if value:
|
||||
state = None
|
||||
try:
|
||||
state = lt.bdecode(open(state_file, "rb").read())
|
||||
with open(state_file, "rb") as _file:
|
||||
state = lt.bdecode(_file.read())
|
||||
except Exception, e:
|
||||
log.warning("Unable to read DHT state file: %s", e)
|
||||
|
||||
@ -308,6 +319,8 @@ class PreferencesManager(component.Component):
|
||||
self.session.start_dht(None)
|
||||
self.session.add_dht_router("router.bittorrent.com", 6881)
|
||||
self.session.add_dht_router("router.utorrent.com", 6881)
|
||||
self.session.add_dht_router("dht.transmissionbt.com", 6881)
|
||||
self.session.add_dht_router("dht.aelitis.com", 6881)
|
||||
self.session.add_dht_router("router.bitcomet.com", 6881)
|
||||
else:
|
||||
self.core.save_dht_state()
|
||||
@ -336,16 +349,19 @@ class PreferencesManager(component.Component):
|
||||
|
||||
def _on_set_utpex(self, key, value):
|
||||
log.debug("utpex value set to %s", value)
|
||||
if value:
|
||||
self.session.add_extension(lt.create_ut_pex_plugin)
|
||||
# In libtorrent versions below 0.16.7.0 disable extension bindings due to GIL issue.
|
||||
# https://code.google.com/p/libtorrent/issues/detail?id=369
|
||||
if value and deluge.common.VersionSplit(lt.version) >= deluge.common.VersionSplit("0.16.7.0"):
|
||||
self.session.add_extension("ut_pex")
|
||||
|
||||
def _on_set_encryption(self, key, value):
|
||||
log.debug("encryption value %s set to %s..", key, value)
|
||||
pe_enc_level = {0: lt.enc_level.plaintext, 1: lt.enc_level.rc4, 2: lt.enc_level.both}
|
||||
pe_settings = lt.pe_settings()
|
||||
pe_settings.out_enc_policy = \
|
||||
lt.enc_policy(self.config["enc_out_policy"])
|
||||
pe_settings.in_enc_policy = lt.enc_policy(self.config["enc_in_policy"])
|
||||
pe_settings.allowed_enc_level = lt.enc_level(self.config["enc_level"])
|
||||
pe_settings.allowed_enc_level = lt.enc_level(pe_enc_level[self.config["enc_level"]])
|
||||
pe_settings.prefer_rc4 = self.config["enc_prefer_rc4"]
|
||||
self.session.set_pe_settings(pe_settings)
|
||||
set = self.session.get_pe_settings()
|
||||
@ -387,51 +403,39 @@ class PreferencesManager(component.Component):
|
||||
self.session.set_max_half_open_connections(value)
|
||||
|
||||
def _on_set_max_connections_per_second(self, key, value):
|
||||
self.settings.connection_speed = value
|
||||
self.session.set_settings(self.settings)
|
||||
self.session_set_setting("connection_speed", value)
|
||||
|
||||
def _on_ignore_limits_on_local_network(self, key, value):
|
||||
self.settings.ignore_limits_on_local_network = value
|
||||
self.session.set_settings(self.settings)
|
||||
self.session_set_setting("ignore_limits_on_local_network", value)
|
||||
|
||||
def _on_set_share_ratio_limit(self, key, value):
|
||||
log.debug("%s set to %s..", key, value)
|
||||
self.settings.share_ratio_limit = value
|
||||
self.session.set_settings(self.settings)
|
||||
self.session_set_setting("share_ratio_limit", value)
|
||||
|
||||
def _on_set_seed_time_ratio_limit(self, key, value):
|
||||
log.debug("%s set to %s..", key, value)
|
||||
self.settings.seed_time_ratio_limit = value
|
||||
self.session.set_settings(self.settings)
|
||||
self.session_set_setting("seed_time_ratio_limit", value)
|
||||
|
||||
def _on_set_seed_time_limit(self, key, value):
|
||||
log.debug("%s set to %s..", key, value)
|
||||
# This value is stored in minutes in deluge, but libtorrent wants seconds
|
||||
self.settings.seed_time_limit = int(value * 60)
|
||||
self.session.set_settings(self.settings)
|
||||
self.session_set_setting("seed_time_limit", int(value * 60))
|
||||
|
||||
def _on_set_max_active_downloading(self, key, value):
|
||||
log.debug("%s set to %s..", key, value)
|
||||
log.debug("active_downloads: %s", self.settings.active_downloads)
|
||||
self.settings.active_downloads = value
|
||||
self.session.set_settings(self.settings)
|
||||
self.session_set_setting("active_downloads", value)
|
||||
|
||||
def _on_set_max_active_seeding(self, key, value):
|
||||
log.debug("%s set to %s..", key, value)
|
||||
log.debug("active_seeds: %s", self.settings.active_seeds)
|
||||
self.settings.active_seeds = value
|
||||
self.session.set_settings(self.settings)
|
||||
self.session_set_setting("active_seeds", value)
|
||||
|
||||
def _on_set_max_active_limit(self, key, value):
|
||||
log.debug("%s set to %s..", key, value)
|
||||
log.debug("active_limit: %s", self.settings.active_limit)
|
||||
self.settings.active_limit = value
|
||||
self.session.set_settings(self.settings)
|
||||
self.session_set_setting("active_limit", value)
|
||||
|
||||
def _on_set_dont_count_slow_torrents(self, key, value):
|
||||
log.debug("%s set to %s..", key, value)
|
||||
self.settings.dont_count_slow_torrents = value
|
||||
self.session.set_settings(self.settings)
|
||||
self.session_set_setting("dont_count_slow_torrents", value)
|
||||
|
||||
def _on_send_info(self, key, value):
|
||||
log.debug("Sending anonymous stats..")
|
||||
@ -467,57 +471,70 @@ class PreferencesManager(component.Component):
|
||||
if value:
|
||||
log.debug("Checking for new release..")
|
||||
threading.Thread(target=self.core.get_new_release).start()
|
||||
if self.new_release_timer:
|
||||
if self.new_release_timer and self.new_release_timer.running:
|
||||
self.new_release_timer.stop()
|
||||
# Set a timer to check for a new release every 3 days
|
||||
self.new_release_timer = LoopingCall(
|
||||
self._on_new_release_check, "new_release_check", True)
|
||||
self.new_release_timer.start(72 * 60 * 60, False)
|
||||
else:
|
||||
if self.new_release_timer:
|
||||
if self.new_release_timer and self.new_release_timer.running:
|
||||
self.new_release_timer.stop()
|
||||
|
||||
def _on_set_proxies(self, key, value):
|
||||
for k, v in value.items():
|
||||
if v["type"]:
|
||||
# Test for single proxy with lt >= 0.16
|
||||
if self.LT_SINGLE_PROXY:
|
||||
for proxy_type in value:
|
||||
if proxy_type == "peer":
|
||||
continue
|
||||
if self.config["proxies"][proxy_type] != value["peer"]:
|
||||
log.warning("This version of libtorrent only supports a single proxy setting "
|
||||
"based upon 'peer' which will apply to all other other types.")
|
||||
self.config["proxies"][proxy_type] = value["peer"]
|
||||
|
||||
proxy_settings = lt.proxy_settings()
|
||||
proxy_settings.type = lt.proxy_type(value["peer"]["type"])
|
||||
proxy_settings.username = str(value["peer"]["username"])
|
||||
proxy_settings.password = str(value["peer"]["password"])
|
||||
proxy_settings.hostname = str(value["peer"]["hostname"])
|
||||
proxy_settings.port = value["peer"]["port"]
|
||||
log.debug("Setting proxy settings: %s", value["peer"])
|
||||
self.session.set_proxy(proxy_settings)
|
||||
else:
|
||||
for k, v in value.items():
|
||||
proxy_settings = lt.proxy_settings()
|
||||
proxy_settings.type = lt.proxy_type(v["type"])
|
||||
proxy_settings.username = str(v["username"])
|
||||
proxy_settings.password = str(v["password"])
|
||||
proxy_settings.hostname = str(v["hostname"])
|
||||
proxy_settings.port = v["port"]
|
||||
log.debug("setting %s proxy settings", k)
|
||||
log.debug("Setting %s proxy settings: %s", k, v)
|
||||
getattr(self.session, "set_%s_proxy" % k)(proxy_settings)
|
||||
|
||||
def _on_rate_limit_ip_overhead(self, key, value):
|
||||
log.debug("%s: %s", key, value)
|
||||
self.settings.rate_limit_ip_overhead = value
|
||||
self.session.set_settings(self.settings)
|
||||
self.session_set_setting("rate_limit_ip_overhead", value)
|
||||
|
||||
def _on_geoip_db_location(self, key, value):
|
||||
log.debug("%s: %s", key, value)
|
||||
# Load the GeoIP DB for country look-ups if available
|
||||
geoip_db = ""
|
||||
if os.path.exists(value):
|
||||
geoip_db = value
|
||||
elif os.path.exists(pkg_resources.resource_filename("deluge", os.path.join("data", "GeoIP.dat"))):
|
||||
geoip_db = pkg_resources.resource_filename("deluge", os.path.join("data", "GeoIP.dat"))
|
||||
try:
|
||||
self.core.geoip_instance = GeoIP.open(value, GeoIP.GEOIP_STANDARD)
|
||||
except AttributeError:
|
||||
try:
|
||||
self.session.load_country_db(value)
|
||||
except RuntimeError, ex:
|
||||
log.error("Unable to load geoip database: %s", ex)
|
||||
except AttributeError:
|
||||
log.warning("GeoIP Unavailable")
|
||||
else:
|
||||
log.warning("Unable to find GeoIP database file!")
|
||||
|
||||
if geoip_db:
|
||||
try:
|
||||
self.session.load_country_db(str(geoip_db))
|
||||
except Exception, e:
|
||||
log.error("Unable to load geoip database!")
|
||||
log.exception(e)
|
||||
|
||||
def _on_cache_size(self, key, value):
|
||||
log.debug("%s: %s", key, value)
|
||||
self.settings.cache_size = value
|
||||
self.session.set_settings(self.settings)
|
||||
self.session_set_setting("cache_size", value)
|
||||
|
||||
def _on_cache_expiry(self, key, value):
|
||||
log.debug("%s: %s", key, value)
|
||||
self.settings.cache_expiry = value
|
||||
self.session.set_settings(self.settings)
|
||||
self.session_set_setting("cache_expiry", value)
|
||||
|
@ -35,6 +35,8 @@
|
||||
|
||||
"""RPCServer Module"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import sys
|
||||
import zlib
|
||||
import os
|
||||
@ -47,7 +49,11 @@ from twisted.internet import ssl, reactor, defer
|
||||
from OpenSSL import crypto, SSL
|
||||
from types import FunctionType
|
||||
|
||||
import deluge.rencode as rencode
|
||||
try:
|
||||
import rencode
|
||||
except ImportError:
|
||||
import deluge.rencode as rencode
|
||||
|
||||
from deluge.log import LOG as log
|
||||
|
||||
import deluge.component as component
|
||||
@ -90,13 +96,13 @@ def export(auth_level=AUTH_LEVEL_DEFAULT):
|
||||
def format_request(call):
|
||||
"""
|
||||
Format the RPCRequest message for debug printing
|
||||
|
||||
|
||||
:param call: the request
|
||||
:type call: a RPCRequest
|
||||
|
||||
|
||||
:returns: a formatted string for printing
|
||||
:rtype: str
|
||||
|
||||
|
||||
"""
|
||||
try:
|
||||
s = call[1] + "("
|
||||
@ -111,7 +117,7 @@ def format_request(call):
|
||||
return "UnicodeEncodeError, call: %s" % call
|
||||
else:
|
||||
return s
|
||||
|
||||
|
||||
class DelugeError(Exception):
|
||||
pass
|
||||
|
||||
@ -127,7 +133,8 @@ class ServerContextFactory(object):
|
||||
SSL transport.
|
||||
"""
|
||||
ssl_dir = deluge.configmanager.get_config_dir("ssl")
|
||||
ctx = SSL.Context(SSL.SSLv3_METHOD)
|
||||
ctx = SSL.Context(SSL.SSLv23_METHOD)
|
||||
ctx.set_options(SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3)
|
||||
ctx.use_certificate_file(os.path.join(ssl_dir, "daemon.cert"))
|
||||
ctx.use_privatekey_file(os.path.join(ssl_dir, "daemon.pkey"))
|
||||
return ctx
|
||||
@ -139,7 +146,7 @@ class DelugeRPCProtocol(Protocol):
|
||||
"""
|
||||
This method is called whenever data is received from a client. The
|
||||
only message that a client sends to the server is a RPC Request message.
|
||||
If the RPC Request message is valid, then the method is called in
|
||||
If the RPC Request message is valid, then the method is called in
|
||||
:meth:`dispatch`.
|
||||
|
||||
:param data: the data from the client. It should be a zlib compressed
|
||||
@ -187,7 +194,7 @@ class DelugeRPCProtocol(Protocol):
|
||||
:param data: the object that is to be sent to the client. This should
|
||||
be one of the RPC message types.
|
||||
:type data: object
|
||||
|
||||
|
||||
"""
|
||||
self.transport.write(zlib.compress(rencode.dumps(data)))
|
||||
|
||||
@ -197,8 +204,8 @@ class DelugeRPCProtocol(Protocol):
|
||||
"""
|
||||
peer = self.transport.getPeer()
|
||||
log.info("Deluge Client connection made from: %s:%s", peer.host, peer.port)
|
||||
# Set the initial auth level of this session to AUTH_LEVEL_NONE
|
||||
self.factory.authorized_sessions[self.transport.sessionno] = AUTH_LEVEL_NONE
|
||||
# Set the initial auth level of this session to AUTH_LEVEL_NONE and empty username.
|
||||
self.factory.authorized_sessions[self.transport.sessionno] = (AUTH_LEVEL_NONE, "")
|
||||
|
||||
def connectionLost(self, reason):
|
||||
"""
|
||||
@ -254,7 +261,7 @@ class DelugeRPCProtocol(Protocol):
|
||||
try:
|
||||
ret = component.get("AuthManager").authorize(*args, **kwargs)
|
||||
if ret:
|
||||
self.factory.authorized_sessions[self.transport.sessionno] = ret
|
||||
self.factory.authorized_sessions[self.transport.sessionno] = (ret, args[0])
|
||||
self.factory.session_protocols[self.transport.sessionno] = self
|
||||
except Exception, e:
|
||||
sendError()
|
||||
@ -283,7 +290,7 @@ class DelugeRPCProtocol(Protocol):
|
||||
if method in self.factory.methods and self.transport.sessionno in self.factory.authorized_sessions:
|
||||
try:
|
||||
method_auth_requirement = self.factory.methods[method]._rpcserver_auth_level
|
||||
auth_level = self.factory.authorized_sessions[self.transport.sessionno]
|
||||
auth_level = self.factory.authorized_sessions[self.transport.sessionno][0]
|
||||
if auth_level < method_auth_requirement:
|
||||
# This session is not allowed to call this method
|
||||
log.debug("Session %s is trying to call a method it is not authorized to call!", self.transport.sessionno)
|
||||
@ -338,7 +345,7 @@ class RPCServer(component.Component):
|
||||
self.factory = Factory()
|
||||
self.factory.protocol = DelugeRPCProtocol
|
||||
self.factory.session_id = -1
|
||||
|
||||
|
||||
# Holds the registered methods
|
||||
self.factory.methods = {}
|
||||
# Holds the session_ids and auth levels
|
||||
@ -417,26 +424,41 @@ class RPCServer(component.Component):
|
||||
def get_session_id(self):
|
||||
"""
|
||||
Returns the session id of the current RPC.
|
||||
|
||||
|
||||
:returns: the session id, this will be -1 if no connections have been made
|
||||
:rtype: int
|
||||
|
||||
"""
|
||||
return self.factory.session_id
|
||||
|
||||
|
||||
def get_session_user(self):
|
||||
"""
|
||||
Returns the username calling the current RPC.
|
||||
|
||||
:returns: the username of the user calling the current RPC
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
session_id = self.get_session_id()
|
||||
if session_id > -1 and session_id in self.factory.authorized_sessions:
|
||||
return self.factory.authorized_sessions[session_id][1]
|
||||
else:
|
||||
# No connections made yet
|
||||
return ""
|
||||
|
||||
def is_session_valid(self, session_id):
|
||||
"""
|
||||
Checks if the session is still valid, eg, if the client is still connected.
|
||||
|
||||
|
||||
:param session_id: the session id
|
||||
:type session_id: int
|
||||
|
||||
|
||||
:returns: True if the session is valid
|
||||
:rtype: bool
|
||||
|
||||
|
||||
"""
|
||||
return session_id in self.factory.authorized_sessions
|
||||
|
||||
|
||||
def emit_event(self, event):
|
||||
"""
|
||||
Emits the event to interested clients.
|
||||
@ -446,7 +468,7 @@ class RPCServer(component.Component):
|
||||
"""
|
||||
log.debug("intevents: %s", self.factory.interested_events)
|
||||
# Find sessions interested in this event
|
||||
for session_id, interest in self.factory.interested_events.iteritems():
|
||||
for session_id, interest in self.factory.interested_events.items():
|
||||
if event.name in interest:
|
||||
log.debug("Emit Event: %s %s", event.name, event.args)
|
||||
# This session is interested so send a RPC_EVENT
|
||||
@ -473,10 +495,10 @@ def generate_ssl_keys():
|
||||
"""
|
||||
This method generates a new SSL key/cert.
|
||||
"""
|
||||
digest = "md5"
|
||||
digest = "sha256"
|
||||
# Generate key pair
|
||||
pkey = crypto.PKey()
|
||||
pkey.generate_key(crypto.TYPE_RSA, 1024)
|
||||
pkey.generate_key(crypto.TYPE_RSA, 2048)
|
||||
|
||||
# Generate cert request
|
||||
req = crypto.X509Req()
|
||||
@ -489,7 +511,7 @@ def generate_ssl_keys():
|
||||
cert = crypto.X509()
|
||||
cert.set_serial_number(0)
|
||||
cert.gmtime_adj_notBefore(0)
|
||||
cert.gmtime_adj_notAfter(60*60*24*365*5) # Five Years
|
||||
cert.gmtime_adj_notAfter(60 * 60 * 24 * 365 * 3) # Three Years
|
||||
cert.set_issuer(req.get_subject())
|
||||
cert.set_subject(req.get_subject())
|
||||
cert.set_pubkey(req.get_pubkey())
|
||||
@ -497,8 +519,10 @@ def generate_ssl_keys():
|
||||
|
||||
# Write out files
|
||||
ssl_dir = deluge.configmanager.get_config_dir("ssl")
|
||||
open(os.path.join(ssl_dir, "daemon.pkey"), "w").write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
|
||||
open(os.path.join(ssl_dir, "daemon.cert"), "w").write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
||||
with open(os.path.join(ssl_dir, "daemon.pkey"), "w") as _file:
|
||||
_file.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
|
||||
with open(os.path.join(ssl_dir, "daemon.cert"), "w") as _file:
|
||||
_file.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
||||
# Make the files only readable by this user
|
||||
for f in ("daemon.pkey", "daemon.cert"):
|
||||
os.chmod(os.path.join(ssl_dir, f), stat.S_IREAD | stat.S_IWRITE)
|
||||
|
@ -34,6 +34,8 @@
|
||||
|
||||
"""Internal Torrent class"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import os
|
||||
import time
|
||||
from urllib import unquote
|
||||
@ -49,6 +51,31 @@ from deluge.event import *
|
||||
|
||||
TORRENT_STATE = deluge.common.TORRENT_STATE
|
||||
|
||||
def sanitize_filepath(filepath, folder=False):
|
||||
"""
|
||||
Returns a sanitized filepath to pass to libotorrent rename_file().
|
||||
The filepath will have backslashes substituted along with whitespace
|
||||
padding and duplicate slashes stripped. If `folder` is True a trailing
|
||||
slash is appended to the returned filepath.
|
||||
"""
|
||||
def clean_filename(filename):
|
||||
filename = filename.strip()
|
||||
if filename.replace('.', '') == '':
|
||||
return ''
|
||||
return filename
|
||||
|
||||
if '\\' in filepath or '/' in filepath:
|
||||
folderpath = filepath.replace('\\', '/').split('/')
|
||||
folderpath = [clean_filename(x) for x in folderpath]
|
||||
newfilepath = '/'.join(filter(None, folderpath))
|
||||
else:
|
||||
newfilepath = clean_filename(filepath)
|
||||
|
||||
if folder is True:
|
||||
return newfilepath + '/'
|
||||
else:
|
||||
return newfilepath
|
||||
|
||||
class TorrentOptions(dict):
|
||||
def __init__(self):
|
||||
config = ConfigManager("core.conf").config
|
||||
@ -73,6 +100,14 @@ class TorrentOptions(dict):
|
||||
self["file_priorities"] = []
|
||||
self["mapped_files"] = {}
|
||||
|
||||
|
||||
class TorrentError(object):
|
||||
def __init__(self, error_message, was_paused=False, restart_to_resume=False):
|
||||
self.error_message = error_message
|
||||
self.was_paused = was_paused
|
||||
self.restart_to_resume = restart_to_resume
|
||||
|
||||
|
||||
class Torrent(object):
|
||||
"""Torrent holds information about torrents added to the libtorrent session.
|
||||
"""
|
||||
@ -82,7 +117,7 @@ class Torrent(object):
|
||||
self.config = ConfigManager("core.conf")
|
||||
|
||||
self.rpcserver = component.get("RPCServer")
|
||||
|
||||
|
||||
# This dict holds previous status dicts returned for this torrent
|
||||
# We use this to return dicts that only contain changes from the previous
|
||||
# {session_id: status_dict, ...}
|
||||
@ -90,7 +125,7 @@ class Torrent(object):
|
||||
from twisted.internet.task import LoopingCall
|
||||
self.prev_status_cleanup_loop = LoopingCall(self.cleanup_prev_status)
|
||||
self.prev_status_cleanup_loop.start(10)
|
||||
|
||||
|
||||
# Set the libtorrent handle
|
||||
self.handle = handle
|
||||
# Set the torrent_id for this torrent
|
||||
@ -109,13 +144,16 @@ class Torrent(object):
|
||||
# We store the filename just in case we need to make a copy of the torrentfile
|
||||
if not filename:
|
||||
# If no filename was provided, then just use the infohash
|
||||
filename = self.torrent_id
|
||||
filename = self.torrent_id + '.torrent'
|
||||
|
||||
self.filename = filename
|
||||
|
||||
# Store the magnet uri used to add this torrent if available
|
||||
self.magnet = magnet
|
||||
|
||||
# Torrent state e.g. Paused, Downloading, etc.
|
||||
self.state = None
|
||||
|
||||
# Holds status info so that we don't need to keep getting it from lt
|
||||
self.status = self.handle.status()
|
||||
|
||||
@ -145,26 +183,20 @@ class Torrent(object):
|
||||
self.filename = state.filename
|
||||
self.is_finished = state.is_finished
|
||||
else:
|
||||
# Tracker list
|
||||
self.trackers = []
|
||||
# Create a list of trackers
|
||||
for value in self.handle.trackers():
|
||||
if lt.version_minor < 15:
|
||||
tracker = {}
|
||||
tracker["url"] = value.url
|
||||
tracker["tier"] = value.tier
|
||||
else:
|
||||
tracker = value
|
||||
self.trackers.append(tracker)
|
||||
# Set trackers from libtorrent
|
||||
self.set_trackers(None)
|
||||
|
||||
# Various torrent options
|
||||
self.handle.resolve_countries(True)
|
||||
|
||||
self.set_options(self.options)
|
||||
# Details of torrent forced into error state (i.e. not by libtorrent).
|
||||
self.forced_error = None
|
||||
|
||||
# Status message holds error info about the torrent
|
||||
self.statusmsg = "OK"
|
||||
|
||||
self.set_options(self.options)
|
||||
|
||||
# The torrents state
|
||||
self.update_state()
|
||||
|
||||
@ -179,7 +211,10 @@ class Torrent(object):
|
||||
else:
|
||||
self.time_added = time.time()
|
||||
|
||||
log.debug("Torrent object created.")
|
||||
# Keep track if we're forcing a recheck of the torrent so that we can
|
||||
# repause it after its done if necessary
|
||||
self.forcing_recheck = False
|
||||
self.forcing_recheck_paused = False
|
||||
|
||||
## Options methods ##
|
||||
def set_options(self, options):
|
||||
@ -205,6 +240,10 @@ class Torrent(object):
|
||||
|
||||
|
||||
def set_max_connections(self, max_connections):
|
||||
if max_connections == 0:
|
||||
max_connections = -1
|
||||
elif max_connections == 1:
|
||||
max_connections = 2
|
||||
self.options["max_connections"] = int(max_connections)
|
||||
self.handle.set_max_connections(max_connections)
|
||||
|
||||
@ -230,11 +269,11 @@ class Torrent(object):
|
||||
self.handle.set_download_limit(v)
|
||||
|
||||
def set_prioritize_first_last(self, prioritize):
|
||||
self.options["prioritize_first_last_pieces"] = prioritize
|
||||
if prioritize:
|
||||
if self.handle.has_metadata():
|
||||
if self.handle.get_torrent_info().num_files() == 1:
|
||||
# We only do this if one file is in the torrent
|
||||
self.options["prioritize_first_last_pieces"] = prioritize
|
||||
priorities = [1] * self.handle.get_torrent_info().num_pieces()
|
||||
priorities[0] = 7
|
||||
priorities[-1] = 7
|
||||
@ -262,19 +301,19 @@ class Torrent(object):
|
||||
self.options["move_completed_path"] = move_completed_path
|
||||
|
||||
def set_file_priorities(self, file_priorities):
|
||||
if len(file_priorities) != len(self.get_files()):
|
||||
log.debug("file_priorities len != num_files")
|
||||
self.options["file_priorities"] = self.handle.file_priorities()
|
||||
return
|
||||
|
||||
if self.options["compact_allocation"]:
|
||||
log.debug("setting file priority with compact allocation does not work!")
|
||||
self.options["file_priorities"] = self.handle.file_priorities()
|
||||
return
|
||||
handle_file_priorities = self.handle.file_priorities()
|
||||
# Workaround for libtorrent 1.1 changing default priorities from 1 to 4.
|
||||
if 4 in handle_file_priorities:
|
||||
handle_file_priorities = [1 if x == 4 else x for x in handle_file_priorities]
|
||||
|
||||
log.debug("setting %s's file priorities: %s", self.torrent_id, file_priorities)
|
||||
|
||||
self.handle.prioritize_files(file_priorities)
|
||||
if (self.handle.has_metadata() and not self.options["compact_allocation"] and
|
||||
file_priorities and len(file_priorities) == len(self.get_files())):
|
||||
self.handle.prioritize_files(file_priorities)
|
||||
else:
|
||||
log.debug("Unable to set new file priorities.")
|
||||
file_priorities = handle_file_priorities
|
||||
|
||||
if 0 in self.options["file_priorities"]:
|
||||
# We have previously marked a file 'Do Not Download'
|
||||
@ -286,19 +325,26 @@ class Torrent(object):
|
||||
self.update_state()
|
||||
break
|
||||
|
||||
self.options["file_priorities"] = file_priorities
|
||||
self.options["file_priorities"] = self.handle.file_priorities()
|
||||
if self.options["file_priorities"] != list(file_priorities):
|
||||
log.warning("File priorities were not set for this torrent")
|
||||
|
||||
# Set the first/last priorities if needed
|
||||
self.set_prioritize_first_last(self.options["prioritize_first_last_pieces"])
|
||||
|
||||
def set_trackers(self, trackers):
|
||||
def set_trackers(self, trackers, reannounce=True):
|
||||
"""Sets trackers"""
|
||||
if trackers == None:
|
||||
trackers = []
|
||||
for value in self.handle.trackers():
|
||||
tracker = {}
|
||||
tracker["url"] = value.url
|
||||
tracker["tier"] = value.tier
|
||||
if lt.version_major == 0 and lt.version_minor < 15:
|
||||
tracker = {}
|
||||
tracker["url"] = value.url
|
||||
tracker["tier"] = value.tier
|
||||
else:
|
||||
tracker = value
|
||||
# These unused lt 1.1.2 tracker datetime entries need to be None for rencode.
|
||||
tracker["min_announce"] = tracker["next_announce"] = None
|
||||
trackers.append(tracker)
|
||||
self.trackers = trackers
|
||||
self.tracker_host = None
|
||||
@ -307,10 +353,13 @@ class Torrent(object):
|
||||
log.debug("Setting trackers for %s: %s", self.torrent_id, trackers)
|
||||
tracker_list = []
|
||||
|
||||
for tracker in trackers:
|
||||
for idx, tracker in enumerate(trackers):
|
||||
new_entry = lt.announce_entry(tracker["url"])
|
||||
new_entry.tier = tracker["tier"]
|
||||
tracker_list.append(new_entry)
|
||||
# These unused lt 1.1.2 tracker datetime entries need to be None for rencode.
|
||||
trackers[idx]["min_announce"] = trackers[idx]["next_announce"] = None
|
||||
|
||||
self.handle.replace_trackers(tracker_list)
|
||||
|
||||
# Print out the trackers
|
||||
@ -318,7 +367,7 @@ class Torrent(object):
|
||||
# log.debug("tier: %s tracker: %s", t["tier"], t["url"])
|
||||
# Set the tracker list in the torrent object
|
||||
self.trackers = trackers
|
||||
if len(trackers) > 0:
|
||||
if len(trackers) > 0 and reannounce:
|
||||
# Force a reannounce if there is at least 1 tracker
|
||||
self.force_reannounce()
|
||||
|
||||
@ -331,48 +380,56 @@ class Torrent(object):
|
||||
|
||||
def set_tracker_status(self, status):
|
||||
"""Sets the tracker status"""
|
||||
self.tracker_host = None
|
||||
self.tracker_status = self.get_tracker_host() + ": " + status
|
||||
|
||||
def update_state(self):
|
||||
"""Updates the state based on what libtorrent's state for the torrent is"""
|
||||
# Set the initial state based on the lt state
|
||||
LTSTATE = deluge.common.LT_TORRENT_STATE
|
||||
ltstate = int(self.handle.status().state)
|
||||
status = self.handle.status()
|
||||
ltstate = status.state
|
||||
|
||||
# Set self.state to the ltstate right away just incase we don't hit some
|
||||
# of the logic below
|
||||
self.state = str(ltstate)
|
||||
# Set self.state to the ltstate right away just incase we don't hit some of the logic below
|
||||
old_state = self.state
|
||||
self.state = LTSTATE.get(int(ltstate), str(ltstate))
|
||||
|
||||
log.debug("set_state_based_on_ltstate: %s", deluge.common.LT_TORRENT_STATE[ltstate])
|
||||
log.debug("session.is_paused: %s", component.get("Core").session.is_paused())
|
||||
is_paused = self.handle.is_paused()
|
||||
is_auto_managed = self.handle.is_auto_managed()
|
||||
session_paused = component.get("Core").session.is_paused()
|
||||
|
||||
# First we check for an error from libtorrent, and set the state to that
|
||||
# if any occurred.
|
||||
if len(self.handle.status().error) > 0:
|
||||
if self.forced_error:
|
||||
self.state = "Error"
|
||||
self.set_status_message("Error: " + self.forced_error.error_message)
|
||||
elif status.error:
|
||||
# This is an error'd torrent
|
||||
self.state = "Error"
|
||||
self.set_status_message(self.handle.status().error)
|
||||
if self.handle.is_paused():
|
||||
self.set_status_message(status.error)
|
||||
if is_paused:
|
||||
self.handle.auto_managed(False)
|
||||
return
|
||||
|
||||
if ltstate == LTSTATE["Queued"] or ltstate == LTSTATE["Checking"]:
|
||||
if self.handle.is_paused():
|
||||
else:
|
||||
if is_paused and is_auto_managed and not session_paused:
|
||||
self.state = "Queued"
|
||||
elif is_paused or session_paused:
|
||||
self.state = "Paused"
|
||||
else:
|
||||
elif ltstate == LTSTATE["Queued"] or ltstate == LTSTATE["Checking"] or \
|
||||
ltstate == LTSTATE["Checking Resume Data"]:
|
||||
self.state = "Checking"
|
||||
return
|
||||
elif ltstate == LTSTATE["Downloading"] or ltstate == LTSTATE["Downloading Metadata"]:
|
||||
self.state = "Downloading"
|
||||
elif ltstate == LTSTATE["Finished"] or ltstate == LTSTATE["Seeding"]:
|
||||
self.state = "Seeding"
|
||||
elif ltstate == LTSTATE["Allocating"]:
|
||||
self.state = "Allocating"
|
||||
elif ltstate == LTSTATE["Downloading"] or ltstate == LTSTATE["Downloading Metadata"]:
|
||||
self.state = "Downloading"
|
||||
elif ltstate == LTSTATE["Finished"] or ltstate == LTSTATE["Seeding"]:
|
||||
self.state = "Seeding"
|
||||
elif ltstate == LTSTATE["Allocating"]:
|
||||
self.state = "Allocating"
|
||||
|
||||
if self.handle.is_paused() and self.handle.is_auto_managed() and not component.get("Core").session.is_paused():
|
||||
self.state = "Queued"
|
||||
elif component.get("Core").session.is_paused() or (self.handle.is_paused() and not self.handle.is_auto_managed()):
|
||||
self.state = "Paused"
|
||||
if self.state != old_state:
|
||||
log.debug("Using torrent state from lt: %s, auto_managed: %s, paused: %s, session_paused: %s",
|
||||
ltstate, is_auto_managed, is_paused, session_paused)
|
||||
log.debug("Torrent %s set from %s to %s: '%s'",
|
||||
self.torrent_id, old_state, self.state, self.statusmsg)
|
||||
component.get("EventManager").emit(TorrentStateChangedEvent(self.torrent_id, self.state))
|
||||
|
||||
def set_state(self, state):
|
||||
"""Accepts state strings, ie, "Paused", "Seeding", etc."""
|
||||
@ -386,6 +443,37 @@ class Torrent(object):
|
||||
def set_status_message(self, message):
|
||||
self.statusmsg = message
|
||||
|
||||
def force_error_state(self, message, restart_to_resume=True):
|
||||
"""Forces the torrent into an error state.
|
||||
|
||||
For setting an error state not covered by libtorrent.
|
||||
|
||||
Args:
|
||||
message (str): The error status message.
|
||||
restart_to_resume (bool, optional): Prevent resuming clearing the error, only restarting
|
||||
session can resume.
|
||||
"""
|
||||
status = self.handle.status()
|
||||
self.handle.auto_managed(False)
|
||||
self.forced_error = TorrentError(message, status.paused, restart_to_resume)
|
||||
if not status.paused:
|
||||
self.handle.pause()
|
||||
self.update_state()
|
||||
|
||||
def clear_forced_error_state(self, update_state=True):
|
||||
if not self.forced_error:
|
||||
return
|
||||
|
||||
if self.forced_error.restart_to_resume:
|
||||
log.error("Restart deluge to clear this torrent error")
|
||||
|
||||
if not self.forced_error.was_paused and self.options["auto_managed"]:
|
||||
self.handle.auto_managed(True)
|
||||
self.forced_error = None
|
||||
self.set_status_message("OK")
|
||||
if update_state:
|
||||
self.update_state()
|
||||
|
||||
def get_eta(self):
|
||||
"""Returns the ETA in seconds for this torrent"""
|
||||
if self.status == None:
|
||||
@ -393,15 +481,14 @@ class Torrent(object):
|
||||
else:
|
||||
status = self.status
|
||||
|
||||
if self.is_finished and (self.options["stop_at_ratio"] or self.config["stop_seed_at_ratio"]):
|
||||
if self.is_finished and self.options["stop_at_ratio"]:
|
||||
# We're a seed, so calculate the time to the 'stop_share_ratio'
|
||||
if not status.upload_payload_rate:
|
||||
return 0
|
||||
stop_ratio = self.config["stop_seed_ratio"] if self.config["stop_seed_at_ratio"] else self.options["stop_ratio"]
|
||||
|
||||
stop_ratio = self.options["stop_ratio"]
|
||||
return ((status.all_time_download * stop_ratio) - status.all_time_upload) / status.upload_payload_rate
|
||||
|
||||
left = status.total_wanted - status.total_done
|
||||
left = status.total_wanted - status.total_wanted_done
|
||||
|
||||
if left <= 0 or status.download_payload_rate == 0:
|
||||
return 0
|
||||
@ -444,7 +531,8 @@ class Torrent(object):
|
||||
for index, file in enumerate(files):
|
||||
ret.append({
|
||||
'index': index,
|
||||
'path': file.path.decode("utf8", "ignore"),
|
||||
# Make path separators consistent across platforms
|
||||
'path': file.path.decode("utf8").replace('\\', '/'),
|
||||
'size': file.size,
|
||||
'offset': file.offset
|
||||
})
|
||||
@ -464,22 +552,24 @@ class Torrent(object):
|
||||
except UnicodeDecodeError:
|
||||
client = str(peer.client).decode("latin-1")
|
||||
|
||||
# Make country a proper string
|
||||
country = str()
|
||||
for c in peer.country:
|
||||
if not c.isalpha():
|
||||
country += " "
|
||||
else:
|
||||
country += c
|
||||
try:
|
||||
country = component.get("Core").geoip_instance.country_code_by_addr(peer.ip[0])
|
||||
except AttributeError:
|
||||
country = peer.country
|
||||
|
||||
try:
|
||||
country = "".join([char if char.isalpha() else " " for char in country])
|
||||
except TypeError:
|
||||
country = ""
|
||||
|
||||
ret.append({
|
||||
"client": client,
|
||||
"country": country,
|
||||
"down_speed": peer.down_speed,
|
||||
"down_speed": peer.payload_down_speed,
|
||||
"ip": "%s:%s" % (peer.ip[0], peer.ip[1]),
|
||||
"progress": peer.progress,
|
||||
"seed": peer.flags & peer.seed,
|
||||
"up_speed": peer.up_speed,
|
||||
"up_speed": peer.payload_up_speed,
|
||||
})
|
||||
|
||||
return ret
|
||||
@ -488,10 +578,21 @@ class Torrent(object):
|
||||
"""Returns the torrents queue position"""
|
||||
return self.handle.queue_position()
|
||||
|
||||
def get_file_priorities(self):
|
||||
"""Return the file priorities"""
|
||||
if not self.handle.has_metadata():
|
||||
return []
|
||||
|
||||
if not self.options["file_priorities"]:
|
||||
# Ensure file_priorities option is populated.
|
||||
self.set_file_priorities([])
|
||||
|
||||
return self.options["file_priorities"]
|
||||
|
||||
def get_file_progress(self):
|
||||
"""Returns the file progress as a list of floats.. 0.0 -> 1.0"""
|
||||
if not self.handle.has_metadata():
|
||||
return 0.0
|
||||
return []
|
||||
|
||||
file_progress = self.handle.file_progress()
|
||||
ret = []
|
||||
@ -500,6 +601,8 @@ class Torrent(object):
|
||||
ret.append(float(file_progress[i]) / float(f["size"]))
|
||||
except ZeroDivisionError:
|
||||
ret.append(0.0)
|
||||
except IndexError:
|
||||
return []
|
||||
|
||||
return ret
|
||||
|
||||
@ -543,18 +646,18 @@ class Torrent(object):
|
||||
def get_status(self, keys, diff=False):
|
||||
"""
|
||||
Returns the status of the torrent based on the keys provided
|
||||
|
||||
|
||||
:param keys: the keys to get the status on
|
||||
:type keys: list of str
|
||||
:param diff: if True, will return a diff of the changes since the last
|
||||
call to get_status based on the session_id
|
||||
:type diff: bool
|
||||
|
||||
|
||||
:returns: a dictionary of the status keys and their values
|
||||
:rtype: dict
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
# Create the full dictionary
|
||||
self.status = self.handle.status()
|
||||
if self.handle.has_metadata():
|
||||
@ -568,6 +671,13 @@ class Torrent(object):
|
||||
if distributed_copies < 0:
|
||||
distributed_copies = 0.0
|
||||
|
||||
# Calculate the seeds:peers ratio
|
||||
if self.status.num_incomplete == 0:
|
||||
# Use -1.0 to signify infinity
|
||||
seeds_peers_ratio = -1.0
|
||||
else:
|
||||
seeds_peers_ratio = self.status.num_complete / float(self.status.num_incomplete)
|
||||
|
||||
#if you add a key here->add it to core.py STATUS_KEYS too.
|
||||
full_status = {
|
||||
"active_time": self.status.active_time,
|
||||
@ -575,7 +685,6 @@ class Torrent(object):
|
||||
"compact": self.options["compact_allocation"],
|
||||
"distributed_copies": distributed_copies,
|
||||
"download_payload_rate": self.status.download_payload_rate,
|
||||
"file_priorities": self.options["file_priorities"],
|
||||
"hash": self.torrent_id,
|
||||
"is_auto_managed": self.options["auto_managed"],
|
||||
"is_finished": self.is_finished,
|
||||
@ -586,6 +695,8 @@ class Torrent(object):
|
||||
"message": self.statusmsg,
|
||||
"move_on_completed_path": self.options["move_completed_path"],
|
||||
"move_on_completed": self.options["move_completed"],
|
||||
"move_completed_path": self.options["move_completed_path"],
|
||||
"move_completed": self.options["move_completed"],
|
||||
"next_announce": self.status.next_announce.seconds,
|
||||
"num_peers": self.status.num_peers - self.status.num_seeds,
|
||||
"num_seeds": self.status.num_seeds,
|
||||
@ -595,6 +706,7 @@ class Torrent(object):
|
||||
"remove_at_ratio": self.options["remove_at_ratio"],
|
||||
"save_path": self.options["download_location"],
|
||||
"seeding_time": self.status.seeding_time,
|
||||
"seeds_peers_ratio": seeds_peers_ratio,
|
||||
"seed_rank": self.status.seed_rank,
|
||||
"state": self.state,
|
||||
"stop_at_ratio": self.options["stop_at_ratio"],
|
||||
@ -623,7 +735,7 @@ class Torrent(object):
|
||||
|
||||
def ti_name():
|
||||
if self.handle.has_metadata():
|
||||
name = self.torrent_info.file_at(0).path.split("/", 1)[0]
|
||||
name = self.torrent_info.file_at(0).path.replace("\\", "/", 1).split("/", 1)[0]
|
||||
if not name:
|
||||
name = self.torrent_info.name()
|
||||
try:
|
||||
@ -671,6 +783,7 @@ class Torrent(object):
|
||||
fns = {
|
||||
"comment": ti_comment,
|
||||
"eta": self.get_eta,
|
||||
"file_priorities": self.get_file_priorities,
|
||||
"file_progress": self.get_file_progress,
|
||||
"files": self.get_files,
|
||||
"is_seed": self.handle.is_seed,
|
||||
@ -699,7 +812,7 @@ class Torrent(object):
|
||||
status_dict[key] = full_status[key]
|
||||
elif key in fns:
|
||||
status_dict[key] = fns[key]()
|
||||
|
||||
|
||||
session_id = self.rpcserver.get_session_id()
|
||||
if diff:
|
||||
if session_id in self.prev_status:
|
||||
@ -711,7 +824,7 @@ class Torrent(object):
|
||||
status_diff[key] = value
|
||||
else:
|
||||
status_diff[key] = value
|
||||
|
||||
|
||||
self.prev_status[session_id] = status_dict
|
||||
return status_diff
|
||||
|
||||
@ -731,6 +844,8 @@ class Torrent(object):
|
||||
|
||||
def pause(self):
|
||||
"""Pause this torrent"""
|
||||
if self.state == "Error":
|
||||
return False
|
||||
# Turn off auto-management so the torrent will not be unpaused by lt queueing
|
||||
self.handle.auto_managed(False)
|
||||
if self.handle.is_paused():
|
||||
@ -754,20 +869,16 @@ class Torrent(object):
|
||||
|
||||
if self.handle.is_paused() and self.handle.is_auto_managed():
|
||||
log.debug("Torrent is being auto-managed, cannot resume!")
|
||||
return
|
||||
elif self.forced_error and self.forced_error.was_paused:
|
||||
log.debug("Skip resuming Error state torrent that was originally paused.")
|
||||
else:
|
||||
# Reset the status message just in case of resuming an Error'd torrent
|
||||
self.set_status_message("OK")
|
||||
|
||||
if self.handle.is_finished():
|
||||
# If the torrent has already reached it's 'stop_seed_ratio' then do not do anything
|
||||
if self.config["stop_seed_at_ratio"] or self.options["stop_at_ratio"]:
|
||||
if self.options["stop_at_ratio"]:
|
||||
ratio = self.options["stop_ratio"]
|
||||
else:
|
||||
ratio = self.config["stop_seed_ratio"]
|
||||
|
||||
if self.get_ratio() >= ratio:
|
||||
if self.options["stop_at_ratio"]:
|
||||
if self.get_ratio() >= self.options["stop_ratio"]:
|
||||
#XXX: This should just be returned in the RPC Response, no event
|
||||
#self.signals.emit_event("torrent_resume_at_stop_ratio")
|
||||
return
|
||||
@ -783,6 +894,11 @@ class Torrent(object):
|
||||
|
||||
return True
|
||||
|
||||
if self.forced_error and not self.forced_error.restart_to_resume:
|
||||
self.clear_forced_error_state()
|
||||
elif self.state == "Error" and not self.forced_error:
|
||||
self.handle.clear_error()
|
||||
|
||||
def connect_peer(self, ip, port):
|
||||
"""adds manual peer"""
|
||||
try:
|
||||
@ -795,8 +911,33 @@ class Torrent(object):
|
||||
def move_storage(self, dest):
|
||||
"""Move a torrent's storage location"""
|
||||
try:
|
||||
self.handle.move_storage(dest.encode("utf8"))
|
||||
except:
|
||||
dest = unicode(dest, "utf-8")
|
||||
except TypeError:
|
||||
# String is already unicode
|
||||
pass
|
||||
|
||||
if not os.path.exists(dest):
|
||||
try:
|
||||
# Try to make the destination path if it doesn't exist
|
||||
os.makedirs(dest)
|
||||
except OSError, ex:
|
||||
log.error("Could not move storage for torrent %s since %s does "
|
||||
"not exist and could not create the directory: %s",
|
||||
self.torrent_id, dest, ex)
|
||||
return False
|
||||
|
||||
kwargs = {}
|
||||
if deluge.common.VersionSplit(lt.version) >= deluge.common.VersionSplit("1.0.0.0"):
|
||||
kwargs['flags'] = 2 # dont_replace
|
||||
dest_bytes = dest.encode('utf-8')
|
||||
try:
|
||||
# libtorrent needs unicode object if wstrings are enabled, utf8 bytestring otherwise
|
||||
try:
|
||||
self.handle.move_storage(dest, **kwargs)
|
||||
except TypeError:
|
||||
self.handle.move_storage(dest_bytes, **kwargs)
|
||||
except Exception, e:
|
||||
log.error("Error calling libtorrent move_storage: %s" % e)
|
||||
return False
|
||||
|
||||
return True
|
||||
@ -804,8 +945,17 @@ class Torrent(object):
|
||||
def save_resume_data(self):
|
||||
"""Signals libtorrent to build resume data for this torrent, it gets
|
||||
returned in a libtorrent alert"""
|
||||
self.handle.save_resume_data()
|
||||
self.waiting_on_resume_data = True
|
||||
# Don't generate fastresume data if torrent is in a Deluge Error state.
|
||||
if self.forced_error:
|
||||
log.debug("Skipped creating resume_data while in Error state")
|
||||
else:
|
||||
self.handle.save_resume_data()
|
||||
self.waiting_on_resume_data = True
|
||||
|
||||
def on_metadata_received(self):
|
||||
if self.options["prioritize_first_last_pieces"]:
|
||||
self.set_prioritize_first_last(True)
|
||||
self.write_torrentfile()
|
||||
|
||||
def write_torrentfile(self):
|
||||
"""Writes the torrent file"""
|
||||
@ -820,7 +970,13 @@ class Torrent(object):
|
||||
md = lt.bdecode(self.torrent_info.metadata())
|
||||
torrent_file = {}
|
||||
torrent_file["info"] = md
|
||||
open(path, "wb").write(lt.bencode(torrent_file))
|
||||
with open(path, "wb") as _file:
|
||||
_file.write(lt.bencode(torrent_file))
|
||||
if self.config["copy_torrent_file"]:
|
||||
config_dir = self.config['torrentfiles_location']
|
||||
filepath = os.path.join(config_dir, self.filename)
|
||||
with open(filepath, "wb") as _file:
|
||||
_file.write(lt.bencode(torrent_file))
|
||||
except Exception, e:
|
||||
log.warning("Unable to save torrent file: %s", e)
|
||||
|
||||
@ -857,19 +1013,43 @@ class Torrent(object):
|
||||
|
||||
def force_recheck(self):
|
||||
"""Forces a recheck of the torrents pieces"""
|
||||
self.forcing_recheck = True
|
||||
if self.forced_error:
|
||||
self.forcing_recheck_paused = self.forced_error.was_paused
|
||||
self.clear_forced_error_state(update_state=False)
|
||||
else:
|
||||
self.forcing_recheck_paused = self.handle.is_paused()
|
||||
# Store trackers for paused torrents to prevent unwanted announce before pausing again.
|
||||
if self.forcing_recheck_paused:
|
||||
self.set_trackers(None, reannounce=False)
|
||||
self.handle.replace_trackers([])
|
||||
|
||||
try:
|
||||
self.handle.force_recheck()
|
||||
self.handle.resume()
|
||||
except Exception, e:
|
||||
log.debug("Unable to force recheck: %s", e)
|
||||
return False
|
||||
return True
|
||||
self.forcing_recheck = False
|
||||
if self.forcing_recheck_paused:
|
||||
self.set_trackers(torrent.trackers, reannounce=False)
|
||||
|
||||
return self.forcing_recheck
|
||||
|
||||
def rename_files(self, filenames):
|
||||
"""Renames files in the torrent. 'filenames' should be a list of
|
||||
(index, filename) pairs."""
|
||||
for index, filename in filenames:
|
||||
self.handle.rename_file(index, filename.encode("utf-8"))
|
||||
# Make sure filename is a unicode object
|
||||
try:
|
||||
filename = unicode(filename, "utf-8")
|
||||
except TypeError:
|
||||
pass
|
||||
filename = sanitize_filepath(filename)
|
||||
# libtorrent needs unicode object if wstrings are enabled, utf8 bytestring otherwise
|
||||
try:
|
||||
self.handle.rename_file(index, filename)
|
||||
except TypeError:
|
||||
self.handle.rename_file(index, filename.encode("utf-8"))
|
||||
|
||||
def rename_folder(self, folder, new_folder):
|
||||
"""Renames a folder within a torrent. This basically does a file rename
|
||||
@ -879,24 +1059,26 @@ class Torrent(object):
|
||||
log.error("Attempting to rename a folder with an invalid folder name: %s", new_folder)
|
||||
return
|
||||
|
||||
if new_folder[-1:] != "/":
|
||||
new_folder += "/"
|
||||
new_folder = sanitize_filepath(new_folder, folder=True)
|
||||
|
||||
wait_on_folder = (folder, new_folder, [])
|
||||
for f in self.get_files():
|
||||
if f["path"].startswith(folder):
|
||||
# Keep a list of filerenames we're waiting on
|
||||
wait_on_folder[2].append(f["index"])
|
||||
self.handle.rename_file(f["index"], f["path"].replace(folder, new_folder, 1).encode("utf-8"))
|
||||
new_path = f["path"].replace(folder, new_folder, 1)
|
||||
try:
|
||||
self.handle.rename_file(f["index"], new_path)
|
||||
except TypeError:
|
||||
self.handle.rename_file(f["index"], new_path.encode("utf-8"))
|
||||
self.waiting_on_folder_rename.append(wait_on_folder)
|
||||
|
||||
|
||||
def cleanup_prev_status(self):
|
||||
"""
|
||||
This method gets called to check the validity of the keys in the prev_status
|
||||
dict. If the key is no longer valid, the dict will be deleted.
|
||||
|
||||
|
||||
"""
|
||||
for key in self.prev_status.keys():
|
||||
if not self.rpcserver.is_session_valid(key):
|
||||
del self.prev_status[key]
|
||||
|
||||
|
@ -17,9 +17,9 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with deluge. If not, write to:
|
||||
# The Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor
|
||||
# Boston, MA 02110-1301, USA.
|
||||
# The Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# In addition, as a special exception, the copyright holders give
|
||||
# permission to link the code of portions of this program with the OpenSSL
|
||||
@ -41,22 +41,21 @@ import os
|
||||
import time
|
||||
import shutil
|
||||
import operator
|
||||
import re
|
||||
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet.task import LoopingCall
|
||||
|
||||
from deluge._libtorrent import lt
|
||||
|
||||
|
||||
from deluge.event import *
|
||||
from deluge.error import *
|
||||
import deluge.common
|
||||
import deluge.component as component
|
||||
from deluge.configmanager import ConfigManager, get_config_dir
|
||||
from deluge.core.torrent import Torrent
|
||||
from deluge.core.torrent import TorrentOptions
|
||||
import deluge.core.oldstateupgrader
|
||||
from deluge.ui.common import utf8_encoded
|
||||
from deluge.common import utf8_encoded, decode_string
|
||||
|
||||
from deluge.log import LOG as log
|
||||
|
||||
@ -112,10 +111,22 @@ class TorrentState:
|
||||
self.move_completed = move_completed
|
||||
self.move_completed_path = move_completed_path
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, TorrentState) and self.__dict__ == other.__dict__
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
class TorrentManagerState:
|
||||
def __init__(self):
|
||||
self.torrents = []
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, TorrentManagerState) and self.torrents == other.torrents
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
class TorrentManager(component.Component):
|
||||
"""
|
||||
TorrentManager contains a list of torrents in the current libtorrent
|
||||
@ -139,6 +150,7 @@ class TorrentManager(component.Component):
|
||||
|
||||
# Create the torrents dict { torrent_id: Torrent }
|
||||
self.torrents = {}
|
||||
self.queued_torrents = set()
|
||||
|
||||
# This is a list of torrent_id when we shutdown the torrentmanager.
|
||||
# We use this list to determine if all active torrents have been paused
|
||||
@ -148,9 +160,18 @@ class TorrentManager(component.Component):
|
||||
# self.num_resume_data used to save resume_data in bulk
|
||||
self.num_resume_data = 0
|
||||
|
||||
# Keep track of torrents finished but moving storage
|
||||
self.waiting_on_finish_moving = []
|
||||
|
||||
# Keeps track of resume data that needs to be saved to disk
|
||||
self.resume_data = {}
|
||||
|
||||
# Workaround to determine if TorrentAddedEvent is from state file
|
||||
self.session_started = False
|
||||
|
||||
# Keep the previous saved state
|
||||
self.prev_saved_state = None
|
||||
|
||||
# Register set functions
|
||||
self.config.register_set_function("max_connections_per_torrent",
|
||||
self.on_set_max_connections_per_torrent)
|
||||
@ -178,6 +199,8 @@ class TorrentManager(component.Component):
|
||||
self.on_alert_tracker_error)
|
||||
self.alerts.register_handler("storage_moved_alert",
|
||||
self.on_alert_storage_moved)
|
||||
self.alerts.register_handler("storage_moved_failed_alert",
|
||||
self.on_alert_storage_moved_failed)
|
||||
self.alerts.register_handler("torrent_resumed_alert",
|
||||
self.on_alert_torrent_resumed)
|
||||
self.alerts.register_handler("state_changed_alert",
|
||||
@ -192,6 +215,14 @@ class TorrentManager(component.Component):
|
||||
self.on_alert_metadata_received)
|
||||
self.alerts.register_handler("file_error_alert",
|
||||
self.on_alert_file_error)
|
||||
self.alerts.register_handler("file_completed_alert",
|
||||
self.on_alert_file_completed)
|
||||
self.alerts.register_handler("fastresume_rejected_alert",
|
||||
self.on_alert_fastresume_rejected)
|
||||
|
||||
# Define timers
|
||||
self.save_state_timer = LoopingCall(self.save_state)
|
||||
self.save_resume_data_timer = LoopingCall(self.save_resume_data)
|
||||
|
||||
def start(self):
|
||||
# Get the pluginmanager reference
|
||||
@ -200,14 +231,12 @@ class TorrentManager(component.Component):
|
||||
# Run the old state upgrader before loading state
|
||||
deluge.core.oldstateupgrader.OldStateUpgrader()
|
||||
|
||||
# Try to load the state from file
|
||||
# Try to load the state from file.
|
||||
self.load_state()
|
||||
|
||||
# Save the state every 5 minutes
|
||||
self.save_state_timer = LoopingCall(self.save_state)
|
||||
# Save the state and resume data every ~3 minutes.
|
||||
self.save_state_timer.start(200, False)
|
||||
self.save_resume_data_timer = LoopingCall(self.save_resume_data)
|
||||
self.save_resume_data_timer.start(190)
|
||||
self.save_resume_data_timer.start(190, False)
|
||||
|
||||
def stop(self):
|
||||
# Stop timers
|
||||
@ -256,16 +285,13 @@ class TorrentManager(component.Component):
|
||||
|
||||
def update(self):
|
||||
for torrent_id, torrent in self.torrents.items():
|
||||
if self.config["stop_seed_at_ratio"] or torrent.options["stop_at_ratio"] and torrent.state not in ("Checking", "Allocating", "Paused", "Queued"):
|
||||
if torrent.options["stop_at_ratio"] and torrent.state not in ("Checking", "Allocating", "Paused", "Queued"):
|
||||
# If the global setting is set, but the per-torrent isn't.. Just skip to the next torrent
|
||||
# This is so that a user can turn-off the stop at ratio option on a per-torrent basis
|
||||
if self.config["stop_seed_at_ratio"] and not torrent.options["stop_at_ratio"]:
|
||||
if not torrent.options["stop_at_ratio"]:
|
||||
continue
|
||||
stop_ratio = self.config["stop_seed_ratio"]
|
||||
if torrent.options["stop_at_ratio"]:
|
||||
stop_ratio = torrent.options["stop_ratio"]
|
||||
if torrent.get_ratio() >= stop_ratio and torrent.is_finished:
|
||||
if self.config["remove_seed_at_ratio"] or torrent.options["remove_at_ratio"]:
|
||||
if torrent.get_ratio() >= torrent.options["stop_ratio"] and torrent.is_finished:
|
||||
if torrent.options["remove_at_ratio"]:
|
||||
self.remove(torrent_id)
|
||||
break
|
||||
if not torrent.handle.is_paused():
|
||||
@ -374,9 +400,44 @@ class TorrentManager(component.Component):
|
||||
resume_data = self.legacy_get_resume_data_from_file(state.torrent_id)
|
||||
self.legacy_delete_resume_data(state.torrent_id)
|
||||
|
||||
add_torrent_params["resume_data"] = resume_data
|
||||
if resume_data:
|
||||
add_torrent_params["resume_data"] = resume_data
|
||||
else:
|
||||
# We have a torrent_info object so we're not loading from state.
|
||||
# We have a torrent_info object or magnet uri so we're not loading from state.
|
||||
if torrent_info:
|
||||
add_torrent_id = str(torrent_info.info_hash())
|
||||
# If this torrent id is already in the session, merge any additional trackers.
|
||||
if add_torrent_id in self.get_torrent_list():
|
||||
log.info("Merging trackers for torrent (%s) already in session...", add_torrent_id)
|
||||
# Don't merge trackers if either torrent has private flag set
|
||||
if torrent_info.priv() or self[add_torrent_id].get_status(["private"])["private"]:
|
||||
log.info("Merging trackers abandoned: Torrent has private flag set.")
|
||||
return
|
||||
|
||||
add_torrent_trackers = []
|
||||
for value in torrent_info.trackers():
|
||||
tracker = {}
|
||||
tracker["url"] = value.url
|
||||
tracker["tier"] = value.tier
|
||||
add_torrent_trackers.append(tracker)
|
||||
|
||||
torrent_trackers = {}
|
||||
tracker_list = []
|
||||
for tracker in self[add_torrent_id].get_status(["trackers"])["trackers"]:
|
||||
torrent_trackers[(tracker["url"])] = tracker
|
||||
tracker_list.append(tracker)
|
||||
|
||||
added_tracker = 0
|
||||
for tracker in add_torrent_trackers:
|
||||
if tracker['url'] not in torrent_trackers:
|
||||
tracker_list.append(tracker)
|
||||
added_tracker += 1
|
||||
|
||||
if added_tracker:
|
||||
log.info("%s tracker(s) merged into torrent.", added_tracker)
|
||||
self[add_torrent_id].set_trackers(tracker_list)
|
||||
return
|
||||
|
||||
# Check if options is None and load defaults
|
||||
if options == None:
|
||||
options = TorrentOptions()
|
||||
@ -388,12 +449,19 @@ class TorrentManager(component.Component):
|
||||
# Check for renamed files and if so, rename them in the torrent_info
|
||||
# before adding to the session.
|
||||
if options["mapped_files"]:
|
||||
for index, name in options["mapped_files"].items():
|
||||
log.debug("renaming file index %s to %s", index, name)
|
||||
torrent_info.rename_file(index, utf8_encoded(name))
|
||||
for index, fname in options["mapped_files"].items():
|
||||
try:
|
||||
fname = unicode(fname, "utf-8")
|
||||
except TypeError:
|
||||
pass
|
||||
fname = deluge.core.torrent.sanitize_filepath(fname)
|
||||
log.debug("renaming file index %s to %s", index, fname)
|
||||
try:
|
||||
torrent_info.rename_file(index, fname)
|
||||
except TypeError:
|
||||
torrent_info.rename_file(index, fname.encode("utf-8"))
|
||||
|
||||
add_torrent_params["ti"] = torrent_info
|
||||
add_torrent_params["resume_data"] = ""
|
||||
|
||||
#log.info("Adding torrent: %s", filename)
|
||||
log.debug("options: %s", options)
|
||||
@ -418,7 +486,8 @@ class TorrentManager(component.Component):
|
||||
handle = None
|
||||
try:
|
||||
if magnet:
|
||||
handle = lt.add_magnet_uri(self.session, magnet, add_torrent_params)
|
||||
magnet_uri = utf8_encoded(magnet.strip())
|
||||
handle = lt.add_magnet_uri(self.session, magnet_uri, add_torrent_params)
|
||||
else:
|
||||
handle = self.session.add_torrent(add_torrent_params)
|
||||
except RuntimeError, e:
|
||||
@ -442,10 +511,17 @@ class TorrentManager(component.Component):
|
||||
|
||||
component.resume("AlertManager")
|
||||
|
||||
# Store the orignal resume_data, in case of errors.
|
||||
if resume_data:
|
||||
self.resume_data[torrent.torrent_id] = resume_data
|
||||
|
||||
# Resume the torrent if needed
|
||||
if not options["add_paused"]:
|
||||
torrent.resume()
|
||||
|
||||
# Add to queued torrents set
|
||||
self.queued_torrents.add(torrent.torrent_id)
|
||||
|
||||
# Write the .torrent file to the state directory
|
||||
if filedump:
|
||||
try:
|
||||
@ -476,6 +552,7 @@ class TorrentManager(component.Component):
|
||||
# Emit the torrent_added signal
|
||||
component.get("EventManager").emit(TorrentAddedEvent(torrent.torrent_id))
|
||||
|
||||
log.info("Torrent %s added by user: %s", torrent.get_status(["name"])["name"], component.get("RPCServer").get_session_user())
|
||||
return torrent.torrent_id
|
||||
|
||||
def load_torrent(self, torrent_id):
|
||||
@ -511,8 +588,9 @@ class TorrentManager(component.Component):
|
||||
:raises InvalidTorrentError: if the torrent_id is not in the session
|
||||
|
||||
"""
|
||||
|
||||
if torrent_id not in self.torrents:
|
||||
try:
|
||||
torrent_name = self.torrents[torrent_id].get_status(["name"])["name"]
|
||||
except KeyError:
|
||||
raise InvalidTorrentError("torrent_id not in session")
|
||||
|
||||
# Emit the signal to the clients
|
||||
@ -551,10 +629,17 @@ class TorrentManager(component.Component):
|
||||
# Stop the looping call
|
||||
self.torrents[torrent_id].prev_status_cleanup_loop.stop()
|
||||
|
||||
# Remove from set if it wasn't finished
|
||||
if not self.torrents[torrent_id].is_finished:
|
||||
try:
|
||||
self.queued_torrents.remove(torrent_id)
|
||||
except KeyError:
|
||||
log.debug("%s isn't in queued torrents set?", torrent_id)
|
||||
|
||||
# Remove the torrent from deluge's session
|
||||
try:
|
||||
del self.torrents[torrent_id]
|
||||
except KeyError, ValueError:
|
||||
except (KeyError, ValueError):
|
||||
return False
|
||||
|
||||
# Save the session state
|
||||
@ -562,26 +647,29 @@ class TorrentManager(component.Component):
|
||||
|
||||
# Emit the signal to the clients
|
||||
component.get("EventManager").emit(TorrentRemovedEvent(torrent_id))
|
||||
|
||||
log.info("Torrent %s removed by user: %s", torrent_name, component.get("RPCServer").get_session_user())
|
||||
return True
|
||||
|
||||
def load_state(self):
|
||||
"""Load the state of the TorrentManager from the torrents.state file"""
|
||||
state = TorrentManagerState()
|
||||
|
||||
try:
|
||||
log.debug("Opening torrent state file for load.")
|
||||
state_file = open(
|
||||
os.path.join(get_config_dir(), "state", "torrents.state"), "rb")
|
||||
state = cPickle.load(state_file)
|
||||
state_file.close()
|
||||
except (EOFError, IOError, Exception), e:
|
||||
log.warning("Unable to load state file: %s", e)
|
||||
filepath = os.path.join(get_config_dir(), "state", "torrents.state")
|
||||
log.debug("Opening torrent state file for load.")
|
||||
for _filepath in (filepath, filepath + ".bak"):
|
||||
try:
|
||||
state_file = open(_filepath, "rb")
|
||||
state = cPickle.load(state_file)
|
||||
state_file.close()
|
||||
except (EOFError, IOError, Exception, cPickle.UnpicklingError), e:
|
||||
log.warning("Unable to load state file: %s", e)
|
||||
state = TorrentManagerState()
|
||||
else:
|
||||
log.info("Successfully loaded state file: %s", _filepath)
|
||||
break
|
||||
|
||||
# Try to use an old state
|
||||
try:
|
||||
state_tmp = TorrentState()
|
||||
if dir(state.torrents[0]) != dir(state_tmp):
|
||||
if state.torrents and dir(state.torrents[0]) != dir(state_tmp):
|
||||
for attr in (set(dir(state_tmp)) - set(dir(state.torrents[0]))):
|
||||
for s in state.torrents:
|
||||
setattr(s, attr, getattr(state_tmp, attr, None))
|
||||
@ -590,7 +678,7 @@ class TorrentManager(component.Component):
|
||||
|
||||
# Reorder the state.torrents list to add torrents in the correct queue
|
||||
# order.
|
||||
state.torrents.sort(key=operator.attrgetter("queue"))
|
||||
state.torrents.sort(key=operator.attrgetter("queue"), reverse=self.config["queue_new_to_top"])
|
||||
|
||||
resume_data = self.load_resume_data_file()
|
||||
|
||||
@ -602,6 +690,7 @@ class TorrentManager(component.Component):
|
||||
log.error("Torrent state file is either corrupt or incompatible! %s", e)
|
||||
break
|
||||
|
||||
self.session_started = True
|
||||
component.get("EventManager").emit(SessionStartedEvent())
|
||||
|
||||
def save_state(self):
|
||||
@ -609,9 +698,14 @@ class TorrentManager(component.Component):
|
||||
state = TorrentManagerState()
|
||||
# Create the state for each Torrent and append to the list
|
||||
for torrent in self.torrents.values():
|
||||
paused = False
|
||||
if torrent.state == "Paused":
|
||||
if self.session.is_paused():
|
||||
paused = torrent.handle.is_paused()
|
||||
elif torrent.forced_error:
|
||||
paused = torrent.forced_error.was_paused
|
||||
elif torrent.state == "Paused":
|
||||
paused = True
|
||||
else:
|
||||
paused = False
|
||||
|
||||
torrent_state = TorrentState(
|
||||
torrent.torrent_id,
|
||||
@ -640,27 +734,38 @@ class TorrentManager(component.Component):
|
||||
)
|
||||
state.torrents.append(torrent_state)
|
||||
|
||||
# If the state hasn't changed, no need to save it
|
||||
if self.prev_saved_state == state:
|
||||
return
|
||||
|
||||
# Pickle the TorrentManagerState object
|
||||
filepath = os.path.join(get_config_dir(), "state", "torrents.state")
|
||||
filepath_tmp = filepath + ".tmp"
|
||||
filepath_bak = filepath + ".bak"
|
||||
|
||||
try:
|
||||
log.debug("Saving torrent state file.")
|
||||
state_file = open(os.path.join(get_config_dir(),
|
||||
"state", "torrents.state.new"), "wb")
|
||||
os.remove(filepath_bak)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
log.debug("Creating backup of state at: %s", filepath_bak)
|
||||
os.rename(filepath, filepath_bak)
|
||||
except OSError, ex:
|
||||
log.error("Unable to backup %s to %s: %s", filepath, filepath_bak, ex)
|
||||
try:
|
||||
log.info("Saving the state at: %s", filepath)
|
||||
state_file = open(filepath_tmp, "wb", 0)
|
||||
cPickle.dump(state, state_file)
|
||||
state_file.flush()
|
||||
os.fsync(state_file.fileno())
|
||||
state_file.close()
|
||||
except IOError:
|
||||
log.warning("Unable to save state file.")
|
||||
return True
|
||||
|
||||
# We have to move the 'torrents.state.new' file to 'torrents.state'
|
||||
try:
|
||||
shutil.move(
|
||||
os.path.join(get_config_dir(), "state", "torrents.state.new"),
|
||||
os.path.join(get_config_dir(), "state", "torrents.state"))
|
||||
except IOError:
|
||||
log.warning("Unable to save state file.")
|
||||
return True
|
||||
os.rename(filepath_tmp, filepath)
|
||||
self.prev_saved_state = state
|
||||
except IOError, ex:
|
||||
log.error("Unable to save %s: %s", filepath, ex)
|
||||
if os.path.isfile(filepath_bak):
|
||||
log.info("Restoring backup of state from: %s", filepath_bak)
|
||||
os.rename(filepath_bak, filepath)
|
||||
|
||||
# We return True so that the timer thread will continue
|
||||
return True
|
||||
@ -680,15 +785,20 @@ class TorrentManager(component.Component):
|
||||
self.num_resume_data = len(torrent_ids)
|
||||
|
||||
def load_resume_data_file(self):
|
||||
resume_data = {}
|
||||
try:
|
||||
log.debug("Opening torrents fastresume file for load.")
|
||||
fastresume_file = open(os.path.join(get_config_dir(), "state",
|
||||
"torrents.fastresume"), "rb")
|
||||
resume_data = lt.bdecode(fastresume_file.read())
|
||||
fastresume_file.close()
|
||||
except (EOFError, IOError, Exception), e:
|
||||
log.warning("Unable to load fastresume file: %s", e)
|
||||
filepath = os.path.join(get_config_dir(), "state", "torrents.fastresume")
|
||||
log.debug("Opening torrents fastresume file for load.")
|
||||
for _filepath in (filepath, filepath + ".bak"):
|
||||
try:
|
||||
fastresume_file = open(_filepath, "rb")
|
||||
resume_data = lt.bdecode(fastresume_file.read())
|
||||
fastresume_file.close()
|
||||
except (EOFError, IOError, Exception), e:
|
||||
if self.torrents:
|
||||
log.warning("Unable to load fastresume file: %s", e)
|
||||
resume_data = None
|
||||
else:
|
||||
log.info("Successfully loaded fastresume file: %s", _filepath)
|
||||
break
|
||||
|
||||
# If the libtorrent bdecode doesn't happen properly, it will return None
|
||||
# so we need to make sure we return a {}
|
||||
@ -712,7 +822,9 @@ class TorrentManager(component.Component):
|
||||
if self.num_resume_data or not self.resume_data:
|
||||
return
|
||||
|
||||
path = os.path.join(get_config_dir(), "state", "torrents.fastresume")
|
||||
filepath = os.path.join(get_config_dir(), "state", "torrents.fastresume")
|
||||
filepath_tmp = filepath + ".tmp"
|
||||
filepath_bak = filepath + ".bak"
|
||||
|
||||
# First step is to load the existing file and update the dictionary
|
||||
if resume_data is None:
|
||||
@ -722,14 +834,64 @@ class TorrentManager(component.Component):
|
||||
self.resume_data = {}
|
||||
|
||||
try:
|
||||
log.debug("Saving fastresume file: %s", path)
|
||||
fastresume_file = open(path, "wb")
|
||||
os.remove(filepath_bak)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
log.debug("Creating backup of fastresume at: %s", filepath_bak)
|
||||
os.rename(filepath, filepath_bak)
|
||||
except OSError, ex:
|
||||
log.error("Unable to backup %s to %s: %s", filepath, filepath_bak, ex)
|
||||
try:
|
||||
log.info("Saving the fastresume at: %s", filepath)
|
||||
fastresume_file = open(filepath_tmp, "wb", 0)
|
||||
fastresume_file.write(lt.bencode(resume_data))
|
||||
fastresume_file.flush()
|
||||
os.fsync(fastresume_file.fileno())
|
||||
fastresume_file.close()
|
||||
except IOError:
|
||||
log.warning("Error trying to save fastresume file")
|
||||
os.rename(filepath_tmp, filepath)
|
||||
except IOError, ex:
|
||||
log.error("Unable to save %s: %s", filepath, ex)
|
||||
if os.path.isfile(filepath_bak):
|
||||
log.info("Restoring backup of fastresume from: %s", filepath_bak)
|
||||
os.rename(filepath_bak, filepath)
|
||||
|
||||
def remove_empty_folders(self, torrent_id, folder):
|
||||
"""
|
||||
Recursively removes folders but only if they are empty.
|
||||
Cleans up after libtorrent folder renames.
|
||||
|
||||
"""
|
||||
try:
|
||||
info = self.torrents[torrent_id].get_status(['save_path'])
|
||||
except KeyError:
|
||||
raise InvalidTorrentError("torrent_id not in session")
|
||||
# Regex removes leading slashes that causes join function to ignore save_path
|
||||
folder_full_path = os.path.join(info['save_path'], re.sub("^/*", "", folder))
|
||||
folder_full_path = os.path.normpath(folder_full_path)
|
||||
|
||||
try:
|
||||
if not os.listdir(folder_full_path):
|
||||
os.removedirs(folder_full_path)
|
||||
log.debug("Removed Empty Folder %s", folder_full_path)
|
||||
else:
|
||||
for root, dirs, files in os.walk(folder_full_path, topdown=False):
|
||||
for name in dirs:
|
||||
try:
|
||||
os.removedirs(os.path.join(root, name))
|
||||
log.debug("Removed Empty Folder %s", os.path.join(root, name))
|
||||
except OSError, (errno, strerror):
|
||||
from errno import ENOTEMPTY
|
||||
if errno == ENOTEMPTY:
|
||||
# Error raised if folder is not empty
|
||||
log.debug("%s", strerror)
|
||||
|
||||
except OSError, (errno, strerror):
|
||||
log.debug("Cannot Remove Folder: %s (ErrNo %s)", strerror, errno)
|
||||
|
||||
def get_queue_position(self, torrent_id):
|
||||
"""Get queue position of torrent"""
|
||||
return self.torrents[torrent_id].get_queue_position()
|
||||
|
||||
def queue_top(self, torrent_id):
|
||||
"""Queue torrent to top"""
|
||||
@ -749,7 +911,7 @@ class TorrentManager(component.Component):
|
||||
|
||||
def queue_down(self, torrent_id):
|
||||
"""Queue torrent down one position"""
|
||||
if self.torrents[torrent_id].get_queue_position() == (len(self.torrents) - 1):
|
||||
if self.torrents[torrent_id].get_queue_position() == (len(self.queued_torrents) - 1):
|
||||
return False
|
||||
|
||||
self.torrents[torrent_id].handle.queue_position_down()
|
||||
@ -757,7 +919,7 @@ class TorrentManager(component.Component):
|
||||
|
||||
def queue_bottom(self, torrent_id):
|
||||
"""Queue torrent to bottom"""
|
||||
if self.torrents[torrent_id].get_queue_position() == (len(self.torrents) - 1):
|
||||
if self.torrents[torrent_id].get_queue_position() == (len(self.queued_torrents) - 1):
|
||||
return False
|
||||
|
||||
self.torrents[torrent_id].handle.queue_position_bottom()
|
||||
@ -790,28 +952,34 @@ class TorrentManager(component.Component):
|
||||
log.debug("on_alert_torrent_finished")
|
||||
try:
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
except:
|
||||
return
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
log.debug("%s is finished..", torrent_id)
|
||||
|
||||
# Get the total_download and if it's 0, do not move.. It's likely
|
||||
# that the torrent wasn't downloaded, but just added.
|
||||
total_download = torrent.get_status(["total_payload_download"])["total_payload_download"]
|
||||
|
||||
# Move completed download to completed folder if needed
|
||||
if not torrent.is_finished and total_download:
|
||||
move_path = None
|
||||
|
||||
if torrent.options["move_completed"]:
|
||||
move_path = torrent.options["move_completed_path"]
|
||||
if torrent.options["download_location"] != move_path:
|
||||
torrent.move_storage(move_path)
|
||||
|
||||
torrent.is_finished = True
|
||||
component.get("EventManager").emit(TorrentFinishedEvent(torrent_id))
|
||||
|
||||
torrent.update_state()
|
||||
if not torrent.is_finished and total_download:
|
||||
# Move completed download to completed folder if needed
|
||||
if torrent.options["move_completed"] and \
|
||||
torrent.options["download_location"] != torrent.options["move_completed_path"]:
|
||||
self.waiting_on_finish_moving.append(torrent_id)
|
||||
torrent.move_storage(torrent.options["move_completed_path"])
|
||||
else:
|
||||
torrent.is_finished = True
|
||||
component.get("EventManager").emit(TorrentFinishedEvent(torrent_id))
|
||||
else:
|
||||
torrent.is_finished = True
|
||||
|
||||
# Torrent is no longer part of the queue
|
||||
try:
|
||||
self.queued_torrents.remove(torrent_id)
|
||||
except KeyError:
|
||||
# Sometimes libtorrent fires a TorrentFinishedEvent twice
|
||||
log.debug("%s isn't in queued torrents set?", torrent_id)
|
||||
|
||||
# Only save resume data if it was actually downloaded something. Helps
|
||||
# on startup with big queues with lots of seeding torrents. Libtorrent
|
||||
@ -825,14 +993,11 @@ class TorrentManager(component.Component):
|
||||
log.debug("on_alert_torrent_paused")
|
||||
try:
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
except:
|
||||
return
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
# Set the torrent state
|
||||
old_state = torrent.state
|
||||
torrent.update_state()
|
||||
if torrent.state != old_state:
|
||||
component.get("EventManager").emit(TorrentStateChangedEvent(torrent_id, torrent.state))
|
||||
|
||||
# Don't save resume data for each torrent after self.stop() was called.
|
||||
# We save resume data in bulk in self.stop() in this case.
|
||||
@ -850,11 +1015,19 @@ class TorrentManager(component.Component):
|
||||
except:
|
||||
return
|
||||
|
||||
# Check to see if we're forcing a recheck and set it back to paused
|
||||
# if necessary
|
||||
if torrent.forcing_recheck:
|
||||
torrent.forcing_recheck = False
|
||||
if torrent.forcing_recheck_paused:
|
||||
torrent.handle.pause()
|
||||
torrent.set_trackers(torrent.trackers, reannounce=False)
|
||||
|
||||
# Set the torrent state
|
||||
torrent.update_state()
|
||||
|
||||
def on_alert_tracker_reply(self, alert):
|
||||
log.debug("on_alert_tracker_reply: %s", alert.message().decode("utf8"))
|
||||
log.debug("on_alert_tracker_reply: %s", decode_string(alert.message()))
|
||||
try:
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
except:
|
||||
@ -862,7 +1035,7 @@ class TorrentManager(component.Component):
|
||||
|
||||
# Set the tracker status for the torrent
|
||||
if alert.message() != "Got peers from DHT":
|
||||
torrent.set_tracker_status(_("Announce OK"))
|
||||
torrent.set_tracker_status("Announce OK")
|
||||
|
||||
# Check to see if we got any peer information from the tracker
|
||||
if alert.handle.status().num_complete == -1 or \
|
||||
@ -878,7 +1051,7 @@ class TorrentManager(component.Component):
|
||||
return
|
||||
|
||||
# Set the tracker status for the torrent
|
||||
torrent.set_tracker_status(_("Announce Sent"))
|
||||
torrent.set_tracker_status("Announce Sent")
|
||||
|
||||
def on_alert_tracker_warning(self, alert):
|
||||
log.debug("on_alert_tracker_warning")
|
||||
@ -886,41 +1059,63 @@ class TorrentManager(component.Component):
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
except:
|
||||
return
|
||||
tracker_status = '%s: %s' % (_("Warning"), str(alert.message()))
|
||||
tracker_status = '%s: %s' % ("Warning", decode_string(alert.message()))
|
||||
# Set the tracker status for the torrent
|
||||
torrent.set_tracker_status(tracker_status)
|
||||
|
||||
def on_alert_tracker_error(self, alert):
|
||||
log.debug("on_alert_tracker_error")
|
||||
"""Alert handler for libtorrent tracker_error_alert"""
|
||||
error_message = decode_string(alert.msg)
|
||||
# If alert.msg is empty then it's a '-1' code so fallback to a.e.message. Note that alert.msg
|
||||
# cannot be replaced by a.e.message because the code is included in the string (for non-'-1').
|
||||
if not error_message:
|
||||
error_message = decode_string(alert.error.message())
|
||||
log.debug("Tracker Error Alert: %s [%s]", decode_string(alert.message()), error_message)
|
||||
try:
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
except:
|
||||
except (RuntimeError, KeyError):
|
||||
return
|
||||
tracker_status = "%s: %s" % (_("Error"), alert.msg)
|
||||
torrent.set_tracker_status(tracker_status)
|
||||
|
||||
torrent.set_tracker_status("Error: %s" % error_message)
|
||||
|
||||
def on_alert_storage_moved(self, alert):
|
||||
log.debug("on_alert_storage_moved")
|
||||
try:
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
except:
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
torrent = self.torrents[torrent_id]
|
||||
except (RuntimeError, KeyError):
|
||||
return
|
||||
torrent.set_save_path(alert.handle.save_path())
|
||||
torrent.set_save_path(os.path.normpath(alert.handle.save_path()))
|
||||
torrent.set_move_completed(False)
|
||||
|
||||
if torrent_id in self.waiting_on_finish_moving:
|
||||
self.waiting_on_finish_moving.remove(torrent_id)
|
||||
torrent.is_finished = True
|
||||
component.get("EventManager").emit(TorrentFinishedEvent(torrent_id))
|
||||
|
||||
def on_alert_storage_moved_failed(self, alert):
|
||||
"""Alert handler for libtorrent storage_moved_failed_alert"""
|
||||
log.debug("on_alert_storage_moved_failed: %s", decode_string(alert.message()))
|
||||
try:
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
torrent = self.torrents[torrent_id]
|
||||
except (RuntimeError, KeyError):
|
||||
return
|
||||
|
||||
log.error("Torrent %s, %s", torrent_id, decode_string(alert.message()))
|
||||
if torrent_id in self.waiting_on_finish_moving:
|
||||
self.waiting_on_finish_moving.remove(torrent_id)
|
||||
torrent.is_finished = True
|
||||
component.get("EventManager").emit(TorrentFinishedEvent(torrent_id))
|
||||
|
||||
def on_alert_torrent_resumed(self, alert):
|
||||
log.debug("on_alert_torrent_resumed")
|
||||
try:
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
except:
|
||||
return
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
torrent.is_finished = torrent.handle.is_seed()
|
||||
old_state = torrent.state
|
||||
torrent.update_state()
|
||||
if torrent.state != old_state:
|
||||
# We need to emit a TorrentStateChangedEvent too
|
||||
component.get("EventManager").emit(TorrentStateChangedEvent(torrent_id, torrent.state))
|
||||
component.get("EventManager").emit(TorrentResumedEvent(torrent_id))
|
||||
|
||||
def on_alert_state_changed(self, alert):
|
||||
@ -931,18 +1126,17 @@ class TorrentManager(component.Component):
|
||||
except:
|
||||
return
|
||||
|
||||
old_state = torrent.state
|
||||
torrent.update_state()
|
||||
# Only emit a state changed event if the state has actually changed
|
||||
if torrent.state != old_state:
|
||||
component.get("EventManager").emit(TorrentStateChangedEvent(torrent_id, torrent.state))
|
||||
|
||||
# Torrent may need to download data after checking.
|
||||
if torrent.state in ('Checking', 'Checking Resume Data', 'Downloading'):
|
||||
torrent.is_finished = False
|
||||
self.queued_torrents.add(torrent_id)
|
||||
|
||||
def on_alert_save_resume_data(self, alert):
|
||||
log.debug("on_alert_save_resume_data")
|
||||
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
|
||||
try:
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
torrent = self.torrents[torrent_id]
|
||||
except:
|
||||
return
|
||||
@ -956,7 +1150,7 @@ class TorrentManager(component.Component):
|
||||
self.save_resume_data_file()
|
||||
|
||||
def on_alert_save_resume_data_failed(self, alert):
|
||||
log.debug("on_alert_save_resume_data_failed: %s", alert.message())
|
||||
log.debug("on_alert_save_resume_data_failed: %s", decode_string(alert.message()))
|
||||
try:
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
except:
|
||||
@ -967,15 +1161,34 @@ class TorrentManager(component.Component):
|
||||
|
||||
self.save_resume_data_file()
|
||||
|
||||
def on_alert_fastresume_rejected(self, alert):
|
||||
"""Alert handler for libtorrent fastresume_rejected_alert"""
|
||||
alert_msg = decode_string(alert.message())
|
||||
log.error("on_alert_fastresume_rejected: %s", alert_msg)
|
||||
try:
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
torrent = self.torrents[torrent_id]
|
||||
except (RuntimeError, KeyError):
|
||||
return
|
||||
|
||||
if alert.error.value() == 134:
|
||||
if not os.path.isdir(torrent.options["download_location"]):
|
||||
error_msg = "Unable to locate Download Folder!"
|
||||
else:
|
||||
error_msg = "Missing or invalid torrent data!"
|
||||
else:
|
||||
error_msg = "Problem with resume data: %s" % alert_msg.split(":", 1)[1].strip()
|
||||
|
||||
torrent.force_error_state(error_msg, restart_to_resume=True)
|
||||
|
||||
def on_alert_file_renamed(self, alert):
|
||||
log.debug("on_alert_file_renamed")
|
||||
log.debug("index: %s name: %s", alert.index, alert.name.decode("utf8"))
|
||||
log.debug("index: %s name: %s", alert.index, decode_string(alert.name))
|
||||
try:
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
except:
|
||||
return
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
|
||||
# We need to see if this file index is in a waiting_on_folder list
|
||||
folder_rename = False
|
||||
@ -985,6 +1198,8 @@ class TorrentManager(component.Component):
|
||||
if len(wait_on_folder[2]) == 1:
|
||||
# This is the last alert we were waiting for, time to send signal
|
||||
component.get("EventManager").emit(TorrentFolderRenamedEvent(torrent_id, wait_on_folder[0], wait_on_folder[1]))
|
||||
# Empty folders are removed after libtorrent folder renames
|
||||
self.remove_empty_folders(torrent_id, wait_on_folder[0])
|
||||
del torrent.waiting_on_folder_rename[i]
|
||||
self.save_resume_data((torrent_id,))
|
||||
break
|
||||
@ -1003,12 +1218,21 @@ class TorrentManager(component.Component):
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
except:
|
||||
return
|
||||
torrent.write_torrentfile()
|
||||
torrent.on_metadata_received()
|
||||
|
||||
def on_alert_file_error(self, alert):
|
||||
log.debug("on_alert_file_error: %s", alert.message())
|
||||
log.debug("on_alert_file_error: %s", decode_string(alert.message()))
|
||||
try:
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
except:
|
||||
return
|
||||
torrent.update_state()
|
||||
|
||||
def on_alert_file_completed(self, alert):
|
||||
log.debug("file_completed_alert: %s", decode_string(alert.message()))
|
||||
try:
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
except:
|
||||
return
|
||||
component.get("EventManager").emit(
|
||||
TorrentFileCompletedEvent(torrent_id, alert.index))
|
||||
|
Before Width: | Height: | Size: 722 B After Width: | Height: | Size: 722 B |
BIN
deluge/data/icons/hicolor/22x22/apps/deluge-panel.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
deluge/data/icons/hicolor/24x24/apps/deluge-panel.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
@ -1,2 +0,0 @@
|
||||
From: http://famfamfam.com/lab/icons/flags/
|
||||
"These flag icons are available for free use for any purpose with no requirement for attribution."
|
Before Width: | Height: | Size: 643 B After Width: | Height: | Size: 444 B |
Before Width: | Height: | Size: 408 B After Width: | Height: | Size: 275 B |
Before Width: | Height: | Size: 604 B After Width: | Height: | Size: 415 B |
Before Width: | Height: | Size: 591 B After Width: | Height: | Size: 431 B |
Before Width: | Height: | Size: 643 B After Width: | Height: | Size: 492 B |
Before Width: | Height: | Size: 600 B After Width: | Height: | Size: 427 B |
Before Width: | Height: | Size: 497 B After Width: | Height: | Size: 322 B |
Before Width: | Height: | Size: 488 B After Width: | Height: | Size: 359 B |
Before Width: | Height: | Size: 428 B After Width: | Height: | Size: 381 B |
Before Width: | Height: | Size: 836 B After Width: | Height: | Size: 582 B |
Before Width: | Height: | Size: 506 B After Width: | Height: | Size: 354 B |
Before Width: | Height: | Size: 647 B After Width: | Height: | Size: 514 B |
Before Width: | Height: | Size: 403 B After Width: | Height: | Size: 286 B |
Before Width: | Height: | Size: 673 B After Width: | Height: | Size: 554 B |
Before Width: | Height: | Size: 524 B After Width: | Height: | Size: 376 B |
Before Width: | Height: | Size: 663 B After Width: | Height: | Size: 470 B |
Before Width: | Height: | Size: 589 B After Width: | Height: | Size: 411 B |
Before Width: | Height: | Size: 593 B After Width: | Height: | Size: 449 B |
Before Width: | Height: | Size: 585 B After Width: | Height: | Size: 392 B |
Before Width: | Height: | Size: 504 B After Width: | Height: | Size: 355 B |
Before Width: | Height: | Size: 449 B After Width: | Height: | Size: 286 B |
Before Width: | Height: | Size: 497 B After Width: | Height: | Size: 333 B |
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 306 B |
Before Width: | Height: | Size: 457 B After Width: | Height: | Size: 331 B |
Before Width: | Height: | Size: 675 B After Width: | Height: | Size: 548 B |
Before Width: | Height: | Size: 486 B After Width: | Height: | Size: 307 B |
Before Width: | Height: | Size: 611 B After Width: | Height: | Size: 475 B |
Before Width: | Height: | Size: 639 B After Width: | Height: | Size: 486 B |
Before Width: | Height: | Size: 500 B After Width: | Height: | Size: 335 B |
Before Width: | Height: | Size: 593 B After Width: | Height: | Size: 458 B |
Before Width: | Height: | Size: 526 B After Width: | Height: | Size: 386 B |
Before Width: | Height: | Size: 631 B After Width: | Height: | Size: 460 B |
Before Width: | Height: | Size: 512 B After Width: | Height: | Size: 380 B |
Before Width: | Height: | Size: 443 B After Width: | Height: | Size: 313 B |
Before Width: | Height: | Size: 514 B After Width: | Height: | Size: 365 B |
Before Width: | Height: | Size: 600 B After Width: | Height: | Size: 457 B |
Before Width: | Height: | Size: 628 B After Width: | Height: | Size: 459 B |
Before Width: | Height: | Size: 625 B After Width: | Height: | Size: 480 B |
Before Width: | Height: | Size: 528 B After Width: | Height: | Size: 448 B |
Before Width: | Height: | Size: 614 B After Width: | Height: | Size: 443 B |
Before Width: | Height: | Size: 521 B After Width: | Height: | Size: 366 B |
Before Width: | Height: | Size: 367 B After Width: | Height: | Size: 231 B |
Before Width: | Height: | Size: 453 B After Width: | Height: | Size: 300 B |
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 474 B |
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 316 B |
Before Width: | Height: | Size: 525 B After Width: | Height: | Size: 335 B |
Before Width: | Height: | Size: 472 B After Width: | Height: | Size: 339 B |
Before Width: | Height: | Size: 483 B After Width: | Height: | Size: 322 B |
Before Width: | Height: | Size: 477 B After Width: | Height: | Size: 339 B |
Before Width: | Height: | Size: 439 B After Width: | Height: | Size: 305 B |
Before Width: | Height: | Size: 563 B After Width: | Height: | Size: 416 B |
Before Width: | Height: | Size: 529 B After Width: | Height: | Size: 403 B |
Before Width: | Height: | Size: 608 B After Width: | Height: | Size: 459 B |
Before Width: | Height: | Size: 428 B After Width: | Height: | Size: 318 B |
Before Width: | Height: | Size: 476 B After Width: | Height: | Size: 349 B |
Before Width: | Height: | Size: 545 B After Width: | Height: | Size: 353 B |
Before Width: | Height: | Size: 572 B After Width: | Height: | Size: 411 B |
Before Width: | Height: | Size: 495 B After Width: | Height: | Size: 342 B |
Before Width: | Height: | Size: 620 B After Width: | Height: | Size: 488 B |
Before Width: | Height: | Size: 508 B After Width: | Height: | Size: 361 B |
Before Width: | Height: | Size: 582 B After Width: | Height: | Size: 442 B |
Before Width: | Height: | Size: 500 B After Width: | Height: | Size: 345 B |
Before Width: | Height: | Size: 429 B After Width: | Height: | Size: 285 B |
Before Width: | Height: | Size: 465 B After Width: | Height: | Size: 344 B |
Before Width: | Height: | Size: 508 B After Width: | Height: | Size: 377 B |
Before Width: | Height: | Size: 653 B After Width: | Height: | Size: 480 B |
Before Width: | Height: | Size: 469 B After Width: | Height: | Size: 336 B |
Before Width: | Height: | Size: 592 B After Width: | Height: | Size: 424 B |
Before Width: | Height: | Size: 489 B After Width: | Height: | Size: 355 B |
Before Width: | Height: | Size: 610 B After Width: | Height: | Size: 485 B |
Before Width: | Height: | Size: 648 B After Width: | Height: | Size: 498 B |