Compare commits
1141 Commits
archive/1.
...
deluge-1.3
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
e7907a63ee | |||
cee416b1b5 | |||
53a9c217e7 | |||
d6c8b13041 | |||
ba03356151 | |||
d28cf93686 | |||
452656e09d | |||
9c460266ac | |||
bb0bd36c51 | |||
5ea7e21943 | |||
facb4669e3 | |||
e4ef17975c | |||
c4b20aa595 | |||
bc028998d7 | |||
7e7da94a6e | |||
53930e0898 | |||
b13adbafbf | |||
556c8b831a | |||
a6d10562f1 | |||
a8ac98bb37 | |||
d7fa383da3 | |||
6ded75caef | |||
88929d4821 | |||
8747611e9e | |||
acba442ddb | |||
5b0f93ba72 | |||
619092aee0 | |||
89fb5b02d7 | |||
75b69b1f11 | |||
f5eddafea9 | |||
d49e1eda79 | |||
e86d2ad4e2 | |||
5effdd4cd4 | |||
c2b4fad389 | |||
82a5b5262c | |||
1b0e08b3d9 | |||
49ec3a1535 | |||
23544bd6b2 | |||
5766e04987 | |||
0857af98d0 | |||
5e78daf726 | |||
1efb700ed8 | |||
e0153e8bdc | |||
b46562d932 | |||
5c8eccdd82 | |||
c45583e8e7 | |||
ce46dcdf7a | |||
63f5c8b116 | |||
111bea19d9 | |||
68f0e9ddc7 | |||
5e1f6a8738 | |||
28a313e74e | |||
5b6faa47b0 | |||
0f12200f6f | |||
7420c6f12f | |||
fb0adbcded | |||
59b7a175eb | |||
cbcd277d91 | |||
269e0b89b6 | |||
698a5ff475 | |||
fd122db7b7 | |||
67905b6f5b | |||
7df7f13e26 | |||
066d199c78 | |||
c8c5e3449a | |||
00dc5f0128 | |||
cd10555a8a | |||
158feaa8e0 | |||
3310cc636f | |||
9849a16d2c | |||
bf28b3ac31 | |||
759ae6356d | |||
c0fd70a856 | |||
028a35bfc8 | |||
fec735f948 | |||
9f185da446 | |||
8414b9cfa9 | |||
91f44c2ad3 | |||
31304d3397 | |||
310b4bad31 | |||
9e7dde8997 | |||
dfde04561f | |||
545e4ef717 | |||
b1cf238489 | |||
7fe5d37094 | |||
a7940d5bf9 | |||
e0e2b1b350 | |||
a58c391675 | |||
9f46958f20 | |||
dd82f95975 | |||
67e27b9b7a | |||
1cc315878a | |||
412b96ba55 | |||
d6b7917350 | |||
8b23af062a | |||
3b1d038d2d | |||
a47f9bc8dd | |||
466b245fdf | |||
bfdaa47aff | |||
c3290b4ac2 | |||
342001c642 | |||
b4404feed7 | |||
85c0725f83 | |||
29634505e4 | |||
faa1752d04 | |||
2edf19c187 | |||
42b3dc7dde | |||
80f151be94 | |||
c8ada0ba07 | |||
e05384909a | |||
3a12a50f3e | |||
5b0ce6b3d8 | |||
9cde1f3e45 | |||
1825ce09fb | |||
62158d7861 | |||
4de8e57f56 | |||
a3d9b93480 | |||
7e12222d33 | |||
bf2fc64ce0 | |||
3fdfedb7f7 | |||
01e847b997 | |||
50162694b5 | |||
ade5f596f4 | |||
8b7c1681ae | |||
2376e857d2 | |||
d024c293ed | |||
54617db03f | |||
70580e35db | |||
8d02fc3db6 | |||
112b0dc1f0 | |||
3fcdcc8eec | |||
7711239452 | |||
283cff7852 | |||
fb4bfe7656 | |||
070443f811 | |||
76483bf766 | |||
dd1716c240 | |||
c176ff900f | |||
9e8d588a05 | |||
0fbbf4ac6c | |||
61dd9a5589 | |||
9a632fc3d3 | |||
e0f2c2473e | |||
80e480854e | |||
86232cac8e | |||
bba825703b | |||
3f3f7bb5b4 | |||
047bdf9e3e | |||
0ae609c6df | |||
76fa8e707a | |||
fdc7d3d7fc | |||
35dfcf3a77 | |||
f21dd242f6 | |||
8bafc9f966 | |||
901d2d715c | |||
02b71451c6 | |||
6e737518d8 | |||
addda6cfcc | |||
afa283cd2d | |||
84b33c3418 | |||
a15500d472 | |||
7d4c791241 | |||
4be615b084 | |||
8e4d88f03c | |||
38c85cf7bb | |||
bc165133d0 | |||
117fe3bb43 | |||
5dab17df89 | |||
3560dac792 | |||
c970a80030 | |||
09de50ec18 | |||
78a1ef0cc5 | |||
d2d9269c87 | |||
0d091cdacc | |||
ac7a1f0065 | |||
772653d872 | |||
ce23ff34a7 | |||
c1200ed63f | |||
af17346ac6 | |||
e9a922f829 | |||
387ea4a911 | |||
7f1dadf3cd | |||
2417e8537b | |||
91692bc966 | |||
4296344502 | |||
7053163f88 | |||
14d9f6b7ba | |||
692ec5bb1b | |||
6039280fb5 | |||
48876fa45c | |||
ae54d3fa18 | |||
47509ee705 | |||
47a80526b3 | |||
183064f857 | |||
7c5dacba5f | |||
1c807ad7c8 | |||
6d83556ba8 | |||
de9ba4986d | |||
2956a7db54 | |||
76de427b96 | |||
e9df745dd0 | |||
bf224b0556 | |||
79e62e6069 | |||
7b84f54974 | |||
4ca14d68c1 | |||
ee9c7d1971 | |||
7dff81b60b | |||
66bd2e3030 | |||
da9af84dc1 | |||
0b44023f92 | |||
dd8400558c | |||
61b5659972 | |||
c987b74d61 | |||
c430ef9a84 | |||
7be5b4c8bc | |||
ab38ca2ad4 | |||
4d5d31a2b0 | |||
eaa03a6f2c | |||
01bb9c4df5 | |||
46a6576c68 | |||
0182641bbb | |||
e939f17654 | |||
17903c78d4 | |||
676c59c318 | |||
4ec10575f3 | |||
323638a751 | |||
ce0dc49572 | |||
3f0edee17a | |||
ea65974dc2 | |||
ddd0f40d4c | |||
12b2f47762 | |||
250471fe47 | |||
1a26287a5a | |||
5c42cfbf64 | |||
1cfc4f522e | |||
73db03a33b | |||
efe2c06347 | |||
53fd0a57ee | |||
017d1e058e | |||
e7e480cf3e | |||
c978c6d016 | |||
8fb56f0410 | |||
211d0bee5d | |||
e7c7b8f4db | |||
78f9f22a40 | |||
c8f2173a04 | |||
256ae0745c | |||
8dad06cfbd | |||
b8270be10f | |||
afbca066d7 | |||
ee8531aa24 | |||
7d27b847fb | |||
73ec9b0338 | |||
4005003003 | |||
3e4f2f94dd | |||
d39b5bd071 | |||
52ea19249c | |||
a93bbc35a1 | |||
79ab0f118f | |||
9d13b17a3c | |||
6aacc6e75c | |||
c03f519f9a | |||
c6caae848f | |||
5945b24476 | |||
606b623d73 | |||
16b832f7ab | |||
b0714f625f | |||
670ad51de1 | |||
986e632475 | |||
f08c0e053c | |||
fd9dc2d892 | |||
67bcaa267a | |||
5a9b671c85 | |||
af9eeb02b0 | |||
ff1ad9d764 | |||
7e5e28ea2b | |||
51555cab83 | |||
dd866f07de | |||
26defff7fc | |||
7947773a88 | |||
ed03721789 | |||
0f6cab42a8 | |||
4031b9f94b | |||
a23648c657 | |||
9672480d39 | |||
7a115622df | |||
f7071b4428 | |||
296d790421 | |||
228d623aef | |||
e83737805d | |||
2952a5a7a3 | |||
21431f18e1 | |||
4929ba3c44 | |||
81d28b686f | |||
e2840148af | |||
450d526eca | |||
ae426eb0cd | |||
3e6c956ac6 | |||
3a54a9aebc | |||
f6c058dd34 | |||
cd7681b909 | |||
d2dafe4180 | |||
92837080cd | |||
97c2d0346a | |||
da9cb956a8 | |||
0ed6eb8564 | |||
f7f928f0b9 | |||
24ce77cdf0 | |||
3a56af99c0 | |||
c1bf8c1da1 | |||
f825e8996a | |||
b919613a51 | |||
38802245b6 | |||
c6da126f55 | |||
84374fd83a | |||
8de2d30de0 | |||
a961947720 | |||
ae5071d6cb | |||
00c896ff1d | |||
0f126bcbd5 | |||
57fa3d8834 | |||
e0a8fd70f5 | |||
e2b78be264 | |||
6fd3cd56ff | |||
88004c0d54 | |||
739636cc0b | |||
238e183851 | |||
8a15a18361 | |||
2ed9f97bb0 | |||
a1be15ffb2 | |||
44d3e2fa2f | |||
2ce62bf19b | |||
3a86fd7068 | |||
a69ed83e25 | |||
e5c734fb05 | |||
cfeae2baf4 | |||
0ffca9a1f6 | |||
b8521e7e28 | |||
50ae65b58a | |||
4a7782af28 | |||
5bbe8bec5c | |||
a065f95075 | |||
8dd4d2f094 | |||
9ad59d5f48 | |||
7999bd1e8c | |||
c81b1620ca | |||
6d4cf138c8 | |||
a011d6d659 | |||
1790a1cf2d | |||
b5533a22bc | |||
6750e9f122 | |||
04abf6d17b | |||
00734d14b6 | |||
bb00387903 | |||
ea365bf671 | |||
1d2a6f7f0e | |||
7bd5ba3cdb | |||
68b5f92ec0 | |||
04242ba91c | |||
eef9d8ec13 | |||
0e9a691954 | |||
9b23ce9e06 | |||
f9d2e96d39 | |||
caed5accea | |||
343521ca25 | |||
1d29f67a75 | |||
da8367de4d | |||
0647c4e3d7 | |||
976824ad99 | |||
a4e12c1d94 | |||
db4d0fabe7 | |||
25b282fdb8 | |||
518a73079a | |||
0634592bd9 | |||
3e13fe1229 | |||
7125eab8b2 | |||
57a9e925a7 | |||
4afcae325a | |||
dc764b2ad5 | |||
454321614b | |||
d39f6843c9 | |||
f63435db90 | |||
8c3b6cb0db | |||
a89d0d42aa | |||
3368c4c67d | |||
447cb52bf1 | |||
2afd0a4e97 | |||
7727a98c45 | |||
744f11e19b | |||
0a86a30c2a | |||
ec410fc1a9 | |||
7af689e57f | |||
fa0d6b1aa0 | |||
8fc4caa2f1 | |||
25afa04ba3 | |||
c202f7727f | |||
d74050e7a6 | |||
c7635b0ff0 | |||
0d069d0fe8 | |||
e441f96204 | |||
99197b2063 | |||
92d19fd58d | |||
b4b95c9423 | |||
e79f6ada2f | |||
0e0cc30128 | |||
9b26f6ebee | |||
30280b0803 | |||
fe6bcd62be | |||
14cea4fbc7 | |||
899ac7c86e | |||
39341f623f | |||
42cbf4f5c6 | |||
a3d98029f9 | |||
031f75a2bb | |||
a59332b4ef | |||
1b2d5bc6ad | |||
f14b3a8459 | |||
76fcfa498d | |||
2073ae0221 | |||
cd24acd74f | |||
1ac0403f05 | |||
90e562ff98 | |||
e010a789c4 | |||
964d85d908 | |||
3068f006e2 | |||
f4cf3d9893 | |||
708ad2e665 | |||
0819697c5b | |||
0121d721cb | |||
a82b6e4fb5 | |||
4420f2fae5 | |||
f8f9438950 | |||
e211b6feca | |||
d9b9f22998 | |||
aaa7dae18e | |||
2de185adea | |||
0e2dd9f389 | |||
cbac2fbd5a | |||
be70305365 | |||
f586b91a59 | |||
623c5ab57b | |||
ed00536468 | |||
430b96f4f5 | |||
ac66f305e7 | |||
8175b2af58 | |||
4dbc93b1fa | |||
9b97c6a578 | |||
97375f1a7c | |||
6312ad4a7e | |||
b06f46ea7c | |||
a570e67a4d | |||
221c9984d6 | |||
ccc97f83a8 | |||
d557cda55c | |||
eee6d4c030 | |||
177ec7d5c2 | |||
f30a10f2b8 | |||
1c665b7d2f | |||
717897b343 | |||
7f33292aca | |||
3e67620e55 | |||
bc8cacdbd1 | |||
ec9564d0c1 | |||
ffb241a4de | |||
d57b2b43ec | |||
19e2ab2187 | |||
8e7d4f2cd4 | |||
ef2ca43c3d | |||
8bedc613a0 | |||
cd3221baf6 | |||
e0ebffb8fa | |||
5a8c443d50 | |||
c7d52f3ce5 | |||
fad6ba2193 | |||
22c9d7c0ee | |||
3a864bcdad | |||
14f894959f | |||
6aebcef2b5 | |||
9e53e33c24 | |||
62a336b7d8 | |||
0cc00ba5df | |||
a161bbfcb6 | |||
f03d1818fa | |||
13b2e7da26 | |||
377f8cf886 | |||
72c9a46fa3 | |||
53b9cdebcd | |||
dc9e2597da | |||
bc27c2cdf7 | |||
a9631daf7f | |||
d1f6ca90fc | |||
68749a7ad5 | |||
5b94861fc9 | |||
a8697114c1 | |||
0042fb1767 | |||
b69e25e308 | |||
3c24d72489 | |||
358b5f4b19 | |||
43df21517e | |||
43a5a9111b | |||
773f65d708 | |||
cd7b5082a0 | |||
f3f3b3669f | |||
7e0d09a7bc | |||
42e904b63c | |||
35186faf78 | |||
62dfd6a664 | |||
8212a66d5a | |||
eac2a10a12 | |||
9f034657b0 | |||
f7b3e11729 | |||
b47f6badd7 | |||
2ecb233b5b | |||
040f1a5c6d | |||
a126081d2c | |||
104852d47e | |||
a2cc2cdd8f | |||
7650ebc373 | |||
a4e8d1eb46 | |||
b9d2094a15 | |||
3f383df479 | |||
1ba3955025 | |||
0d3f364aac | |||
d119fa3629 | |||
5129682727 | |||
96becf60bd | |||
9c3efd17cb | |||
19455a7adf | |||
57c96477c1 | |||
f9c61bbc11 | |||
a80c4e18e7 | |||
e73c65e602 | |||
8c283875fb | |||
1353ce6903 | |||
3136e5490f | |||
cffce4a706 | |||
1c318504cf | |||
115a7c3795 | |||
3fcb0e7ae5 | |||
a567b23262 | |||
93468f342a | |||
635260e686 | |||
94625c48b2 | |||
20dc0a5e8a | |||
e6135aa2a9 | |||
1cb42252b8 | |||
7812f7b4e4 | |||
127b577440 | |||
a5e8a9dc69 | |||
9bfa5f10b6 | |||
67c0f8609b | |||
7061e09a8e | |||
e327d87ebc | |||
c4f0920c18 | |||
adb22bdfa5 | |||
01a43544d5 | |||
8420d6105b | |||
1b7a50f88b | |||
8a6ec7232d | |||
535940e2e6 | |||
0723a77214 | |||
1f58910a38 | |||
634ecdeb1d | |||
aa86aa6fe1 | |||
789356d44d | |||
2b3bd4f1f3 | |||
ad04b2a137 | |||
1a1ab4e780 | |||
330b8b3ced | |||
bdba9cd00a | |||
f98c3adf2f | |||
ec2c5ab937 | |||
8dff2375d0 | |||
4e2c0a70c4 | |||
41353c9ae4 | |||
e73052df1c | |||
9b8282010c | |||
dd67a935cb | |||
0a84bc73c5 | |||
baa177a1b1 | |||
fa5b7e7a66 | |||
902ef3fa28 | |||
8144d15689 | |||
dd860e67f7 | |||
19d9c71b13 | |||
46906f5447 | |||
1893b92f37 | |||
fb1d7cfece | |||
59b93f4d2e | |||
6880a142e3 | |||
3d76122666 | |||
0f81e2816f | |||
59c7fcf854 | |||
f6a4d19084 | |||
ec259d6aea | |||
7983187818 | |||
2a4cf7cb56 | |||
3b07cc40bd | |||
fd24e1c17c | |||
f876c17efd | |||
ee7f4e452d | |||
7c10dd4c0e | |||
a110ad1d20 | |||
7c2a2af1f0 | |||
6d2d3c0fd0 | |||
888997372c | |||
5991abcec5 | |||
675d1219cd | |||
a1e4c51c9c | |||
93e3d2acf6 | |||
219f745e68 | |||
d6e18f7729 | |||
d27d7c6733 | |||
831b81529c | |||
826f1a2be9 | |||
e17bd472a8 | |||
de82302c67 | |||
81949449ae | |||
acc850dab9 | |||
208eef713a | |||
dbe90d2882 | |||
3153a545ca | |||
65545df485 | |||
29d01993c9 | |||
9627b7cb92 | |||
c068384845 | |||
92eb10be29 | |||
c6d4208a29 | |||
c6eaec6998 | |||
b77e846744 | |||
566d3c5ebf | |||
6eee4fb31f | |||
36e74e05ca | |||
2289b5f173 | |||
f6b5eb0a0c | |||
d70edb245b | |||
21701cb096 | |||
da2a7ef138 | |||
3f1d769ffc | |||
9c491c13cc | |||
bdc173cf4b | |||
4ae439a99a | |||
04217e16d4 | |||
4a00edc066 | |||
aa274eca74 | |||
c0b5bcc7d3 | |||
551d38c7a0 | |||
c3f433500b | |||
23c949d4ec | |||
3d85791a03 | |||
a3636ccdb7 | |||
e0111271a3 | |||
44217fd977 | |||
6274a32ca1 | |||
11c0e9a304 | |||
844f98db04 | |||
3c69822761 | |||
3a90109724 | |||
ce0968e6dc | |||
2e6b21aa85 | |||
8a5ccbc2d4 |
6
.gitattributes
vendored
6
.gitattributes
vendored
@ -4,6 +4,12 @@ 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
|
||||
.gitattributes export-ignore
|
||||
.gitmodules export-ignore
|
||||
.gitignore export-ignore
|
||||
*.py diff=python
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -6,3 +6,6 @@ dist
|
||||
*.log
|
||||
*.pyc
|
||||
*.tar.*
|
||||
_trial_temp
|
||||
deluge/i18n/*/
|
||||
*.desktop
|
||||
|
797
AUTHORS
Normal file
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 Pinto ('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 Pinto
|
||||
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
|
||||
Γιάννης Κατσαμπίρης
|
||||
Артём Попов
|
||||
Миша
|
||||
Шаймарданов Максим
|
||||
蔡查理
|
615
ChangeLog
615
ChangeLog
@ -1,229 +1,298 @@
|
||||
=== Deluge 1.2.3 (27 March 2010) ===
|
||||
=== Deluge 1.3.5 (09 April 2012) ===
|
||||
==== Core ====
|
||||
* Fix libtorrent version compare to use a proper numerical compare instead
|
||||
of string. This fixes a problem where using lt 0.14.10 wouldn't allow
|
||||
Deluge to start.
|
||||
* Fix libtorrent 0.15 compatibility
|
||||
* Fix hang on quit
|
||||
|
||||
=== Deluge 1.2.2 (19 March 2010) ===
|
||||
==== Core ====
|
||||
* Fix #1109 auto add halting when a torrent with invalid filename encoding
|
||||
is in the directory
|
||||
* Fix not properly detecting when torrent is at end of queue
|
||||
* #2049: Preserve order when moving multiple torrents in the queue
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix #1162 problem with the queued torrents dialog from not properly adding
|
||||
to the add torrent dialog if set to auto add
|
||||
* Fix #1172 notify startup complete when adding torrents externally
|
||||
* Fix issue where the gtkui sometimes won't start if there is a stale lock
|
||||
file or socket in the ipc/ directory.
|
||||
* 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
|
||||
|
||||
==== Console ====
|
||||
* Fix #1143 deluge-console crashes when tab-completing not-existent directory
|
||||
|
||||
=== Deluge 1.2.1 (20 February 2010) ===
|
||||
==== Core ====
|
||||
* Make Deluge dependent upon libtorrent 0.14.9 or greater. This is due to
|
||||
an over-downloading bug in previous versions of libtorrent.
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix #1128 Show an error dialog when unable to start a 'deluged' process
|
||||
* Increased max enterable download / upload limit to 60000 KiB/s
|
||||
|
||||
==== Console ====
|
||||
* Fix hanging when using commands from the command-line
|
||||
|
||||
==== Web ====
|
||||
* Fix #1147: Cannot upload a torrent in a multi-user system via the web interface
|
||||
* Fix #1148: Unable to save execute command in execute plugin
|
||||
|
||||
=== Deluge 1.2.0 - "Bursting like an infected kidney" (10 January 2010) ===
|
||||
==== Core ====
|
||||
* Fix file renaming
|
||||
* Fix tracker host filtering (Closes #1106)
|
||||
* Fix exceptions when gettext/locale cannot be initialized properly
|
||||
* Change share ratio calculation to use the total done instead of the all time
|
||||
downloaded value. This change will make the share ratio calculation not
|
||||
use data downloaded in failed hash checks.
|
||||
* Fix torrent info name not being utf-8 decoded when root file/folder name
|
||||
is blank
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix #1104, #735 use path.utf-8 if available
|
||||
* Fix #1114 test active port not working in classic mode
|
||||
|
||||
==== Console ====
|
||||
* Fix #1115 not showing usage for the 'debug' command
|
||||
* Fix #1116 not being able to use command aliases when not connected to a daemon
|
||||
* Fix #1117 can't use the '3' key
|
||||
|
||||
==== Windows ====
|
||||
* Fix displaying folders in the add torrent dialog
|
||||
* Fix displaying the new release dialog
|
||||
|
||||
==== Blocklist ====
|
||||
* Fix blocklist status icon not opening the blocklist preference
|
||||
page in certain locales
|
||||
* Fix blocklist not recognising comments that begin with whitespace
|
||||
* Minor speedup in parsing blocklists
|
||||
* Blocklist now attempts to download the URL multiple times before giving
|
||||
up
|
||||
* Fix blocklist not being able to open zipped blocklists with python 2.5
|
||||
|
||||
==== Web ====
|
||||
* Put the default password in the manpage.
|
||||
|
||||
=== Deluge 1.2.0_rc5 (17 December 2009) ===
|
||||
==== Web ====
|
||||
* Swap order of buttons in Remove window (Closes #1083)
|
||||
* Change the compressed js script to deluge-all.js to avoid naming
|
||||
conflicts on case-sensitive filesystems.
|
||||
* Apply patch from adios fixing the cookie date
|
||||
* Add tooltips to the statusbar items
|
||||
* Add disk usage to the statusbar
|
||||
* Add a ToggleField widget and use this on the Downloads preferences
|
||||
page allowing the movecom/copytorrent/autoadd boxes to be enabled.
|
||||
* Fix enabling plugins.
|
||||
* Implement installing plugins.
|
||||
* Update some icons
|
||||
* Fixed #1075 (changing priority on a whole folder doesn't work)
|
||||
|
||||
==== GtkUI ====
|
||||
* Attempt to register as the default magnet uri handler in GNOME on startup
|
||||
* Properly show 100.00% and reduce number of progress bar updates during a
|
||||
torrent creation
|
||||
* Fix crash in Windows when creating a torrent
|
||||
* Add button to Other preferences to associate magnet links with Deluge
|
||||
* Fix uploading plugins when the daemon is not localhost
|
||||
* Fix #692 no longer require tray password when quitting from the tray icon
|
||||
while the window is visible.
|
||||
* Fix #782 do not ask for tray password when window is not minimized to tray
|
||||
* Fix #1036 autoconnecting to localhost daemon on start-up
|
||||
* Fix issue where hosts will show up erroneously as Offline
|
||||
* Add #891 remove torrents by pressing the Delete key
|
||||
* Fix issue where stoping a daemon that you aren't connected to causes the
|
||||
gtkui to shutdown the currently connected daemon.
|
||||
* Fix #594 tray password dialog freeze in Windows
|
||||
* Made the password dialog prettier
|
||||
* Fix #1086 deprecated gtk.Tooltips usage
|
||||
* Fix #768 save tracker list for create torrent dialog
|
||||
* Fix #1095 incorrect piece size used when using some non-English languages
|
||||
|
||||
==== Console ====
|
||||
* Fix using the console in Windows, but only in command-line mode
|
||||
* Fix #823 setting config values to -1.0
|
||||
==== WebUI ====
|
||||
* #2050: Fix 'Up Speed' column not sorting
|
||||
* Hide unused Infohash button in WebUI
|
||||
|
||||
==== Label ====
|
||||
* Fix #1085 only use ints for specific options to prevent unhandled exception
|
||||
* 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 ====
|
||||
* Use the move_completed path if it is different to the save path
|
||||
in the completed event.
|
||||
* 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 ====
|
||||
* Fix the upload_plugin rpc method (was still using XML-RPC stuff)
|
||||
* Fix possible exception when upgrading from a 0.5 state file
|
||||
* Use metavar to modify the help output by optparse.
|
||||
* Partial fix for #1103 if the per-torrent option for stopping at a ratio has
|
||||
been unchecked, then do not stop it at the global setting.
|
||||
* 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
|
||||
* Implement #1012 httpdownloader supports gzip decoding
|
||||
* #496: Remove deprecated functions in favour of get_session_status()
|
||||
* #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 ====
|
||||
* Fix blocklist not working for all locales
|
||||
* Fix blocklist checking for updates when it shouldn't
|
||||
* Implement local blocklist support
|
||||
* #861: Pause transfers until blocklist is imported
|
||||
* Fix redirection not working with relative paths
|
||||
|
||||
=== Deluge 1.2.0_rc4 (24 November 2009) ===
|
||||
==== Core ====
|
||||
* Fix deleting old .fastresume files with fresh configs
|
||||
* Fix files list when using magnet uris
|
||||
* Fix loading the saved metadata when loading state with magnet uris
|
||||
==== Execute ====
|
||||
* Fix running commands with the TorrentAdded event
|
||||
* Fix the web interface
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix showing the 'Other' speed dialogs in Windows
|
||||
* Fix adding torrents from the Queued Torrents dialog
|
||||
* Fix disabling/enabling plugins after switching daemons
|
||||
* Reduce height of Add Torrent Dialog by ~80 pixels
|
||||
* Fix #1071 issue where Deluge will fail to start if there is a stale ipc lockfile
|
||||
* Fix autoconnecting to the next host in the list if the selected one isn't available
|
||||
* Fix endless loop when trying to autoconnect to an offline daemon
|
||||
* Fix exception on startup when the system tray icon is not enabled
|
||||
* Fix issue where some torrents with special characters could not be added
|
||||
* Fix issues adding magnet uris
|
||||
==== Label ====
|
||||
* Fix the web interface (#733)
|
||||
|
||||
==== Web ====
|
||||
* Fix installing the deluge-web manpage
|
||||
* Escape hyphens in the manpage
|
||||
* Migrate to ExtJS 3.1
|
||||
* Add gzip compression of HTTP data to the server
|
||||
* 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
|
||||
|
||||
==== Console ====
|
||||
* Escape hyphens in the manpage
|
||||
* Make the delete key work
|
||||
* Allow ~ to be used in the path in the add command
|
||||
* Allow commands that are .pyc files to be used
|
||||
* Fix printing info, help, etc.. on the command line
|
||||
|
||||
==== Blocklist ====
|
||||
* Force blocklist to auto-detect format when a download / import is forced
|
||||
* Fix blocklist failing on certain PeerGuardian/SafePeer lists
|
||||
|
||||
=== Deluge 1.2.0_rc3 (01 November 2009) ===
|
||||
==== Core ====
|
||||
* Fix #1047 move completed does not work if saving to non default path
|
||||
* Fix renamed files not being utf-8 encoded
|
||||
* Fix torrent name being blank when renaming root folder to /
|
||||
* Do not include an 'announce-list' key in torrents when there is only one tracker
|
||||
|
||||
==== GtkUI ====
|
||||
* Replace & with & in the details tab to ensure there are no markup errors
|
||||
* Consider 0 unlimited when displaying limits in the statusbar
|
||||
* Fix adding torrents when not showing the add torrent dialog in Windows
|
||||
* Fix crash when removing multiple torrents
|
||||
|
||||
==== Web ====
|
||||
* Fix #1046 changing auto managed via the details tab
|
||||
* Fix setting torrent options when adding
|
||||
* Fix setting file priorities when adding
|
||||
* HTML escape the field values on the details tab
|
||||
* Fix #215, make infinite eta values display in the correct order
|
||||
* Fix displaying the protocol upload speed
|
||||
* Fix #990, showing 0 as a limit when it means unlimited in the statusbar
|
||||
|
||||
==== Console ====
|
||||
* Fix displaying non-ascii strings
|
||||
* Fix #1052 crash when issuing commands while not connected to a daemon
|
||||
* Fix crash when string length makes line longer than terminal width
|
||||
* Improve 'info' command draw speed
|
||||
|
||||
=== Deluge 1.2.0_rc2 (25 October 2009) ===
|
||||
==== GtkUI ====
|
||||
* Fix path errors when adding torrents externally in Windows
|
||||
* Fix localclient authentication by stripping the lines read from the auth file
|
||||
* Do not try to call doIteration() on the reactor if it has already stopped
|
||||
* Fix 'autostart localhost if needed' option
|
||||
* Fix starting plugins when the pluginmanager is started
|
||||
* Fix #799 translate connection status
|
||||
* Fix #215 ETA sort order
|
||||
|
||||
==== Core ====
|
||||
* Fix saving torrent state on fresh configs
|
||||
|
||||
==== Web ====
|
||||
* Fix changing of the allocation in the preferences.
|
||||
* Fix updating the Connection Manager when a host is added.
|
||||
* Add a `--fork` option to allow forking the webui to the background
|
||||
* Fix the statusbar menu limits
|
||||
* Fix setting the torrent options via the options tab
|
||||
* Fix the private flag in the options tab
|
||||
|
||||
==== Console ====
|
||||
* Fix exception when using the 'halt' command
|
||||
|
||||
==== Misc ====
|
||||
* Add man pages for deluge-console, deluge-gtk and deluge-web
|
||||
|
||||
==== Extractor ====
|
||||
* Fix issue where the plugin would not stop extracting files after being disabled
|
||||
* Add option to create torrent name sub-folders in extract folder
|
||||
|
||||
=== Deluge 1.2.0_rc1 (07 October 2009) ===
|
||||
=== Deluge 1.2.0 - "Bursting like an infected kidney" (10 January 2010) ===
|
||||
==== Core ====
|
||||
* Implement new RPC protocol DelugeRPC replacing XMLRPC
|
||||
* Move to a twisted framework
|
||||
@ -261,134 +330,6 @@
|
||||
* Add new scripts for invoking UIs: deluge-gtk, deluge-web, deluge-console
|
||||
* Remove GeoIP database from the source tree
|
||||
|
||||
=== Deluge 1.1.9 - (15 June 2009) ===
|
||||
==== Core ====
|
||||
* Only move a torrent due to 'move on complete' when some data has been downloaded
|
||||
* Update libtorrent for CVE-2009-1760
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix #950 renaming a parent folder into multiple folders
|
||||
|
||||
==== WebUI ====
|
||||
* Fix remote torrent add
|
||||
|
||||
=== Deluge 1.1.8 - (21 May 2009) ===
|
||||
==== Core ====
|
||||
* Fix pause all/resume all
|
||||
* Torrent name is now changed when the root folder or file is renamed
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix high cpu usage when displaying speeds in titlebar
|
||||
* Fix showing non-utf8 encoded torrents in add torrent dialog -- this adds
|
||||
an additional dependency on chardet.
|
||||
* Fix exception when timing out trying to send notification email
|
||||
* Set some sane defaults for peers/file tabs column widths
|
||||
|
||||
==== WebUI ====
|
||||
* Fix starting when -l option is used
|
||||
|
||||
=== Deluge 1.1.7 - (25 April 2009) ===
|
||||
==== Core ====
|
||||
* Fix issue where cannot resume torrent after doing a 'Pause All'
|
||||
* Add workaround for 'address_v4 from unsigned long' bug experienced by users
|
||||
with 64-bit machines. This bug is fixed in libtorrent 0.14.3.
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix #883 segfault if locale is not using UTF-8 encoding
|
||||
* Fix for adding torrents with invalid filename encodings
|
||||
* Fix displaying IPv6 peers in the Peers tab
|
||||
* Fix starting the daemon in OS X
|
||||
* Fix loading improperly created torrents with mismatched encodings
|
||||
* Fix displaying improper progress when creating torrent
|
||||
|
||||
==== Windows ====
|
||||
* Fix freezing in create torrent dialog
|
||||
* Fix creating torrents in Windows
|
||||
* Fix free space check
|
||||
|
||||
=== Deluge 1.1.6 - (06 April 2009) ===
|
||||
==== Core ====
|
||||
* Fix udp trackers being classified as DHT source
|
||||
* Fix #855 force a resume on a torrent if a 'Force Recheck' is initiated
|
||||
* Fix #862 deluged crash when access http://localhost:58846
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix displaying torrents with non-utf8 encodings in add torrent dialog
|
||||
|
||||
==== WebUI ====
|
||||
* Fix #870 use proper config location for loading ssl cert
|
||||
|
||||
==== Misc ====
|
||||
* Add OpenSSL exception to license
|
||||
|
||||
=== Deluge 1.1.5 - (16 March 2009) ===
|
||||
==== Core ====
|
||||
* Fix config file saving when no current config file exists
|
||||
|
||||
==== GtkUI ====
|
||||
* Add 'Comments' field to the Details tab
|
||||
* Fix #841 maximum upload slots tooltip
|
||||
|
||||
=== Deluge 1.1.4 - (08 March 2009) ===
|
||||
==== Core ====
|
||||
* Fix displaying file errors when the torrent isn't paused
|
||||
* Fix issue where torrents being check would get removed due to "stop at ratio" rules
|
||||
* Fix #790 tracker hosts not correct for some .uk trackers
|
||||
* Make sure config files, resume data and state are fsync'd when saved. This should help prevent data losses on crashes/improper shutdowns.
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix hiding bottom pane when no tabs are enabled upon restart
|
||||
* Fix saving file priorities when switching torrents in the addtorrentdialog
|
||||
* Fix the allocate mode not being preserved when selecting different torrents in addtorrentdialog
|
||||
* Fix #655 issue where default torrent options wouldn't be set for new torrents added to the addtorrentdialog
|
||||
* Fix #817 email notifications fail to substitute format strings
|
||||
|
||||
==== Plugins ====
|
||||
* Label: Fix setting 'Move on completed' folder when connected to a remote daemon
|
||||
|
||||
=== Deluge 1.1.3 - (15 February 2009) ===
|
||||
==== Core ====
|
||||
* Fix issue where checking queue would stop
|
||||
* Fix announcing to SSL trackers
|
||||
|
||||
==== Misc ====
|
||||
* Fix issue when initializing gettext that would prevent deluge from starting
|
||||
* Fix logging exceptions when starting the daemon
|
||||
* Fix displaying errors when a torrent is Checking
|
||||
* Fix #790 tracker hosts not correct for some 3rd-level domain names
|
||||
|
||||
=== Deluge 1.1.2 - (31 January 2009) ===
|
||||
==== Core ====
|
||||
* Fix issue where torrents get stuck Checking
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix #761 use proper theme colours in sidebar
|
||||
* Fix saving files/peers tab state when no column is sorted
|
||||
|
||||
=== Deluge 1.1.1 - (24 January 2009) ===
|
||||
==== Core ====
|
||||
* Fix oldstateupgrader for those upgrading from 0.5.x
|
||||
* Fix setting Peer TOS byte
|
||||
* Fix setting outgoing ports
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix opening links from Help menu and others
|
||||
* Fix remembering sorted column in the torrent list
|
||||
* Fix saving Files tab and Peers tab state
|
||||
* Disable popup notification in preferences on Windows
|
||||
* Fix crashing in Add Torrent Dialog when removing torrents from the list
|
||||
* Do not allow duplicate torrents in the Add Torrent Dialog
|
||||
* Fix translating speed units in status tab when a per-torrent limit is set
|
||||
* Fix torrents not displaying properly after disconnecting and reconnecting to the daemon
|
||||
* Fix when sorting # column, downloads should be on top
|
||||
|
||||
==== Misc ====
|
||||
* Fix bdecoding some torrent files
|
||||
* Fix the -l, --logfile option
|
||||
* Fix #729 tracker icons not being saved in the correct location
|
||||
* Add support for more tracker icons
|
||||
* Fix being able to connect to a local daemon from another user account
|
||||
|
||||
=== Deluge 1.1.0 - "Time gas!" (10 January 2009) ===
|
||||
==== Core ====
|
||||
* Implement #79 ability to change outgoing port range
|
||||
|
14
DEPENDS
14
DEPENDS
@ -6,12 +6,18 @@
|
||||
* simplejson (if python < 2.6)
|
||||
* setuptools
|
||||
* gettext
|
||||
* intltool
|
||||
* pyxdg
|
||||
* geoip-database (optional)
|
||||
* libtorrent >= 0.14.9
|
||||
|
||||
=== UIs ===
|
||||
* chardet
|
||||
* geoip-database (optional)
|
||||
* setproctitle (optional)
|
||||
|
||||
* libtorrent >= 0.14, or build the included version
|
||||
|
||||
* If building included libtorrent::
|
||||
* boost >= 1.34.1
|
||||
* openssl
|
||||
* zlib
|
||||
|
||||
=== Gtk ===
|
||||
* python-notify (libnotify python wrapper)
|
||||
|
12
MANIFEST.in
Normal file
12
MANIFEST.in
Normal file
@ -0,0 +1,12 @@
|
||||
recursive-include docs/man *
|
||||
recursive-include deluge *
|
||||
recursive-include win32 *
|
||||
|
||||
recursive-exclude deluge *.egg-link
|
||||
exclude deluge/ui/web/gen_gettext.py
|
||||
exclude deluge/ui/web/css/*-debug.css
|
||||
exclude deluge/ui/web/js/build.sh
|
||||
exclude deluge/ui/web/js/Deluge*.js
|
||||
exclude deluge/ui/web/js/*-debug.js
|
||||
prune deluge/ui/web/docs
|
||||
prune deluge/scripts
|
71
README
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
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
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")
|
||||
|
256
deluge/common.py
256
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
|
||||
@ -41,6 +41,7 @@ import time
|
||||
import subprocess
|
||||
import platform
|
||||
import sys
|
||||
import chardet
|
||||
|
||||
try:
|
||||
import json
|
||||
@ -63,7 +64,22 @@ if not hasattr(json, "dumps"):
|
||||
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.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 *
|
||||
|
||||
@ -101,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():
|
||||
@ -127,11 +147,19 @@ 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:
|
||||
import xdg.BaseDirectory
|
||||
if filename:
|
||||
return os.path.join(xdg.BaseDirectory.save_config_path("deluge"), filename)
|
||||
else:
|
||||
@ -144,8 +172,20 @@ def get_default_download_dir():
|
||||
|
||||
"""
|
||||
if windows_check():
|
||||
return os.path.expanduser("~")
|
||||
return os.path.join(os.path.expanduser("~"), 'Downloads')
|
||||
else:
|
||||
from xdg.BaseDirectory import xdg_config_home
|
||||
userdir_file = os.path.join(xdg_config_home, 'user-dirs.dirs')
|
||||
try:
|
||||
for line in open(userdir_file, 'r'):
|
||||
if not line.startswith('#') and 'XDG_DOWNLOAD_DIR' in line:
|
||||
download_dir = os.path.expandvars(\
|
||||
line.partition("=")[2].rstrip().strip('"'))
|
||||
if os.path.isdir(download_dir):
|
||||
return download_dir
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
return os.environ.get("HOME")
|
||||
|
||||
def windows_check():
|
||||
@ -234,12 +274,36 @@ def fsize(fsize_b):
|
||||
"""
|
||||
fsize_kb = fsize_b / 1024.0
|
||||
if fsize_kb < 1024:
|
||||
return "%.1f KiB" % fsize_kb
|
||||
return "%.1f %s" % (fsize_kb, _("KiB"))
|
||||
fsize_mb = fsize_kb / 1024.0
|
||||
if fsize_mb < 1024:
|
||||
return "%.1f MiB" % fsize_mb
|
||||
return "%.1f %s" % (fsize_mb, _("MiB"))
|
||||
fsize_gb = fsize_mb / 1024.0
|
||||
return "%.1f GiB" % fsize_gb
|
||||
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):
|
||||
"""
|
||||
@ -273,7 +337,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):
|
||||
"""
|
||||
@ -340,21 +411,21 @@ def ftime(seconds):
|
||||
|
||||
def fdate(seconds):
|
||||
"""
|
||||
Formats a date string in the locale's date representation based on the systems timezone
|
||||
Formats a date time string in the locale's date representation based on the systems timezone
|
||||
|
||||
:param seconds: time in seconds since the Epoch
|
||||
:type seconds: float
|
||||
:returns: a string in the locale's date representation or "" if seconds < 0
|
||||
:returns: a string in the locale's datetime representation or "" if seconds < 0
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
if seconds < 0:
|
||||
return ""
|
||||
return time.strftime("%x", time.localtime(seconds))
|
||||
return time.strftime("%x %X", time.localtime(seconds))
|
||||
|
||||
def is_url(url):
|
||||
"""
|
||||
A simple regex test to check if the URL is valid
|
||||
A simple test to check if the URL is valid
|
||||
|
||||
:param url: the url to test
|
||||
:type url: string
|
||||
@ -367,8 +438,7 @@ def is_url(url):
|
||||
True
|
||||
|
||||
"""
|
||||
import re
|
||||
return bool(re.search('^(https?|ftp|udp)://', url))
|
||||
return url.partition('://')[0] in ("http", "https", "ftp", "udp")
|
||||
|
||||
def is_magnet(uri):
|
||||
"""
|
||||
@ -389,30 +459,6 @@ def is_magnet(uri):
|
||||
return True
|
||||
return False
|
||||
|
||||
def fetch_url(url):
|
||||
"""
|
||||
Downloads a torrent file from a given URL and checks the file's validity
|
||||
|
||||
:param url: the url of the .torrent file to fetch
|
||||
:type url: string
|
||||
:returns: the filepath to the downloaded file
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
import urllib
|
||||
from deluge.log import LOG as log
|
||||
try:
|
||||
filename, headers = urllib.urlretrieve(url)
|
||||
except IOError:
|
||||
log.debug("Network error while trying to fetch torrent from %s", url)
|
||||
else:
|
||||
if filename.endswith(".torrent") or headers["content-type"] ==\
|
||||
"application/x-bittorrent":
|
||||
return filename
|
||||
else:
|
||||
log.debug("URL doesn't appear to be a valid torrent file: %s", url)
|
||||
return None
|
||||
|
||||
def create_magnet_uri(infohash, name=None, trackers=[]):
|
||||
"""
|
||||
Creates a magnet uri
|
||||
@ -481,8 +527,8 @@ def free_space(path):
|
||||
sectors, bytes, free, total = map(long, win32file.GetDiskFreeSpace(path))
|
||||
return (free * sectors * bytes)
|
||||
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):
|
||||
@ -515,6 +561,93 @@ def is_ip(ip):
|
||||
except socket.error:
|
||||
return False
|
||||
|
||||
def path_join(*parts):
|
||||
"""
|
||||
An implementation of os.path.join that always uses / for the separator
|
||||
to ensure that the correct paths are produced when working with internal
|
||||
paths on Windows.
|
||||
"""
|
||||
path = ''
|
||||
for part in parts:
|
||||
if not part:
|
||||
continue
|
||||
elif part[0] == '/':
|
||||
path = part
|
||||
elif not path:
|
||||
path = part
|
||||
else:
|
||||
path += '/' + part
|
||||
return path
|
||||
|
||||
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.
|
||||
:rtype: string
|
||||
"""
|
||||
for char, escape in XML_ESCAPES:
|
||||
string = string.replace(escape, char)
|
||||
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.
|
||||
:rtype: string
|
||||
"""
|
||||
for char, escape in XML_ESCAPES:
|
||||
string = string.replace(char, escape)
|
||||
return string
|
||||
|
||||
def decode_string(s, encoding="utf8"):
|
||||
"""
|
||||
Decodes a string and re-encodes it in utf8. If it cannot decode using
|
||||
`:param:encoding` then it will try to detect the string encoding and
|
||||
decode it.
|
||||
|
||||
:param s: string to decode
|
||||
:type s: string
|
||||
:keyword encoding: the encoding to use in the decoding
|
||||
:type encoding: string
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
s = s.decode(encoding).encode("utf8", "ignore")
|
||||
except UnicodeDecodeError:
|
||||
s = s.decode(chardet.detect(s)["encoding"], "ignore").encode("utf8", "ignore")
|
||||
return s
|
||||
|
||||
def utf8_encoded(s):
|
||||
"""
|
||||
Returns a utf8 encoded string of s
|
||||
|
||||
:param s: (unicode) string to (re-)encode
|
||||
:type s: basestring
|
||||
:returns: a utf8 encoded string of s
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
if isinstance(s, str):
|
||||
s = decode_string(s)
|
||||
elif isinstance(s, unicode):
|
||||
s = s.encode("utf8", "ignore")
|
||||
return s
|
||||
|
||||
class VersionSplit(object):
|
||||
"""
|
||||
Used for comparing version numbers.
|
||||
@ -525,13 +658,15 @@ class VersionSplit(object):
|
||||
"""
|
||||
def __init__(self, ver):
|
||||
ver = ver.lower()
|
||||
vs = ver.split("_") if "_" in ver else ver.split("-")
|
||||
vs = ver.replace("_", "-").split("-")
|
||||
self.version = [int(x) for x in vs[0].split(".")]
|
||||
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):
|
||||
"""
|
||||
@ -542,19 +677,8 @@ 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 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
|
||||
# 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)
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# component.py
|
||||
#
|
||||
# Copyright (C) 2007, 2008 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2007-2010 Andrew Resch <andrewresch@gmail.com>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
@ -33,213 +33,395 @@
|
||||
#
|
||||
#
|
||||
|
||||
from twisted.internet.defer import maybeDeferred, succeed, DeferredList, fail
|
||||
from twisted.internet.task import LoopingCall
|
||||
from deluge.log import LOG as log
|
||||
|
||||
COMPONENT_STATE = [
|
||||
"Stopped",
|
||||
"Started",
|
||||
"Paused"
|
||||
]
|
||||
class ComponentAlreadyRegistered(Exception):
|
||||
pass
|
||||
|
||||
class Component(object):
|
||||
"""
|
||||
Component objects are singletons managed by the :class:`ComponentRegistry`.
|
||||
When a new Component object is instantiated, it will be automatically
|
||||
registered with the :class:`ComponentRegistry`.
|
||||
|
||||
The ComponentRegistry has the ability to start, stop, pause and shutdown the
|
||||
components registered with it.
|
||||
|
||||
**Events:**
|
||||
|
||||
**start()** - This method is called when the client has connected to a
|
||||
Deluge core.
|
||||
|
||||
**stop()** - This method is called when the client has disconnected from a
|
||||
Deluge core.
|
||||
|
||||
**update()** - This method is called every 1 second by default while the
|
||||
Componented is in a *Started* state. The interval can be
|
||||
specified during instantiation. The update() timer can be
|
||||
paused by instructing the :class:`ComponentRegistry` to pause
|
||||
this Component.
|
||||
|
||||
**shutdown()** - This method is called when the client is exiting. If the
|
||||
Component is in a "Started" state when this is called, a
|
||||
call to stop() will be issued prior to shutdown().
|
||||
|
||||
**States:**
|
||||
|
||||
A Component can be in one of these 5 states.
|
||||
|
||||
**Started** - The Component has been started by the :class:`ComponentRegistry`
|
||||
and will have it's update timer started.
|
||||
|
||||
**Starting** - The Component has had it's start method called, but it hasn't
|
||||
fully started yet.
|
||||
|
||||
**Stopped** - The Component has either been stopped or has yet to be started.
|
||||
|
||||
**Stopping** - The Component has had it's stop method called, but it hasn't
|
||||
fully stopped yet.
|
||||
|
||||
**Paused** - The Component has had it's update timer stopped, but will
|
||||
still be considered in a Started state.
|
||||
|
||||
"""
|
||||
def __init__(self, name, interval=1, depend=None):
|
||||
# Register with the ComponentRegistry
|
||||
register(name, self, depend)
|
||||
self._interval = interval
|
||||
self._timer = None
|
||||
self._state = COMPONENT_STATE.index("Stopped")
|
||||
self._name = name
|
||||
self._component_name = name
|
||||
self._component_interval = interval
|
||||
self._component_depend = depend
|
||||
self._component_state = "Stopped"
|
||||
self._component_timer = None
|
||||
self._component_starting_deferred = None
|
||||
self._component_stopping_deferred = None
|
||||
_ComponentRegistry.register(self)
|
||||
|
||||
def get_state(self):
|
||||
return self._state
|
||||
def __del__(self):
|
||||
if _ComponentRegistry:
|
||||
_ComponentRegistry.deregister(self._component_name)
|
||||
|
||||
def get_component_name(self):
|
||||
return self._name
|
||||
|
||||
def start(self):
|
||||
pass
|
||||
|
||||
def _start(self):
|
||||
self._state = COMPONENT_STATE.index("Started")
|
||||
def _component_start_timer(self):
|
||||
if hasattr(self, "update"):
|
||||
self._timer = LoopingCall(self.update)
|
||||
self._timer.start(self._interval)
|
||||
self._component_timer = LoopingCall(self.update)
|
||||
self._component_timer.start(self._component_interval)
|
||||
|
||||
def stop(self):
|
||||
pass
|
||||
def _component_start(self):
|
||||
def on_start(result):
|
||||
self._component_state = "Started"
|
||||
self._component_starting_deferred = None
|
||||
self._component_start_timer()
|
||||
return True
|
||||
|
||||
def _stop(self):
|
||||
self._state = COMPONENT_STATE.index("Stopped")
|
||||
try:
|
||||
self._timer.stop()
|
||||
except:
|
||||
pass
|
||||
def on_start_fail(result):
|
||||
self._component_state = "Stopped"
|
||||
self._component_starting_deferred = None
|
||||
log.error(result)
|
||||
return result
|
||||
|
||||
def _pause(self):
|
||||
self._state = COMPONENT_STATE.index("Paused")
|
||||
try:
|
||||
self._timer.stop()
|
||||
except:
|
||||
pass
|
||||
if self._component_state == "Stopped":
|
||||
if hasattr(self, "start"):
|
||||
self._component_state = "Starting"
|
||||
d = maybeDeferred(self.start)
|
||||
d.addCallback(on_start)
|
||||
d.addErrback(on_start_fail)
|
||||
self._component_starting_deferred = d
|
||||
else:
|
||||
d = maybeDeferred(on_start, None)
|
||||
elif self._component_state == "Starting":
|
||||
return self._component_starting_deferred
|
||||
elif self._component_state == "Started":
|
||||
d = succeed(True)
|
||||
else:
|
||||
d = fail("Cannot start a component not in a Stopped state!")
|
||||
|
||||
def _resume(self):
|
||||
self._start()
|
||||
return d
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
def _component_stop(self):
|
||||
def on_stop(result):
|
||||
self._component_state = "Stopped"
|
||||
if self._component_timer and self._component_timer.running:
|
||||
self._component_timer.stop()
|
||||
return True
|
||||
|
||||
class ComponentRegistry:
|
||||
def __init__(self):
|
||||
self.components = {}
|
||||
self.depend = {}
|
||||
def on_stop_fail(result):
|
||||
self._component_state = "Started"
|
||||
self._component_stopping_deferred = None
|
||||
log.error(result)
|
||||
return result
|
||||
|
||||
def register(self, name, obj, depend):
|
||||
"""Registers a component.. depend must be list or None"""
|
||||
log.debug("Registered %s with ComponentRegistry..", name)
|
||||
self.components[name] = obj
|
||||
if depend != None:
|
||||
self.depend[name] = depend
|
||||
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)
|
||||
|
||||
def deregister(self, name):
|
||||
"""Deregisters a component"""
|
||||
if name in self.components:
|
||||
log.debug("Deregistering Component: %s", name)
|
||||
self.stop_component(name)
|
||||
del self.components[name]
|
||||
if self._component_state == "Stopping":
|
||||
return self._component_stopping_deferred
|
||||
|
||||
def get(self, name):
|
||||
"""Returns a reference to the component 'name'"""
|
||||
return self.components[name]
|
||||
return succeed(None)
|
||||
|
||||
def _component_pause(self):
|
||||
def on_pause(result):
|
||||
self._component_state = "Paused"
|
||||
|
||||
if self._component_state == "Started":
|
||||
if self._component_timer and self._component_timer.running:
|
||||
d = maybeDeferred(self._component_timer.stop)
|
||||
d.addCallback(on_pause)
|
||||
else:
|
||||
d = succeed(None)
|
||||
elif self._component_state == "Paused":
|
||||
d = succeed(None)
|
||||
else:
|
||||
d = fail("Cannot pause a component in a non-Started state!")
|
||||
|
||||
return d
|
||||
|
||||
def _component_resume(self):
|
||||
def on_resume(result):
|
||||
self._component_state = "Started"
|
||||
|
||||
if self._component_state == "Paused":
|
||||
d = maybeDeferred(self._component_start_timer)
|
||||
d.addCallback(on_resume)
|
||||
else:
|
||||
d = fail("Component cannot be resumed from a non-Paused state!")
|
||||
|
||||
return d
|
||||
|
||||
def _component_shutdown(self):
|
||||
def on_stop(result):
|
||||
if hasattr(self, "shutdown"):
|
||||
return maybeDeferred(self.shutdown)
|
||||
return succeed(None)
|
||||
|
||||
d = self._component_stop()
|
||||
d.addCallback(on_stop)
|
||||
return d
|
||||
|
||||
def start(self):
|
||||
"""Starts all components"""
|
||||
for component in self.components.keys():
|
||||
self.start_component(component)
|
||||
|
||||
def start_component(self, name):
|
||||
"""Starts a component"""
|
||||
# Check to see if this component has any dependencies
|
||||
if self.depend.has_key(name):
|
||||
for depend in self.depend[name]:
|
||||
self.start_component(depend)
|
||||
|
||||
# Only start if the component is stopped.
|
||||
if self.components[name].get_state() == \
|
||||
COMPONENT_STATE.index("Stopped"):
|
||||
log.debug("Starting component %s..", name)
|
||||
self.components[name].start()
|
||||
self.components[name]._start()
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
"""Stops all components"""
|
||||
# We create a separate list of the keys and do an additional check to
|
||||
# make sure the key still exists in the components dict.
|
||||
# This is because components could be deregistered during a stop and
|
||||
# the dictionary would get modified while iterating through it.
|
||||
components = self.components.keys()
|
||||
for component in components:
|
||||
if component in self.components:
|
||||
self.stop_component(component)
|
||||
|
||||
def stop_component(self, component):
|
||||
if self.components[component].get_state() != \
|
||||
COMPONENT_STATE.index("Stopped"):
|
||||
log.debug("Stopping component %s..", component)
|
||||
self.components[component].stop()
|
||||
self.components[component]._stop()
|
||||
|
||||
def pause(self):
|
||||
"""Pauses all components. Stops calling update()"""
|
||||
for component in self.components.keys():
|
||||
self.pause_component(component)
|
||||
|
||||
def pause_component(self, component):
|
||||
if self.components[component].get_state() not in \
|
||||
[COMPONENT_STATE.index("Paused"), COMPONENT_STATE.index("Stopped")]:
|
||||
log.debug("Pausing component %s..", component)
|
||||
self.components[component]._pause()
|
||||
|
||||
def resume(self):
|
||||
"""Resumes all components. Starts calling update()"""
|
||||
for component in self.components.keys():
|
||||
self.resume_component(component)
|
||||
|
||||
def resume_component(self, component):
|
||||
if self.components[component].get_state() == COMPONENT_STATE.index("Paused"):
|
||||
log.debug("Resuming component %s..", component)
|
||||
self.components[component]._resume()
|
||||
pass
|
||||
|
||||
def update(self):
|
||||
"""Updates all components"""
|
||||
for component in self.components.keys():
|
||||
# Only update the component if it's started
|
||||
if self.components[component].get_state() == \
|
||||
COMPONENT_STATE.index("Started"):
|
||||
self.components[component].update()
|
||||
|
||||
return True
|
||||
pass
|
||||
|
||||
def shutdown(self):
|
||||
"""Shuts down all components. This should be called when the program
|
||||
exits so that components can do any necessary clean-up."""
|
||||
# Stop all components first
|
||||
self.stop()
|
||||
for component in self.components.keys():
|
||||
log.debug("Shutting down component %s..", component)
|
||||
try:
|
||||
self.components[component].shutdown()
|
||||
except Exception, e:
|
||||
log.debug("Unable to call shutdown()")
|
||||
log.exception(e)
|
||||
pass
|
||||
|
||||
class ComponentRegistry(object):
|
||||
"""
|
||||
The ComponentRegistry holds a list of currently registered
|
||||
:class:`Component` objects. It is used to manage the Components by
|
||||
starting, stopping, pausing and shutting them down.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.components = {}
|
||||
|
||||
def register(self, obj):
|
||||
"""
|
||||
Registers a component object with the registry. This is done
|
||||
automatically when a Component object is instantiated.
|
||||
|
||||
:param obj: the Component object
|
||||
:type obj: object
|
||||
|
||||
:raises ComponentAlreadyRegistered: if a component with the same name is already registered.
|
||||
|
||||
"""
|
||||
name = obj._component_name
|
||||
if name in self.components:
|
||||
raise ComponentAlreadyRegistered(
|
||||
"Component already registered with name %s" % name)
|
||||
|
||||
self.components[obj._component_name] = obj
|
||||
|
||||
def deregister(self, name):
|
||||
"""
|
||||
Deregisters a component from the registry. A stop will be
|
||||
issued to the component prior to deregistering it.
|
||||
|
||||
:param name: the name of the component
|
||||
:type name: string
|
||||
|
||||
"""
|
||||
|
||||
if name in self.components:
|
||||
log.debug("Deregistering Component: %s", name)
|
||||
d = self.stop([name])
|
||||
def on_stop(result, name):
|
||||
del self.components[name]
|
||||
return d.addCallback(on_stop, name)
|
||||
else:
|
||||
return succeed(None)
|
||||
|
||||
def start(self, names=[]):
|
||||
"""
|
||||
Starts Components that are currently in a Stopped state and their
|
||||
dependencies. If *names* is specified, will only start those
|
||||
Components and their dependencies and if not it will start all
|
||||
registered components.
|
||||
|
||||
:param names: a list of Components to start
|
||||
:type names: list
|
||||
|
||||
:returns: a Deferred object that will fire once all Components have been sucessfully started
|
||||
:rtype: twisted.internet.defer.Deferred
|
||||
|
||||
"""
|
||||
# Start all the components if names is empty
|
||||
if not names:
|
||||
names = self.components.keys()
|
||||
elif isinstance(names, str):
|
||||
names = [names]
|
||||
|
||||
def on_depends_started(result, name):
|
||||
return self.components[name]._component_start()
|
||||
|
||||
deferreds = []
|
||||
|
||||
for name in names:
|
||||
if self.components[name]._component_depend:
|
||||
# This component has depends, so we need to start them first.
|
||||
d = self.start(self.components[name]._component_depend)
|
||||
d.addCallback(on_depends_started, name)
|
||||
deferreds.append(d)
|
||||
else:
|
||||
deferreds.append(self.components[name]._component_start())
|
||||
|
||||
return DeferredList(deferreds)
|
||||
|
||||
def stop(self, names=[]):
|
||||
"""
|
||||
Stops Components that are currently not in a Stopped state. If
|
||||
*names* is specified, then it will only stop those Components,
|
||||
and if not it will stop all the registered Components.
|
||||
|
||||
:param names: a list of Components to start
|
||||
:type names: list
|
||||
|
||||
:returns: a Deferred object that will fire once all Components have been sucessfully stopped
|
||||
:rtype: twisted.internet.defer.Deferred
|
||||
|
||||
"""
|
||||
if not names:
|
||||
names = self.components.keys()
|
||||
elif isinstance(names, str):
|
||||
names = [names]
|
||||
|
||||
deferreds = []
|
||||
|
||||
for name in names:
|
||||
if name in self.components:
|
||||
deferreds.append(self.components[name]._component_stop())
|
||||
|
||||
return DeferredList(deferreds)
|
||||
|
||||
def pause(self, names=[]):
|
||||
"""
|
||||
Pauses Components that are currently in a Started state. If
|
||||
*names* is specified, then it will only pause those Components,
|
||||
and if not it will pause all the registered Components.
|
||||
|
||||
:param names: a list of Components to pause
|
||||
:type names: list
|
||||
|
||||
:returns: a Deferred object that will fire once all Components have been sucessfully paused
|
||||
:rtype: twisted.internet.defer.Deferred
|
||||
|
||||
"""
|
||||
if not names:
|
||||
names = self.components.keys()
|
||||
elif isinstance(names, str):
|
||||
names = [names]
|
||||
|
||||
deferreds = []
|
||||
|
||||
for name in names:
|
||||
if self.components[name]._component_state == "Started":
|
||||
deferreds.append(self.components[name]._component_pause())
|
||||
|
||||
return DeferredList(deferreds)
|
||||
|
||||
def resume(self, names=[]):
|
||||
"""
|
||||
Resumes Components that are currently in a Paused state. If
|
||||
*names* is specified, then it will only resume those Components,
|
||||
and if not it will resume all the registered Components.
|
||||
|
||||
:param names: a list of Components to resume
|
||||
:type names: list
|
||||
|
||||
:returns: a Deferred object that will fire once all Components have been sucessfully resumed
|
||||
:rtype: twisted.internet.defer.Deferred
|
||||
|
||||
"""
|
||||
if not names:
|
||||
names = self.components.keys()
|
||||
elif isinstance(names, str):
|
||||
names = [names]
|
||||
|
||||
deferreds = []
|
||||
|
||||
for name in names:
|
||||
if self.components[name]._component_state == "Paused":
|
||||
deferreds.append(self.components[name]._component_resume())
|
||||
|
||||
return DeferredList(deferreds)
|
||||
|
||||
def shutdown(self):
|
||||
"""
|
||||
Shutdowns all Components regardless of state. This will call
|
||||
:meth:`stop` on call the components prior to shutting down. This should
|
||||
be called when the program is exiting to ensure all Components have a
|
||||
chance to properly shutdown.
|
||||
|
||||
:returns: a Deferred object that will fire once all Components have been sucessfully resumed
|
||||
:rtype: twisted.internet.defer.Deferred
|
||||
|
||||
"""
|
||||
deferreds = []
|
||||
|
||||
for component in self.components.values():
|
||||
deferreds.append(component._component_shutdown())
|
||||
|
||||
return DeferredList(deferreds)
|
||||
|
||||
def update(self):
|
||||
"""
|
||||
Updates all Components that are in a Started state.
|
||||
|
||||
"""
|
||||
for component in self.components.items():
|
||||
component.update()
|
||||
|
||||
_ComponentRegistry = ComponentRegistry()
|
||||
|
||||
def register(name, obj, depend=None):
|
||||
"""Registers a component with the registry"""
|
||||
_ComponentRegistry.register(name, obj, depend)
|
||||
deregister = _ComponentRegistry.deregister
|
||||
start = _ComponentRegistry.start
|
||||
stop = _ComponentRegistry.stop
|
||||
pause = _ComponentRegistry.pause
|
||||
resume = _ComponentRegistry.resume
|
||||
update = _ComponentRegistry.update
|
||||
shutdown = _ComponentRegistry.shutdown
|
||||
|
||||
def deregister(name):
|
||||
"""Deregisters a component"""
|
||||
_ComponentRegistry.deregister(name)
|
||||
def get(name):
|
||||
"""
|
||||
Return a reference to a component.
|
||||
|
||||
def start(component=None):
|
||||
"""Starts all components"""
|
||||
if component == None:
|
||||
_ComponentRegistry.start()
|
||||
else:
|
||||
_ComponentRegistry.start_component(component)
|
||||
:param name: the Component name to get
|
||||
:type name: string
|
||||
|
||||
def stop(component=None):
|
||||
"""Stops all or specified components"""
|
||||
if component == None:
|
||||
_ComponentRegistry.stop()
|
||||
else:
|
||||
_ComponentRegistry.stop_component(component)
|
||||
:returns: the Component object
|
||||
:rtype: object
|
||||
|
||||
def pause(component=None):
|
||||
"""Pauses all or specificed components"""
|
||||
if component == None:
|
||||
_ComponentRegistry.pause()
|
||||
else:
|
||||
_ComponentRegistry.pause_component(component)
|
||||
:raises KeyError: if the Component does not exist
|
||||
|
||||
def resume(component=None):
|
||||
"""Resumes all or specificed components"""
|
||||
if component == None:
|
||||
_ComponentRegistry.resume()
|
||||
else:
|
||||
_ComponentRegistry.resume_component(component)
|
||||
|
||||
def update():
|
||||
"""Updates all components"""
|
||||
_ComponentRegistry.update()
|
||||
|
||||
def shutdown():
|
||||
"""Shutdowns all components"""
|
||||
_ComponentRegistry.shutdown()
|
||||
|
||||
def get(component):
|
||||
"""Return a reference to the component"""
|
||||
return _ComponentRegistry.get(component)
|
||||
"""
|
||||
return _ComponentRegistry.components[name]
|
||||
|
@ -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
|
||||
@ -93,13 +93,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
|
||||
@ -119,8 +119,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 +146,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 +188,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 +205,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 +258,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):
|
||||
"""
|
||||
@ -348,21 +362,21 @@ what is currently in the config and it could not convert the value
|
||||
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 +385,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)
|
||||
|
||||
@ -396,26 +410,24 @@ what is currently in the config and it could not convert the value
|
||||
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 +436,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.
|
||||
|
@ -66,9 +66,17 @@ class AlertManager(component.Component):
|
||||
# handlers is a dictionary of lists {"alert_type": [handler1,h2,..]}
|
||||
self.handlers = {}
|
||||
|
||||
self.delayed_calls = []
|
||||
|
||||
def update(self):
|
||||
self.delayed_calls = [dc for dc in self.delayed_calls if dc.active()]
|
||||
self.handle_alerts()
|
||||
|
||||
def stop(self):
|
||||
for dc in self.delayed_calls:
|
||||
dc.cancel()
|
||||
self.delayed_calls = []
|
||||
|
||||
def register_handler(self, alert_type, handler):
|
||||
"""
|
||||
Registers a function that will be called when 'alert_type' is pop'd
|
||||
@ -117,7 +125,7 @@ class AlertManager(component.Component):
|
||||
if alert_type in self.handlers:
|
||||
for handler in self.handlers[alert_type]:
|
||||
if not wait:
|
||||
reactor.callLater(0, handler, alert)
|
||||
self.delayed_calls.append(reactor.callLater(0, handler, alert))
|
||||
else:
|
||||
handler(alert)
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
@ -42,11 +42,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
|
||||
@ -92,6 +95,11 @@ class Core(component.Component):
|
||||
|
||||
# Set session settings
|
||||
self.settings.send_redundant_have = True
|
||||
if deluge.common.windows_check():
|
||||
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
|
||||
@ -128,9 +136,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,8 +158,9 @@ 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)
|
||||
|
||||
@ -236,20 +248,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 occured 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
|
||||
@ -288,28 +315,6 @@ class Core(component.Component):
|
||||
log.debug("Removing torrent %s from the core.", torrent_id)
|
||||
return self.torrentmanager.remove(torrent_id, remove_data)
|
||||
|
||||
@export
|
||||
def get_stats(self):
|
||||
"""
|
||||
Deprecated: please use get_session_status()
|
||||
|
||||
"""
|
||||
warnings.warn("Use get_session_status() instead of get_stats()", DeprecationWarning)
|
||||
stats = self.get_session_status(["payload_download_rate", "payload_upload_rate",
|
||||
"dht_nodes", "has_incoming_connections", "download_rate", "upload_rate"])
|
||||
|
||||
stats.update({
|
||||
#dynamic stats:
|
||||
"num_connections":self.session.num_connections(),
|
||||
"free_space":deluge.common.free_space(self.config["download_location"]),
|
||||
#max config values:
|
||||
"max_download":self.config["max_download_speed"],
|
||||
"max_upload":self.config["max_upload_speed"],
|
||||
"max_num_connections":self.config["max_connections_global"],
|
||||
})
|
||||
|
||||
return stats
|
||||
|
||||
@export
|
||||
def get_session_status(self, keys):
|
||||
"""
|
||||
@ -407,9 +412,9 @@ class Core(component.Component):
|
||||
self.torrentmanager[torrent_id].resume()
|
||||
|
||||
@export
|
||||
def get_torrent_status(self, torrent_id, keys):
|
||||
def get_torrent_status(self, torrent_id, keys, diff=False):
|
||||
# Build the status dictionary
|
||||
status = self.torrentmanager[torrent_id].get_status(keys)
|
||||
status = self.torrentmanager[torrent_id].get_status(keys, diff)
|
||||
|
||||
# Get the leftover fields and ask the plugin manager to fill them
|
||||
leftover_fields = list(set(keys) - set(status.keys()))
|
||||
@ -418,7 +423,7 @@ class Core(component.Component):
|
||||
return status
|
||||
|
||||
@export
|
||||
def get_torrents_status(self, filter_dict, keys):
|
||||
def get_torrents_status(self, filter_dict, keys, diff=False):
|
||||
"""
|
||||
returns all torrents , optionally filtered by filter_dict.
|
||||
"""
|
||||
@ -427,7 +432,7 @@ class Core(component.Component):
|
||||
|
||||
# Get the torrent status for each torrent_id
|
||||
for torrent_id in torrent_ids:
|
||||
status_dict[torrent_id] = self.get_torrent_status(torrent_id, keys)
|
||||
status_dict[torrent_id] = self.get_torrent_status(torrent_id, keys, diff)
|
||||
|
||||
return status_dict
|
||||
|
||||
@ -490,24 +495,6 @@ class Core(component.Component):
|
||||
"""Returns the current number of connections"""
|
||||
return self.session.num_connections()
|
||||
|
||||
@export
|
||||
def get_dht_nodes(self):
|
||||
"""Returns the number of dht nodes"""
|
||||
warnings.warn("Use get_session_status().", DeprecationWarning)
|
||||
return self.session.status().dht_nodes
|
||||
|
||||
@export
|
||||
def get_download_rate(self):
|
||||
"""Returns the payload download rate"""
|
||||
warnings.warn("Use get_session_status().", DeprecationWarning)
|
||||
return self.session.status().payload_download_rate
|
||||
|
||||
@export
|
||||
def get_upload_rate(self):
|
||||
"""Returns the payload upload rate"""
|
||||
warnings.warn("Use get_session_status().", DeprecationWarning)
|
||||
return self.session.status().payload_upload_rate
|
||||
|
||||
@export
|
||||
def get_available_plugins(self):
|
||||
"""Returns a list of plugins available in the core"""
|
||||
@ -605,12 +592,6 @@ class Core(component.Component):
|
||||
"""Sets the path for the torrent to be moved when completed"""
|
||||
return self.torrentmanager[torrent_id].set_move_completed_path(value)
|
||||
|
||||
@export
|
||||
def get_health(self):
|
||||
"""Returns True if we have established incoming connections"""
|
||||
warnings.warn("Use get_session_status().", DeprecationWarning)
|
||||
return self.session.status().has_incoming_connections
|
||||
|
||||
@export
|
||||
def get_path_size(self, path):
|
||||
"""Returns the size of the file or folder 'path' and -1 if the path is
|
||||
@ -622,7 +603,7 @@ class Core(component.Component):
|
||||
webseeds, private, created_by, trackers, add_to_session):
|
||||
|
||||
log.debug("creating torrent..")
|
||||
threading.Thread(target=_create_torrent_thread,
|
||||
threading.Thread(target=self._create_torrent_thread,
|
||||
args=(
|
||||
path,
|
||||
tracker,
|
||||
@ -725,7 +706,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):
|
||||
@ -736,35 +718,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):
|
||||
@ -787,21 +782,27 @@ class Core(component.Component):
|
||||
"""
|
||||
from twisted.web.client import getPage
|
||||
|
||||
d = getPage("http://deluge-torrent.org/test_port.php?port=%s" % self.get_listen_port())
|
||||
d = getPage("http://deluge-torrent.org/test_port.php?port=%s" %
|
||||
self.get_listen_port(), timeout=30)
|
||||
|
||||
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
|
||||
|
||||
@export
|
||||
def get_free_space(self, path):
|
||||
def get_free_space(self, path=None):
|
||||
"""
|
||||
Returns the number of free bytes at path
|
||||
|
||||
:param path: the path to check free space at
|
||||
:param path: the path to check free space at, if None, use the default
|
||||
download location
|
||||
:type path: string
|
||||
|
||||
:returns: the number of free bytes at path
|
||||
@ -810,7 +811,12 @@ class Core(component.Component):
|
||||
:raises InvalidPathError: if the path is invalid
|
||||
|
||||
"""
|
||||
return deluge.common.free_space(path)
|
||||
if not path:
|
||||
path = self.config["download_location"]
|
||||
try:
|
||||
return deluge.common.free_space(path)
|
||||
except InvalidPathError:
|
||||
return 0
|
||||
|
||||
@export
|
||||
def get_libtorrent_version(self):
|
||||
|
@ -110,7 +110,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 +120,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 +133,14 @@ 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 = ""
|
||||
|
||||
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)
|
||||
|
||||
port = self.core.config["daemon_port"]
|
||||
if options and options.port:
|
||||
@ -177,17 +182,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:
|
||||
log.error("Event handler %s failed in %s", event.name, handler)
|
||||
|
||||
def register_event_handler(self, event, handler):
|
||||
"""
|
||||
|
@ -91,7 +91,7 @@ def tracker_error_filter(torrent_ids, values):
|
||||
# Check all the torrent's tracker_status for 'Error:' and only return torrent_ids
|
||||
# that have this substring in their tracker_status
|
||||
for torrent_id in torrent_ids:
|
||||
if "Error:" in tm[torrent_id].get_status(["tracker_status"])["tracker_status"]:
|
||||
if _("Error") + ":" in tm[torrent_id].get_status(["tracker_status"])["tracker_status"]:
|
||||
filtered_torrent_ids.append(torrent_id)
|
||||
|
||||
return filtered_torrent_ids
|
||||
@ -197,6 +197,7 @@ class FilterManager(component.Component):
|
||||
items[field][value] = items[field].get(value, 0) + 1
|
||||
|
||||
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:
|
||||
|
@ -96,8 +96,7 @@ class PluginManager(deluge.pluginmanagerbase.PluginManagerBase,
|
||||
try:
|
||||
status[field] = self.status_fields[field](torrent_id)
|
||||
except KeyError:
|
||||
log.warning("Status field %s is not registered with the\
|
||||
PluginManager.", field)
|
||||
pass
|
||||
return status
|
||||
|
||||
def register_status_field(self, field, function):
|
||||
|
@ -58,6 +58,7 @@ DEFAULT_PREFS = {
|
||||
"listen_ports": [6881, 6891],
|
||||
"listen_interface": "",
|
||||
"copy_torrent_file": False,
|
||||
"del_copy_torrent_file": False,
|
||||
"torrentfiles_location": deluge.common.get_default_download_dir(),
|
||||
"plugins_location": os.path.join(deluge.configmanager.get_config_dir(), "plugins"),
|
||||
"prioritize_first_last_pieces": False,
|
||||
@ -151,7 +152,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",
|
||||
@ -228,10 +228,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))
|
||||
|
||||
@ -273,8 +278,7 @@ class PreferencesManager(component.Component):
|
||||
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:
|
||||
@ -283,13 +287,11 @@ 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")
|
||||
@ -386,51 +388,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..")
|
||||
@ -466,14 +456,14 @@ 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):
|
||||
@ -490,8 +480,7 @@ class PreferencesManager(component.Component):
|
||||
|
||||
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)
|
||||
@ -513,10 +502,8 @@ class PreferencesManager(component.Component):
|
||||
|
||||
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)
|
||||
|
@ -47,7 +47,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
|
||||
@ -86,6 +90,32 @@ def export(auth_level=AUTH_LEVEL_DEFAULT):
|
||||
else:
|
||||
return wrap
|
||||
|
||||
|
||||
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] + "("
|
||||
if call[2]:
|
||||
s += ", ".join([str(x) for x in call[2]])
|
||||
if call[3]:
|
||||
if call[2]:
|
||||
s += ", "
|
||||
s += ", ".join([key + "=" + str(value) for key, value in call[3].items()])
|
||||
s += ")"
|
||||
except UnicodeEncodeError:
|
||||
return "UnicodeEncodeError, call: %s" % call
|
||||
else:
|
||||
return s
|
||||
|
||||
class DelugeError(Exception):
|
||||
pass
|
||||
|
||||
@ -113,8 +143,8 @@ 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 a thread
|
||||
with :meth:`dispatch`.
|
||||
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
|
||||
rencoded string.
|
||||
@ -151,24 +181,7 @@ class DelugeRPCProtocol(Protocol):
|
||||
if len(call) != 4:
|
||||
log.debug("Received invalid rpc request: number of items in request is %s", len(call))
|
||||
continue
|
||||
|
||||
# Format the RPCRequest message for debug printing
|
||||
try:
|
||||
s = call[1] + "("
|
||||
if call[2]:
|
||||
s += ", ".join([str(x) for x in call[2]])
|
||||
if call[3]:
|
||||
if call[2]:
|
||||
s += ", "
|
||||
s += ", ".join([key + "=" + str(value) for key, value in call[3].items()])
|
||||
s += ")"
|
||||
except UnicodeEncodeError:
|
||||
pass
|
||||
#log.debug("RPCRequest had some non-ascii text..")
|
||||
else:
|
||||
pass
|
||||
#log.debug("RPCRequest: %s", s)
|
||||
|
||||
#log.debug("RPCRequest: %s", format_request(call))
|
||||
reactor.callLater(0, self.dispatch, *call)
|
||||
|
||||
def sendData(self, data):
|
||||
@ -177,6 +190,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)))
|
||||
@ -244,7 +258,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()
|
||||
@ -273,11 +287,14 @@ 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)
|
||||
raise NotAuthorizedError("Auth level too low: %s < %s" % (auth_level, method_auth_requirement))
|
||||
# Set the session_id in the factory so that methods can know
|
||||
# which session is calling it.
|
||||
self.factory.session_id = self.transport.sessionno
|
||||
ret = self.factory.methods[method](*args, **kwargs)
|
||||
except Exception, e:
|
||||
sendError()
|
||||
@ -324,6 +341,8 @@ 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
|
||||
@ -399,6 +418,44 @@ class RPCServer(component.Component):
|
||||
"""
|
||||
return self.factory.methods.keys()
|
||||
|
||||
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.
|
||||
|
@ -36,6 +36,7 @@
|
||||
|
||||
import os
|
||||
import time
|
||||
from urllib import unquote
|
||||
from urlparse import urlparse
|
||||
|
||||
from deluge._libtorrent import lt
|
||||
@ -48,6 +49,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
|
||||
@ -72,7 +98,7 @@ class TorrentOptions(dict):
|
||||
self["file_priorities"] = []
|
||||
self["mapped_files"] = {}
|
||||
|
||||
class Torrent:
|
||||
class Torrent(object):
|
||||
"""Torrent holds information about torrents added to the libtorrent session.
|
||||
"""
|
||||
def __init__(self, handle, options, state=None, filename=None, magnet=None):
|
||||
@ -80,6 +106,16 @@ class Torrent:
|
||||
# Get the core config
|
||||
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, ...}
|
||||
self.prev_status = {}
|
||||
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
|
||||
@ -168,6 +204,11 @@ class Torrent:
|
||||
else:
|
||||
self.time_added = time.time()
|
||||
|
||||
# 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
|
||||
|
||||
log.debug("Torrent object created.")
|
||||
|
||||
## Options methods ##
|
||||
@ -219,11 +260,11 @@ class Torrent:
|
||||
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
|
||||
@ -275,7 +316,9 @@ class Torrent:
|
||||
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"])
|
||||
@ -285,12 +328,9 @@ class Torrent:
|
||||
if trackers == None:
|
||||
trackers = []
|
||||
for value in self.handle.trackers():
|
||||
if lt.version_minor < 15:
|
||||
tracker = {}
|
||||
tracker["url"] = value.url
|
||||
tracker["tier"] = value.tier
|
||||
else:
|
||||
tracker = value
|
||||
tracker = {}
|
||||
tracker["url"] = value.url
|
||||
tracker["tier"] = value.tier
|
||||
trackers.append(tracker)
|
||||
self.trackers = trackers
|
||||
self.tracker_host = None
|
||||
@ -333,7 +373,10 @@ class Torrent:
|
||||
|
||||
# Set self.state to the ltstate right away just incase we don't hit some
|
||||
# of the logic below
|
||||
self.state = str(ltstate)
|
||||
if ltstate in LTSTATE:
|
||||
self.state = LTSTATE[ltstate]
|
||||
else:
|
||||
self.state = 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())
|
||||
@ -385,15 +428,14 @@ class Torrent:
|
||||
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
|
||||
@ -467,11 +509,11 @@ class Torrent:
|
||||
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
|
||||
@ -532,8 +574,21 @@ class Torrent:
|
||||
return host
|
||||
return ""
|
||||
|
||||
def get_status(self, keys):
|
||||
"""Returns the status of the torrent based on the keys provided"""
|
||||
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():
|
||||
@ -547,6 +602,13 @@ class Torrent:
|
||||
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,
|
||||
@ -565,6 +627,8 @@ class Torrent:
|
||||
"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,
|
||||
@ -574,6 +638,7 @@ class Torrent:
|
||||
"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"],
|
||||
@ -610,7 +675,22 @@ class Torrent:
|
||||
except UnicodeDecodeError:
|
||||
return name
|
||||
|
||||
elif self.magnet:
|
||||
try:
|
||||
keys = dict([k.split('=') for k in self.magnet.split('?')[-1].split('&')])
|
||||
name = keys.get('dn')
|
||||
if not name:
|
||||
return self.torrent_id
|
||||
name = unquote(name).replace('+', ' ')
|
||||
try:
|
||||
return name.decode("utf8", "ignore")
|
||||
except UnicodeDecodeError:
|
||||
return name
|
||||
except:
|
||||
pass
|
||||
|
||||
return self.torrent_id
|
||||
|
||||
def ti_priv():
|
||||
if self.handle.has_metadata():
|
||||
return self.torrent_info.priv()
|
||||
@ -664,6 +744,24 @@ class Torrent:
|
||||
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:
|
||||
# We have a previous status dict, so lets make a diff
|
||||
status_diff = {}
|
||||
for key, value in status_dict.items():
|
||||
if key in self.prev_status[session_id]:
|
||||
if value != self.prev_status[session_id][key]:
|
||||
status_diff[key] = value
|
||||
else:
|
||||
status_diff[key] = value
|
||||
|
||||
self.prev_status[session_id] = status_dict
|
||||
return status_diff
|
||||
|
||||
self.prev_status[session_id] = status_dict
|
||||
return status_dict
|
||||
|
||||
return status_dict
|
||||
|
||||
def apply_options(self):
|
||||
@ -707,13 +805,8 @@ class Torrent:
|
||||
|
||||
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
|
||||
@ -740,11 +833,25 @@ class Torrent:
|
||||
|
||||
def move_storage(self, dest):
|
||||
"""Move a torrent's storage location"""
|
||||
if not os.path.exists(dest):
|
||||
log.error("Could not move storage for torrent %s since %s does not exist!", self.torrent_id, dest)
|
||||
return False
|
||||
|
||||
# Attempt to convert utf8 path to unicode
|
||||
# Note: Inconsistent encoding for 'dest', needs future investigation
|
||||
try:
|
||||
self.handle.move_storage(dest.encode("utf8"))
|
||||
dest_u = unicode(dest, "utf-8")
|
||||
except TypeError:
|
||||
# String is already unicode
|
||||
dest_u = dest
|
||||
|
||||
if not os.path.exists(dest_u):
|
||||
try:
|
||||
# Try to make the destination path if it doesn't exist
|
||||
os.makedirs(dest_u)
|
||||
except IOError, e:
|
||||
log.exception(e)
|
||||
log.error("Could not move storage for torrent %s since %s does not exist and could not create the directory.", self.torrent_id, dest_u)
|
||||
return False
|
||||
try:
|
||||
self.handle.move_storage(dest_u)
|
||||
except:
|
||||
return False
|
||||
|
||||
@ -806,18 +913,22 @@ class Torrent:
|
||||
|
||||
def force_recheck(self):
|
||||
"""Forces a recheck of the torrents pieces"""
|
||||
paused = self.handle.is_paused()
|
||||
try:
|
||||
self.handle.force_recheck()
|
||||
self.handle.resume()
|
||||
except Exception, e:
|
||||
log.debug("Unable to force recheck: %s", e)
|
||||
return False
|
||||
self.forcing_recheck = True
|
||||
self.forcing_recheck_paused = paused
|
||||
return True
|
||||
|
||||
def rename_files(self, filenames):
|
||||
"""Renames files in the torrent. 'filenames' should be a list of
|
||||
(index, filename) pairs."""
|
||||
for index, filename in filenames:
|
||||
filename = sanitize_filepath(filename)
|
||||
self.handle.rename_file(index, filename.encode("utf-8"))
|
||||
|
||||
def rename_folder(self, folder, new_folder):
|
||||
@ -828,8 +939,7 @@ class Torrent:
|
||||
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():
|
||||
@ -838,3 +948,14 @@ class Torrent:
|
||||
wait_on_folder[2].append(f["index"])
|
||||
self.handle.rename_file(f["index"], f["path"].replace(folder, new_folder, 1).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,21 +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.common import utf8_encoded
|
||||
|
||||
from deluge.log import LOG as log
|
||||
|
||||
@ -138,6 +138,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
|
||||
@ -150,6 +151,9 @@ class TorrentManager(component.Component):
|
||||
# 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
|
||||
|
||||
# Register set functions
|
||||
self.config.register_set_function("max_connections_per_torrent",
|
||||
self.on_set_max_connections_per_torrent)
|
||||
@ -191,6 +195,8 @@ 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)
|
||||
|
||||
def start(self):
|
||||
# Get the pluginmanager reference
|
||||
@ -225,7 +231,9 @@ class TorrentManager(component.Component):
|
||||
# torrent_id is removed from it in self.on_alert_torrent_paused()
|
||||
# before we call self.save_resume_data() here.
|
||||
save_resume_data_list = []
|
||||
for key in self.torrents.keys():
|
||||
for key in self.torrents:
|
||||
# Stop the status cleanup LoopingCall here
|
||||
self.torrents[key].prev_status_cleanup_loop.stop()
|
||||
if not self.torrents[key].handle.is_paused():
|
||||
# We set auto_managed false to prevent lt from resuming the torrent
|
||||
self.torrents[key].handle.auto_managed(False)
|
||||
@ -253,16 +261,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():
|
||||
@ -373,7 +378,35 @@ class TorrentManager(component.Component):
|
||||
|
||||
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 add_torrent_id in self.get_torrent_list():
|
||||
# Torrent already exists just append any extra trackers.
|
||||
log.debug("Torrent (%s) exists, checking for trackers to add...", add_torrent_id)
|
||||
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 = False
|
||||
for tracker in add_torrent_trackers:
|
||||
if tracker['url'] not in torrent_trackers:
|
||||
tracker_list.append(tracker)
|
||||
added_tracker = True
|
||||
|
||||
if added_tracker:
|
||||
self[add_torrent_id].set_trackers(tracker_list)
|
||||
return
|
||||
|
||||
# Check if options is None and load defaults
|
||||
if options == None:
|
||||
options = TorrentOptions()
|
||||
@ -385,9 +418,10 @@ 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, name.encode("utf-8"))
|
||||
for index, filename in options["mapped_files"].items():
|
||||
filename = deluge.core.torrent.sanitize_filepath(filename)
|
||||
log.debug("renaming file index %s to %s", index, filename)
|
||||
torrent_info.rename_file(index, utf8_encoded(filename))
|
||||
|
||||
add_torrent_params["ti"] = torrent_info
|
||||
add_torrent_params["resume_data"] = ""
|
||||
@ -401,14 +435,8 @@ class TorrentManager(component.Component):
|
||||
else:
|
||||
storage_mode = lt.storage_mode_t(1)
|
||||
|
||||
try:
|
||||
# Try to encode this as utf8 if needed
|
||||
options["download_location"] = options["download_location"].encode("utf8")
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
|
||||
# Fill in the rest of the add_torrent_params dictionary
|
||||
add_torrent_params["save_path"] = options["download_location"]
|
||||
add_torrent_params["save_path"] = utf8_encoded(options["download_location"])
|
||||
add_torrent_params["storage_mode"] = storage_mode
|
||||
add_torrent_params["paused"] = True
|
||||
add_torrent_params["auto_managed"] = False
|
||||
@ -421,7 +449,7 @@ class TorrentManager(component.Component):
|
||||
handle = None
|
||||
try:
|
||||
if magnet:
|
||||
handle = lt.add_magnet_uri(self.session, magnet, add_torrent_params)
|
||||
handle = lt.add_magnet_uri(self.session, utf8_encoded(magnet), add_torrent_params)
|
||||
else:
|
||||
handle = self.session.add_torrent(add_torrent_params)
|
||||
except RuntimeError, e:
|
||||
@ -449,6 +477,9 @@ class TorrentManager(component.Component):
|
||||
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:
|
||||
@ -479,6 +510,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):
|
||||
@ -518,6 +550,8 @@ class TorrentManager(component.Component):
|
||||
if torrent_id not in self.torrents:
|
||||
raise InvalidTorrentError("torrent_id not in session")
|
||||
|
||||
torrent_name = self.torrents[torrent_id].get_status(["name"])["name"]
|
||||
|
||||
# Emit the signal to the clients
|
||||
component.get("EventManager").emit(PreTorrentRemovedEvent(torrent_id))
|
||||
|
||||
@ -536,6 +570,28 @@ class TorrentManager(component.Component):
|
||||
# Remove the .torrent file in the state
|
||||
self.torrents[torrent_id].delete_torrentfile()
|
||||
|
||||
# Remove the torrent file from the user specified directory
|
||||
filename = self.torrents[torrent_id].filename
|
||||
if self.config["copy_torrent_file"] \
|
||||
and self.config["del_copy_torrent_file"] \
|
||||
and filename:
|
||||
try:
|
||||
users_torrent_file = os.path.join(
|
||||
self.config["torrentfiles_location"],
|
||||
filename)
|
||||
log.info("Delete user's torrent file: %s",
|
||||
users_torrent_file)
|
||||
os.remove(users_torrent_file)
|
||||
except Exception, e:
|
||||
log.warning("Unable to remove copy torrent file: %s", e)
|
||||
|
||||
# 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:
|
||||
self.queued_torrents.remove(torrent_id)
|
||||
|
||||
# Remove the torrent from deluge's session
|
||||
try:
|
||||
del self.torrents[torrent_id]
|
||||
@ -547,7 +603,7 @@ 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):
|
||||
@ -560,7 +616,7 @@ class TorrentManager(component.Component):
|
||||
os.path.join(get_config_dir(), "state", "torrents.state"), "rb")
|
||||
state = cPickle.load(state_file)
|
||||
state_file.close()
|
||||
except (EOFError, IOError, Exception), e:
|
||||
except (EOFError, IOError, Exception, cPickle.UnpicklingError), e:
|
||||
log.warning("Unable to load state file: %s", e)
|
||||
|
||||
# Try to use an old state
|
||||
@ -587,6 +643,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):
|
||||
@ -634,8 +691,8 @@ class TorrentManager(component.Component):
|
||||
state_file.flush()
|
||||
os.fsync(state_file.fileno())
|
||||
state_file.close()
|
||||
except IOError:
|
||||
log.warning("Unable to save state file.")
|
||||
except IOError, e:
|
||||
log.warning("Unable to save state file: %s", e)
|
||||
return True
|
||||
|
||||
# We have to move the 'torrents.state.new' file to 'torrents.state'
|
||||
@ -716,6 +773,43 @@ class TorrentManager(component.Component):
|
||||
except IOError:
|
||||
log.warning("Error trying to save fastresume file")
|
||||
|
||||
def remove_empty_folders(self, torrent_id, folder):
|
||||
"""
|
||||
Recursively removes folders but only if they are empty.
|
||||
Cleans up after libtorrent folder renames.
|
||||
|
||||
"""
|
||||
if torrent_id not in self.torrents:
|
||||
raise InvalidTorrentError("torrent_id is not in session")
|
||||
|
||||
info = self.torrents[torrent_id].get_status(['save_path'])
|
||||
# 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"""
|
||||
if self.torrents[torrent_id].get_queue_position() == 0:
|
||||
@ -734,7 +828,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()
|
||||
@ -742,7 +836,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()
|
||||
@ -775,9 +869,9 @@ 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
|
||||
@ -793,11 +887,14 @@ class TorrentManager(component.Component):
|
||||
if torrent.options["download_location"] != move_path:
|
||||
torrent.move_storage(move_path)
|
||||
|
||||
torrent.is_finished = True
|
||||
component.get("EventManager").emit(TorrentFinishedEvent(torrent_id))
|
||||
|
||||
torrent.is_finished = True
|
||||
torrent.update_state()
|
||||
|
||||
# Torrent is no longer part of the queue
|
||||
self.queued_torrents.remove(torrent_id)
|
||||
|
||||
# Only save resume data if it was actually downloaded something. Helps
|
||||
# on startup with big queues with lots of seeding torrents. Libtorrent
|
||||
# emits alert_torrent_finished for them, but there seems like nothing
|
||||
@ -810,9 +907,9 @@ 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()
|
||||
@ -835,6 +932,13 @@ 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()
|
||||
|
||||
# Set the torrent state
|
||||
torrent.update_state()
|
||||
|
||||
@ -890,16 +994,16 @@ class TorrentManager(component.Component):
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
except:
|
||||
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)
|
||||
|
||||
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:
|
||||
@ -917,16 +1021,20 @@ class TorrentManager(component.Component):
|
||||
|
||||
old_state = torrent.state
|
||||
torrent.update_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)
|
||||
|
||||
# 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))
|
||||
|
||||
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
|
||||
@ -957,9 +1065,9 @@ class TorrentManager(component.Component):
|
||||
log.debug("index: %s name: %s", alert.index, alert.name.decode("utf8"))
|
||||
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
|
||||
@ -969,6 +1077,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
|
||||
@ -996,3 +1106,12 @@ class TorrentManager(component.Component):
|
||||
except:
|
||||
return
|
||||
torrent.update_state()
|
||||
|
||||
def on_alert_file_completed(self, alert):
|
||||
log.debug("file_completed_alert: %s", alert.message())
|
||||
try:
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
except:
|
||||
return
|
||||
component.get("EventManager").emit(
|
||||
TorrentFileCompletedEvent(torrent_id, alert.index))
|
||||
|
@ -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."
|
BIN
deluge/data/pixmaps/loading.gif
Normal file
BIN
deluge/data/pixmaps/loading.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 723 B |
BIN
deluge/data/pixmaps/tracker_all16.png
Normal file
BIN
deluge/data/pixmaps/tracker_all16.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
BIN
deluge/data/pixmaps/tracker_warning16.png
Normal file
BIN
deluge/data/pixmaps/tracker_warning16.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 683 B |
@ -1,12 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=Deluge BitTorrent Client
|
||||
GenericName=Bittorrent Client
|
||||
Comment=Transfer files using the Bittorrent protocol
|
||||
Exec=deluge-gtk
|
||||
Icon=deluge
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Network;
|
||||
StartupNotify=true
|
||||
MimeType=application/x-bittorrent;
|
14
deluge/data/share/applications/deluge.desktop.in
Normal file
14
deluge/data/share/applications/deluge.desktop.in
Normal file
@ -0,0 +1,14 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
_Name=Deluge
|
||||
_GenericName=BitTorrent Client
|
||||
_X-GNOME-FullName=Deluge BitTorrent Client
|
||||
_Comment=Download and share files over BitTorrent
|
||||
TryExec=deluge-gtk
|
||||
Exec=deluge-gtk %U
|
||||
Icon=deluge
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Network;FileTransfer;P2P;GTK;
|
||||
StartupNotify=true
|
||||
MimeType=application/x-bittorrent;x-scheme-handler/magnet;
|
51
deluge/decorators.py
Normal file
51
deluge/decorators.py
Normal file
@ -0,0 +1,51 @@
|
||||
#
|
||||
# decorators.py
|
||||
#
|
||||
# Copyright (C) 2010 John Garland <johnnybg+deluge@gmail.com>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
# You may redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License, as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# deluge is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# In addition, as a special exception, the copyright holders give
|
||||
# permission to link the code of portions of this program with the OpenSSL
|
||||
# library.
|
||||
# You must obey the GNU General Public License in all respects for all of
|
||||
# the code used other than OpenSSL. If you modify file(s) with this
|
||||
# exception, you may extend this exception to your version of the file(s),
|
||||
# but you are not obligated to do so. If you do not wish to do so, delete
|
||||
# this exception statement from your version. If you delete this exception
|
||||
# statement from all source files in the program, then also delete it here.
|
||||
#
|
||||
#
|
||||
|
||||
from functools import wraps
|
||||
|
||||
def proxy(proxy_func):
|
||||
"""
|
||||
Factory class which returns a decorator that passes
|
||||
the decorated function to a proxy function
|
||||
|
||||
:param proxy_func: the proxy function
|
||||
:type proxy_func: function
|
||||
"""
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
return proxy_func(func, *args, **kwargs)
|
||||
return wrapper
|
||||
return decorator
|
@ -164,6 +164,22 @@ class TorrentResumedEvent(DelugeEvent):
|
||||
"""
|
||||
self._args = [torrent_id]
|
||||
|
||||
class TorrentFileCompletedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a file completes.
|
||||
|
||||
This will only work with libtorrent 0.15 or greater.
|
||||
|
||||
"""
|
||||
def __init__(self, torrent_id, index):
|
||||
"""
|
||||
:param torrent_id: the torrent_id
|
||||
:type torrent_id: string
|
||||
:param index: the file index
|
||||
:type index: int
|
||||
"""
|
||||
self._args = [torrent_id, index]
|
||||
|
||||
class NewVersionAvailableEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a more recent version of Deluge is available.
|
||||
|
@ -36,27 +36,37 @@ from twisted.web import client, http
|
||||
from twisted.web.error import PageRedirect
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.internet import reactor
|
||||
from deluge.log import setupLogger, LOG as log
|
||||
from common import get_version
|
||||
import os.path
|
||||
import zlib
|
||||
|
||||
class HTTPDownloader(client.HTTPDownloader):
|
||||
"""
|
||||
Factory class for downloading files and keeping track of progress.
|
||||
"""
|
||||
def __init__(self, url, filename, part_callback=None, headers=None):
|
||||
def __init__(self, url, filename, part_callback=None, headers=None, force_filename=False, allow_compression=True):
|
||||
"""
|
||||
:param url: the url to download from
|
||||
:type url: string
|
||||
:param filename: the filename to save the file as
|
||||
:type filename: string
|
||||
:param force_filename: forces use of the supplied filename, regardless of header content
|
||||
:type force_filename: bool
|
||||
:param part_callback: a function to be called when a part of data
|
||||
is received, it's signature should be: func(data, current_length, total_length)
|
||||
:type part_callback: function
|
||||
:param headers: any optional headers to send
|
||||
:type headers: dictionary
|
||||
"""
|
||||
self.__part_callback = part_callback
|
||||
self.part_callback = part_callback
|
||||
self.current_length = 0
|
||||
self.decoder = None
|
||||
self.value = filename
|
||||
client.HTTPDownloader.__init__(self, url, filename, headers=headers)
|
||||
self.force_filename = force_filename
|
||||
self.allow_compression = allow_compression
|
||||
agent = "Deluge/%s (http://deluge-torrent.org)" % get_version()
|
||||
client.HTTPDownloader.__init__(self, url, filename, headers=headers, agent=agent)
|
||||
|
||||
def gotStatus(self, version, status, message):
|
||||
self.code = int(status)
|
||||
@ -68,7 +78,30 @@ class HTTPDownloader(client.HTTPDownloader):
|
||||
self.total_length = int(headers["content-length"][0])
|
||||
else:
|
||||
self.total_length = 0
|
||||
elif self.code in (http.TEMPORARY_REDIRECT, http.MOVED_PERMANENTLY):
|
||||
|
||||
if self.allow_compression and "content-encoding" in headers and \
|
||||
headers["content-encoding"][0] in ("gzip", "x-gzip", "deflate"):
|
||||
# Adding 32 to the wbits enables gzip & zlib decoding (with automatic header detection)
|
||||
# Adding 16 just enables gzip decoding (no zlib)
|
||||
self.decoder = zlib.decompressobj(zlib.MAX_WBITS + 32)
|
||||
|
||||
if "content-disposition" in headers and not self.force_filename:
|
||||
new_file_name = str(headers["content-disposition"][0]).split(";")[1].split("=")[1]
|
||||
new_file_name = sanitise_filename(new_file_name)
|
||||
new_file_name = os.path.join(os.path.split(self.fileName)[0], new_file_name)
|
||||
|
||||
count = 1
|
||||
fileroot = os.path.splitext(new_file_name)[0]
|
||||
fileext = os.path.splitext(new_file_name)[1]
|
||||
while os.path.isfile(new_file_name):
|
||||
# Increment filename if already exists
|
||||
new_file_name = "%s-%s%s" % (fileroot, count, fileext)
|
||||
count += 1
|
||||
|
||||
self.fileName = new_file_name
|
||||
self.value = new_file_name
|
||||
|
||||
elif self.code in (http.MOVED_PERMANENTLY, http.FOUND, http.SEE_OTHER, http.TEMPORARY_REDIRECT):
|
||||
location = headers["location"][0]
|
||||
error = PageRedirect(self.code, location=location)
|
||||
self.noPage(Failure(error))
|
||||
@ -78,12 +111,50 @@ class HTTPDownloader(client.HTTPDownloader):
|
||||
def pagePart(self, data):
|
||||
if self.code == http.OK:
|
||||
self.current_length += len(data)
|
||||
if self.__part_callback:
|
||||
self.__part_callback(data, self.current_length, self.total_length)
|
||||
if self.decoder:
|
||||
data = self.decoder.decompress(data)
|
||||
if self.part_callback:
|
||||
self.part_callback(data, self.current_length, self.total_length)
|
||||
|
||||
return client.HTTPDownloader.pagePart(self, data)
|
||||
|
||||
def download_file(url, filename, callback=None, headers=None):
|
||||
def pageEnd(self):
|
||||
if self.decoder:
|
||||
data = self.decoder.flush()
|
||||
self.current_length -= len(data)
|
||||
self.decoder = None
|
||||
self.pagePart(data)
|
||||
|
||||
return client.HTTPDownloader.pageEnd(self)
|
||||
|
||||
def sanitise_filename(filename):
|
||||
"""
|
||||
Sanitises a filename to use as a download destination file.
|
||||
Logs any filenames that could be considered malicious.
|
||||
|
||||
:param filename: the filename to sanitise
|
||||
:type filename: string
|
||||
:returns: the sanitised filename
|
||||
:rtype: string
|
||||
"""
|
||||
|
||||
# Remove any quotes
|
||||
filename = filename.strip("'\"")
|
||||
|
||||
if os.path.basename(filename) != filename:
|
||||
# Dodgy server, log it
|
||||
log.warning("Potentially malicious server: trying to write to file '%s'" % filename)
|
||||
# Only use the basename
|
||||
filename = os.path.basename(filename)
|
||||
|
||||
filename = filename.strip()
|
||||
if filename.startswith(".") or ";" in filename or "|" in filename:
|
||||
# Dodgy server, log it
|
||||
log.warning("Potentially malicious server: trying to write to file '%s'" % filename)
|
||||
|
||||
return filename
|
||||
|
||||
def download_file(url, filename, callback=None, headers=None, force_filename=False, allow_compression=True):
|
||||
"""
|
||||
Downloads a file from a specific URL and returns a Deferred. You can also
|
||||
specify a callback function to be called as parts are received.
|
||||
@ -97,6 +168,11 @@ def download_file(url, filename, callback=None, headers=None):
|
||||
:type callback: function
|
||||
:param headers: any optional headers to send
|
||||
:type headers: dictionary
|
||||
:param force_filename: force us to use the filename specified rather than
|
||||
one the server may suggest
|
||||
:type force_filename: boolean
|
||||
:param allow_compression: allows gzip & deflate decoding
|
||||
:type allow_compression: boolean
|
||||
|
||||
:returns: the filename of the downloaded file
|
||||
:rtype: Deferred
|
||||
@ -111,8 +187,13 @@ def download_file(url, filename, callback=None, headers=None):
|
||||
for key, value in headers.items():
|
||||
headers[str(key)] = str(value)
|
||||
|
||||
if allow_compression:
|
||||
if not headers:
|
||||
headers = {}
|
||||
headers["accept-encoding"] = "deflate, gzip, x-gzip"
|
||||
|
||||
scheme, host, port, path = client._parse(url)
|
||||
factory = HTTPDownloader(url, filename, callback, headers)
|
||||
factory = HTTPDownloader(url, filename, callback, headers, force_filename, allow_compression)
|
||||
if scheme == "https":
|
||||
from twisted.internet import ssl
|
||||
reactor.connectSSL(host, port, factory, ssl.ClientContextFactory())
|
||||
|
@ -1,161 +0,0 @@
|
||||
deluge/error.py
|
||||
deluge/common.py
|
||||
deluge/rencode.py
|
||||
deluge/httpdownloader.py
|
||||
deluge/main.py
|
||||
deluge/configmanager.py
|
||||
deluge/bencode.py
|
||||
deluge/__rpcapi.py
|
||||
deluge/pluginmanagerbase.py
|
||||
deluge/metafile.py
|
||||
deluge/event.py
|
||||
deluge/__init__.py
|
||||
deluge/_libtorrent.py
|
||||
deluge/log.py
|
||||
deluge/component.py
|
||||
deluge/config.py
|
||||
deluge/plugins/pluginbase.py
|
||||
deluge/plugins/init.py
|
||||
deluge/plugins/__init__.py
|
||||
deluge/plugins/execute/setup.py
|
||||
deluge/plugins/execute/execute/common.py
|
||||
deluge/plugins/execute/execute/core.py
|
||||
deluge/plugins/execute/execute/gtkui.py
|
||||
deluge/plugins/execute/execute/__init__.py
|
||||
deluge/plugins/execute/execute/webui.py
|
||||
deluge/plugins/execute/execute/data/execute_prefs.glade
|
||||
deluge/plugins/extractor/setup.py
|
||||
deluge/plugins/extractor/extractor/common.py
|
||||
deluge/plugins/extractor/extractor/core.py
|
||||
deluge/plugins/extractor/extractor/gtkui.py
|
||||
deluge/plugins/extractor/extractor/__init__.py
|
||||
deluge/plugins/extractor/extractor/webui.py
|
||||
deluge/plugins/extractor/extractor/data/extractor_prefs.glade
|
||||
deluge/plugins/webui/setup.py
|
||||
deluge/plugins/webui/webui/common.py
|
||||
deluge/plugins/webui/webui/core.py
|
||||
deluge/plugins/webui/webui/gtkui.py
|
||||
deluge/plugins/webui/webui/__init__.py
|
||||
deluge/plugins/webui/webui/data/config.glade
|
||||
deluge/plugins/scheduler/setup.py
|
||||
deluge/plugins/scheduler/scheduler/common.py
|
||||
deluge/plugins/scheduler/scheduler/core.py
|
||||
deluge/plugins/scheduler/scheduler/gtkui.py
|
||||
deluge/plugins/scheduler/scheduler/__init__.py
|
||||
deluge/plugins/scheduler/scheduler/webui.py
|
||||
deluge/plugins/label/setup.py
|
||||
deluge/plugins/label/label/core.py
|
||||
deluge/plugins/label/label/test.py
|
||||
deluge/plugins/label/label/__init__.py
|
||||
deluge/plugins/label/label/webui.py
|
||||
deluge/plugins/label/label/data/label_pref.glade
|
||||
deluge/plugins/label/label/data/label_options.glade
|
||||
deluge/plugins/label/label/gtkui/submenu.py
|
||||
deluge/plugins/label/label/gtkui/sidebar_menu.py
|
||||
deluge/plugins/label/label/gtkui/__init__.py
|
||||
deluge/plugins/label/label/gtkui/label_config.py
|
||||
deluge/plugins/blocklist/setup.py
|
||||
deluge/plugins/blocklist/blocklist/readers.py
|
||||
deluge/plugins/blocklist/blocklist/peerguardian.py
|
||||
deluge/plugins/blocklist/blocklist/common.py
|
||||
deluge/plugins/blocklist/blocklist/detect.py
|
||||
deluge/plugins/blocklist/blocklist/core.py
|
||||
deluge/plugins/blocklist/blocklist/decompressers.py
|
||||
deluge/plugins/blocklist/blocklist/gtkui.py
|
||||
deluge/plugins/blocklist/blocklist/__init__.py
|
||||
deluge/plugins/blocklist/blocklist/webui.py
|
||||
deluge/plugins/blocklist/blocklist/data/blocklist_pref.glade
|
||||
deluge/ui/ui.py
|
||||
deluge/ui/common.py
|
||||
deluge/ui/coreconfig.py
|
||||
deluge/ui/countries.py
|
||||
deluge/ui/tracker_icons.py
|
||||
deluge/ui/client.py
|
||||
deluge/ui/__init__.py
|
||||
deluge/ui/gtkui/toolbar.py
|
||||
deluge/ui/gtkui/addtorrentdialog.py
|
||||
deluge/ui/gtkui/removetorrentdialog.py
|
||||
deluge/ui/gtkui/status_tab.py
|
||||
deluge/ui/gtkui/common.py
|
||||
deluge/ui/gtkui/peers_tab.py
|
||||
deluge/ui/gtkui/connectionmanager.py
|
||||
deluge/ui/gtkui/systemtray.py
|
||||
deluge/ui/gtkui/notification.py
|
||||
deluge/ui/gtkui/new_release_dialog.py
|
||||
deluge/ui/gtkui/details_tab.py
|
||||
deluge/ui/gtkui/options_tab.py
|
||||
deluge/ui/gtkui/torrentdetails.py
|
||||
deluge/ui/gtkui/sidebar.py
|
||||
deluge/ui/gtkui/edittrackersdialog.py
|
||||
deluge/ui/gtkui/mainwindow.py
|
||||
deluge/ui/gtkui/dialogs.py
|
||||
deluge/ui/gtkui/aboutdialog.py
|
||||
deluge/ui/gtkui/listview.py
|
||||
deluge/ui/gtkui/createtorrentdialog.py
|
||||
deluge/ui/gtkui/statusbar.py
|
||||
deluge/ui/gtkui/ipcinterface.py
|
||||
deluge/ui/gtkui/gtkui.py
|
||||
deluge/ui/gtkui/torrentview.py
|
||||
deluge/ui/gtkui/queuedtorrents.py
|
||||
deluge/ui/gtkui/filtertreeview.py
|
||||
deluge/ui/gtkui/__init__.py
|
||||
deluge/ui/gtkui/menubar.py
|
||||
deluge/ui/gtkui/files_tab.py
|
||||
deluge/ui/gtkui/preferences.py
|
||||
deluge/ui/gtkui/pluginmanager.py
|
||||
deluge/ui/gtkui/glade/main_window.glade
|
||||
deluge/ui/gtkui/glade/remove_torrent_dialog.glade
|
||||
deluge/ui/gtkui/glade/create_torrent_dialog.glade
|
||||
deluge/ui/gtkui/glade/torrent_menu.glade
|
||||
deluge/ui/gtkui/glade/edit_trackers.glade
|
||||
deluge/ui/gtkui/glade/filtertree_menu.glade
|
||||
deluge/ui/gtkui/glade/tray_menu.glade
|
||||
deluge/ui/gtkui/glade/dgtkpopups.glade
|
||||
deluge/ui/gtkui/glade/move_storage_dialog.glade
|
||||
deluge/ui/gtkui/glade/connection_manager.glade
|
||||
deluge/ui/gtkui/glade/queuedtorrents.glade
|
||||
deluge/ui/gtkui/glade/add_torrent_dialog.glade
|
||||
deluge/ui/gtkui/glade/preferences_dialog.glade
|
||||
deluge/ui/web/gen_gettext.py
|
||||
deluge/ui/web/web.py
|
||||
deluge/ui/web/common.py
|
||||
deluge/ui/web/auth.py
|
||||
deluge/ui/web/server.py
|
||||
deluge/ui/web/__init__.py
|
||||
deluge/ui/web/json_api.py
|
||||
deluge/ui/web/pluginmanager.py
|
||||
deluge/ui/console/eventlog.py
|
||||
deluge/ui/console/main.py
|
||||
deluge/ui/console/statusbars.py
|
||||
deluge/ui/console/colors.py
|
||||
deluge/ui/console/__init__.py
|
||||
deluge/ui/console/screen.py
|
||||
deluge/ui/console/commands/quit.py
|
||||
deluge/ui/console/commands/help.py
|
||||
deluge/ui/console/commands/add.py
|
||||
deluge/ui/console/commands/pause.py
|
||||
deluge/ui/console/commands/resume.py
|
||||
deluge/ui/console/commands/halt.py
|
||||
deluge/ui/console/commands/recheck.py
|
||||
deluge/ui/console/commands/cache.py
|
||||
deluge/ui/console/commands/connect.py
|
||||
deluge/ui/console/commands/plugin.py
|
||||
deluge/ui/console/commands/info.py
|
||||
deluge/ui/console/commands/__init__.py
|
||||
deluge/ui/console/commands/rm.py
|
||||
deluge/ui/console/commands/config.py
|
||||
deluge/ui/console/commands/debug.py
|
||||
deluge/core/torrent.py
|
||||
deluge/core/alertmanager.py
|
||||
deluge/core/filtermanager.py
|
||||
deluge/core/authmanager.py
|
||||
deluge/core/core.py
|
||||
deluge/core/preferencesmanager.py
|
||||
deluge/core/torrentmanager.py
|
||||
deluge/core/oldstateupgrader.py
|
||||
deluge/core/__init__.py
|
||||
deluge/core/rpcserver.py
|
||||
deluge/core/daemon.py
|
||||
deluge/core/eventmanager.py
|
||||
deluge/core/pluginmanager.py
|
||||
deluge/core/autoadd.py
|
4515
deluge/i18n/ar.po
4515
deluge/i18n/ar.po
File diff suppressed because it is too large
Load Diff
5978
deluge/i18n/ast.po
5978
deluge/i18n/ast.po
File diff suppressed because it is too large
Load Diff
5991
deluge/i18n/be.po
5991
deluge/i18n/be.po
File diff suppressed because it is too large
Load Diff
4374
deluge/i18n/bg.po
4374
deluge/i18n/bg.po
File diff suppressed because it is too large
Load Diff
6356
deluge/i18n/bn.po
6356
deluge/i18n/bn.po
File diff suppressed because it is too large
Load Diff
6402
deluge/i18n/bs.po
6402
deluge/i18n/bs.po
File diff suppressed because it is too large
Load Diff
4457
deluge/i18n/ca.po
4457
deluge/i18n/ca.po
File diff suppressed because it is too large
Load Diff
4429
deluge/i18n/cs.po
4429
deluge/i18n/cs.po
File diff suppressed because it is too large
Load Diff
6293
deluge/i18n/cy.po
6293
deluge/i18n/cy.po
File diff suppressed because it is too large
Load Diff
4663
deluge/i18n/da.po
4663
deluge/i18n/da.po
File diff suppressed because it is too large
Load Diff
4481
deluge/i18n/de.po
4481
deluge/i18n/de.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
3963
deluge/i18n/el.po
3963
deluge/i18n/el.po
File diff suppressed because it is too large
Load Diff
4413
deluge/i18n/en_AU.po
4413
deluge/i18n/en_AU.po
File diff suppressed because it is too large
Load Diff
4392
deluge/i18n/en_CA.po
4392
deluge/i18n/en_CA.po
File diff suppressed because it is too large
Load Diff
4426
deluge/i18n/en_GB.po
4426
deluge/i18n/en_GB.po
File diff suppressed because it is too large
Load Diff
6293
deluge/i18n/eo.po
6293
deluge/i18n/eo.po
File diff suppressed because it is too large
Load Diff
4494
deluge/i18n/es.po
4494
deluge/i18n/es.po
File diff suppressed because it is too large
Load Diff
4415
deluge/i18n/et.po
4415
deluge/i18n/et.po
File diff suppressed because it is too large
Load Diff
4590
deluge/i18n/eu.po
4590
deluge/i18n/eu.po
File diff suppressed because it is too large
Load Diff
5802
deluge/i18n/fa.po
5802
deluge/i18n/fa.po
File diff suppressed because it is too large
Load Diff
4253
deluge/i18n/fi.po
4253
deluge/i18n/fi.po
File diff suppressed because it is too large
Load Diff
3761
deluge/i18n/fo.po
Normal file
3761
deluge/i18n/fo.po
Normal file
File diff suppressed because it is too large
Load Diff
4632
deluge/i18n/fr.po
4632
deluge/i18n/fr.po
File diff suppressed because it is too large
Load Diff
7200
deluge/i18n/fy.po
7200
deluge/i18n/fy.po
File diff suppressed because it is too large
Load Diff
4706
deluge/i18n/gl.po
4706
deluge/i18n/gl.po
File diff suppressed because it is too large
Load Diff
4345
deluge/i18n/he.po
4345
deluge/i18n/he.po
File diff suppressed because it is too large
Load Diff
3951
deluge/i18n/hi.po
3951
deluge/i18n/hi.po
File diff suppressed because it is too large
Load Diff
5731
deluge/i18n/hr.po
5731
deluge/i18n/hr.po
File diff suppressed because it is too large
Load Diff
3727
deluge/i18n/hu.po
3727
deluge/i18n/hu.po
File diff suppressed because it is too large
Load Diff
4212
deluge/i18n/id.po
4212
deluge/i18n/id.po
File diff suppressed because it is too large
Load Diff
4303
deluge/i18n/is.po
4303
deluge/i18n/is.po
File diff suppressed because it is too large
Load Diff
4599
deluge/i18n/it.po
4599
deluge/i18n/it.po
File diff suppressed because it is too large
Load Diff
6244
deluge/i18n/iu.po
6244
deluge/i18n/iu.po
File diff suppressed because it is too large
Load Diff
4326
deluge/i18n/ja.po
4326
deluge/i18n/ja.po
File diff suppressed because it is too large
Load Diff
5785
deluge/i18n/ka.po
5785
deluge/i18n/ka.po
File diff suppressed because it is too large
Load Diff
4058
deluge/i18n/kk.po
4058
deluge/i18n/kk.po
File diff suppressed because it is too large
Load Diff
6344
deluge/i18n/kn.po
6344
deluge/i18n/kn.po
File diff suppressed because it is too large
Load Diff
4348
deluge/i18n/ko.po
4348
deluge/i18n/ko.po
File diff suppressed because it is too large
Load Diff
6258
deluge/i18n/ku.po
6258
deluge/i18n/ku.po
File diff suppressed because it is too large
Load Diff
6251
deluge/i18n/la.po
6251
deluge/i18n/la.po
File diff suppressed because it is too large
Load Diff
3761
deluge/i18n/lb.po
Normal file
3761
deluge/i18n/lb.po
Normal file
File diff suppressed because it is too large
Load Diff
4486
deluge/i18n/lt.po
4486
deluge/i18n/lt.po
File diff suppressed because it is too large
Load Diff
4785
deluge/i18n/lv.po
4785
deluge/i18n/lv.po
File diff suppressed because it is too large
Load Diff
5827
deluge/i18n/mk.po
5827
deluge/i18n/mk.po
File diff suppressed because it is too large
Load Diff
3743
deluge/i18n/ml.po
Normal file
3743
deluge/i18n/ml.po
Normal file
File diff suppressed because it is too large
Load Diff
4414
deluge/i18n/ms.po
4414
deluge/i18n/ms.po
File diff suppressed because it is too large
Load Diff
4395
deluge/i18n/nb.po
4395
deluge/i18n/nb.po
File diff suppressed because it is too large
Load Diff
6253
deluge/i18n/nds.po
6253
deluge/i18n/nds.po
File diff suppressed because it is too large
Load Diff
4414
deluge/i18n/nl.po
4414
deluge/i18n/nl.po
File diff suppressed because it is too large
Load Diff
4227
deluge/i18n/nn.po
4227
deluge/i18n/nn.po
File diff suppressed because it is too large
Load Diff
4230
deluge/i18n/oc.po
4230
deluge/i18n/oc.po
File diff suppressed because it is too large
Load Diff
4691
deluge/i18n/pl.po
4691
deluge/i18n/pl.po
File diff suppressed because it is too large
Load Diff
6245
deluge/i18n/pms.po
6245
deluge/i18n/pms.po
File diff suppressed because it is too large
Load Diff
4180
deluge/i18n/pt.po
4180
deluge/i18n/pt.po
File diff suppressed because it is too large
Load Diff
3791
deluge/i18n/pt_BR.po
3791
deluge/i18n/pt_BR.po
File diff suppressed because it is too large
Load Diff
3870
deluge/i18n/ro.po
3870
deluge/i18n/ro.po
File diff suppressed because it is too large
Load Diff
3838
deluge/i18n/ru.po
3838
deluge/i18n/ru.po
File diff suppressed because it is too large
Load Diff
6244
deluge/i18n/si.po
6244
deluge/i18n/si.po
File diff suppressed because it is too large
Load Diff
5008
deluge/i18n/sk.po
5008
deluge/i18n/sk.po
File diff suppressed because it is too large
Load Diff
4483
deluge/i18n/sl.po
4483
deluge/i18n/sl.po
File diff suppressed because it is too large
Load Diff
4517
deluge/i18n/sr.po
4517
deluge/i18n/sr.po
File diff suppressed because it is too large
Load Diff
4428
deluge/i18n/sv.po
4428
deluge/i18n/sv.po
File diff suppressed because it is too large
Load Diff
6251
deluge/i18n/ta.po
6251
deluge/i18n/ta.po
File diff suppressed because it is too large
Load Diff
3743
deluge/i18n/te.po
Normal file
3743
deluge/i18n/te.po
Normal file
File diff suppressed because it is too large
Load Diff
4200
deluge/i18n/th.po
4200
deluge/i18n/th.po
File diff suppressed because it is too large
Load Diff
6245
deluge/i18n/tl.po
6245
deluge/i18n/tl.po
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user