Compare commits
770 Commits
archive/1.
...
deluge-1.3
Author | SHA1 | Date | |
---|---|---|---|
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 |
10
.gitattributes
vendored
Normal file
10
.gitattributes
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
/libtorrent export-ignore
|
||||
/win32 export-ignore
|
||||
docs/build export-ignore
|
||||
docs/source export-ignore
|
||||
/tests export-ignore
|
||||
deluge/scripts export-ignore
|
||||
.gitattributes export-ignore
|
||||
.gitmodules export-ignore
|
||||
.gitignore export-ignore
|
||||
*.py diff=python
|
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
*~
|
||||
build
|
||||
dist
|
||||
*egg-info
|
||||
*.egg
|
||||
*.log
|
||||
*.pyc
|
||||
*.tar.*
|
||||
_trial_temp
|
226
ChangeLog
226
ChangeLog
@ -1,98 +1,176 @@
|
||||
=== Deluge 1.2.0 (In Development) ===
|
||||
=== Deluge 1.3.0 (18 September 2010) ===
|
||||
* 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
|
||||
* Add max active downloading and seeding options to scheduler.
|
||||
* Ignore global stop ratio related settings in logic, so per torrent ones are used.
|
||||
* Fix scheduler so that it keeps current state, even after global settings change.
|
||||
* 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
|
||||
* AutoAdd plugin can now recover when one of the watchfolders has an unhandled exception.
|
||||
* Increase max piece size to 8 MiB in create torrent dialog (closes #1358)
|
||||
* 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 ====
|
||||
* Implement new RPC protocol DelugeRPC replacing XMLRPC
|
||||
* Move to a twisted framework
|
||||
* Add an 'Error' filter for Trackers to show trackers that currently have a tracker error
|
||||
* Use system GeoIP database if available, this is now an optional dependency
|
||||
|
||||
==== GtkUI ====
|
||||
* Remove SignalReceiver
|
||||
* Implemented a cross-platform IPC method thus removing the DBUS dependency
|
||||
* Implement a "True" Classic Mode where there is no longer a separate daemon process
|
||||
* Add preferences option "Add torrent in paused state"
|
||||
* Add tracker icons to the Tracker column
|
||||
* Implement #259 show tooltip with country name in the peers tab
|
||||
* Add an error category to the tracker sidebar list
|
||||
* Add Find More Plugins button to Plugins preference page
|
||||
* Fix #518 remove header in add torrent dialog to save vertical space
|
||||
* Add a Cache preferences page to adjust cache settings and examine cache status
|
||||
* Add ability to rename files prior to adding them
|
||||
* Fix shutdown handler with GNOME session manager
|
||||
* Allow 4 MiB piece sizes when creating a torrent
|
||||
* 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 ====
|
||||
* Changed to use curses for a more interactive client
|
||||
* #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 ====
|
||||
* Move over to using Twisted-Web for the webserver.
|
||||
* Move to only AJAX interface built upon Ext-JS.
|
||||
* #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 ====
|
||||
* Implement local blocklist support
|
||||
* #861: Pause transfers until blocklist is imported
|
||||
* Fix redirection not working with relative paths
|
||||
|
||||
==== Execute ====
|
||||
* Fix running commands with the TorrentAdded event
|
||||
* Fix the web interface
|
||||
|
||||
==== Label ====
|
||||
* Fix the web interface (#733)
|
||||
|
||||
==== Web ====
|
||||
* Migrate to ExtJS 3.1
|
||||
* Add gzip compression of HTTP data to the server
|
||||
* Improve the efficiency of the TorrentGrid with lots of torrents (#1026)
|
||||
* Add a base parameter to allow reverse proxying (#1076)
|
||||
* Fix showing all the peers in the details tab (#1054)
|
||||
* Fix uploading torrent files in Opera or IE (#1087)
|
||||
* Complete IE support
|
||||
|
||||
=== Deluge 1.2.0 - "Bursting like an infected kidney" (10 January 2010) ===
|
||||
==== Core ====
|
||||
* Implement new RPC protocol DelugeRPC replacing XMLRPC
|
||||
* Move to a twisted framework
|
||||
* Add an 'Error' filter for Trackers to show trackers that currently have a tracker error
|
||||
* Use system GeoIP database if available, this is now an optional dependency
|
||||
|
||||
==== GtkUI ====
|
||||
* Remove SignalReceiver
|
||||
* Implemented a cross-platform IPC method thus removing the DBUS dependency
|
||||
* Implement a "True" Classic Mode where there is no longer a separate daemon process
|
||||
* Add preferences option "Add torrent in paused state"
|
||||
* Add tracker icons to the Tracker column
|
||||
* Implement #259 show tooltip with country name in the peers tab
|
||||
* Add an error category to the tracker sidebar list
|
||||
* Add Find More Plugins button to Plugins preference page
|
||||
* Fix #518 remove header in add torrent dialog to save vertical space
|
||||
* Add a Cache preferences page to adjust cache settings and examine cache status
|
||||
* Add ability to rename files prior to adding them
|
||||
* Fix shutdown handler with GNOME session manager
|
||||
* Allow 4 MiB piece sizes when creating a torrent
|
||||
|
||||
==== ConsoleUI ====
|
||||
* Changed to use curses for a more interactive client
|
||||
|
||||
==== WebUI ====
|
||||
* Move over to using Twisted-Web for the webserver.
|
||||
* Move to only AJAX interface built upon Ext-JS.
|
||||
|
||||
==== Plugins ====
|
||||
* Add Scheduler plugin
|
||||
* Add Extractor plugin
|
||||
* Add Scheduler plugin
|
||||
* Add Extractor plugin
|
||||
|
||||
==== Misc ====
|
||||
* PyGTK dependency bumped to => 2.12 to use new tooltip system
|
||||
* Add new scripts for invoking UIs: deluge-gtk, deluge-web, deluge-console
|
||||
* Remove GeoIP database from the source tree
|
||||
* PyGTK dependency bumped to => 2.12 to use new tooltip system
|
||||
* Add new scripts for invoking UIs: deluge-gtk, deluge-web, deluge-console
|
||||
* Remove GeoIP database from the source tree
|
||||
|
||||
=== Deluge 1.1.0 - "Time gas!" (10 January 2009) ===
|
||||
==== Core ====
|
||||
* Implement #79 ability to change outgoing port range
|
||||
* Implement #296 ability to change peer TOS byte
|
||||
* Add per-torrent move on completed settings
|
||||
* Implement #414 use async save_resume_data method
|
||||
* Filter Manager with torrent filtering in get_torrents_status , for sidebar and plugins.
|
||||
* Implement #368 add torrents by infohash/magnet uri (trackerless torrents)
|
||||
* Remove remaining gtk functions in common
|
||||
* Tracker icons.
|
||||
* Add ETA for torrents with stop at seed ratio set
|
||||
* Fix #47 the state and config files are no longer invalidated when there is no diskspace
|
||||
* Fix #619 return "" instead of "Infinity" if seconds == 0 in ftime
|
||||
* Add -P, --pidfile option to deluged
|
||||
* Implement #79 ability to change outgoing port range
|
||||
* Implement #296 ability to change peer TOS byte
|
||||
* Add per-torrent move on completed settings
|
||||
* Implement #414 use async save_resume_data method
|
||||
* Filter Manager with torrent filtering in get_torrents_status , for sidebar and plugins.
|
||||
* Implement #368 add torrents by infohash/magnet uri (trackerless torrents)
|
||||
* Remove remaining gtk functions in common
|
||||
* Tracker icons.
|
||||
* Add ETA for torrents with stop at seed ratio set
|
||||
* Fix #47 the state and config files are no longer invalidated when there is no diskspace
|
||||
* Fix #619 return "" instead of "Infinity" if seconds == 0 in ftime
|
||||
* Add -P, --pidfile option to deluged
|
||||
|
||||
==== GtkUI ====
|
||||
* Add peer progress to the peers tab
|
||||
* Add ability to manually add peers
|
||||
* Sorting # column will place downloaders above seeds
|
||||
* Remove dependency on libtorrent for add torrent dialog
|
||||
* Allow adding multiple trackers at once in the edit tracker dialog
|
||||
* Implement #28 Create Torrent Dialog
|
||||
* Redesiged sidebar with filters for Active and Tracker (see Filter Manager)
|
||||
* Implement #428 the ability to rename files and directories
|
||||
* Implement #229 add date added column
|
||||
* Implement #596 show speeds in title
|
||||
* Fix #636 not setting the daemon's config directory when using --config= with the UI in classic mode.
|
||||
* Fix #624 do not allow changing file priorities when using compact allocation
|
||||
* Fix #602 re-did files/peers tab state saving/loading
|
||||
* Fix gtk warnings
|
||||
* Add protocol traffic statusbar item
|
||||
* Rework the Remove Torrent Dialog to only have 2 options, remove data and remove from session.
|
||||
* Add "Install Plugin" and "Rescan Plugins" buttons to the Plugins preferences
|
||||
* Make active port test use internal graphic instead of launching browser
|
||||
* Add peer progress to the peers tab
|
||||
* Add ability to manually add peers
|
||||
* Sorting # column will place downloaders above seeds
|
||||
* Remove dependency on libtorrent for add torrent dialog
|
||||
* Allow adding multiple trackers at once in the edit tracker dialog
|
||||
* Implement #28 Create Torrent Dialog
|
||||
* Redesiged sidebar with filters for Active and Tracker (see Filter Manager)
|
||||
* Implement #428 the ability to rename files and directories
|
||||
* Implement #229 add date added column
|
||||
* Implement #596 show speeds in title
|
||||
* Fix #636 not setting the daemon's config directory when using --config= with the UI in classic mode.
|
||||
* Fix #624 do not allow changing file priorities when using compact allocation
|
||||
* Fix #602 re-did files/peers tab state saving/loading
|
||||
* Fix gtk warnings
|
||||
* Add protocol traffic statusbar item
|
||||
* Rework the Remove Torrent Dialog to only have 2 options, remove data and remove from session.
|
||||
* Add "Install Plugin" and "Rescan Plugins" buttons to the Plugins preferences
|
||||
* Make active port test use internal graphic instead of launching browser
|
||||
|
||||
==== WebUI ====
|
||||
* Lots of smaller tweaks.
|
||||
* All details tabs have the same features as in gtk-ui 1.0.x
|
||||
* Persistent sessions #486
|
||||
* Plugin improvements for easy use of templates and images in eggs. #497
|
||||
* Classic template takes over some style elements from white template.
|
||||
* https (for users that know how to create certificates)
|
||||
* Easier apache mod_proxy use.
|
||||
* Redesigned sidebar
|
||||
* Lots of smaller tweaks.
|
||||
* All details tabs have the same features as in gtk-ui 1.0.x
|
||||
* Persistent sessions #486
|
||||
* Plugin improvements for easy use of templates and images in eggs. #497
|
||||
* Classic template takes over some style elements from white template.
|
||||
* https (for users that know how to create certificates)
|
||||
* Easier apache mod_proxy use.
|
||||
* Redesigned sidebar
|
||||
|
||||
==== AjaxUI ====
|
||||
* Hosted in a webui template.
|
||||
* Hosted in a webui template.
|
||||
|
||||
==== ConsoleUI ====
|
||||
* New ConsoleUI written by Idoa01
|
||||
* Callable from command-line for scripts.
|
||||
* New ConsoleUI written by Idoa01
|
||||
* Callable from command-line for scripts.
|
||||
|
||||
==== Plugins ====
|
||||
* Stats plugin for graphs.
|
||||
* Label plugin for grouping torrents and per torrent settings.
|
||||
* Stats plugin for graphs.
|
||||
* Label plugin for grouping torrents and per torrent settings.
|
||||
|
||||
==== Misc ====
|
||||
* Implement #478 display UI options in usage help
|
||||
* Fix #547 add description to name field per HIG entry 2.1.1.1
|
||||
* Fix #531 set default log level to ERROR and add 2 command-line options, "-L, --loglevel" and "-q, --quiet".
|
||||
* Implement #478 display UI options in usage help
|
||||
* Fix #547 add description to name field per HIG entry 2.1.1.1
|
||||
* Fix #531 set default log level to ERROR and add 2 command-line options, "-L, --loglevel" and "-q, --quiet".
|
||||
|
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
|
23
create_potfiles_in.py
Normal file
23
create_potfiles_in.py
Normal file
@ -0,0 +1,23 @@
|
||||
import os
|
||||
|
||||
# Paths to exclude
|
||||
EXCLUSIONS = [
|
||||
"deluge/scripts"
|
||||
]
|
||||
|
||||
POTFILE_IN = "deluge/i18n/POTFILES.in"
|
||||
|
||||
print "Creating " + POTFILE_IN + " .."
|
||||
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:
|
||||
to_translate.append(os.path.join(dirpath, filename))
|
||||
|
||||
f = open(POTFILE_IN, "wb")
|
||||
for line in to_translate:
|
||||
f.write(line + "\n")
|
||||
|
||||
f.close()
|
||||
|
||||
print "Done"
|
5
debian/changelog
vendored
5
debian/changelog
vendored
@ -1,5 +0,0 @@
|
||||
deluge-torrent (0.9.08-1) unstable; urgency=low
|
||||
|
||||
* R8 release
|
||||
|
||||
-- Andrew Resch (andar) <andrewresch@gmail.com> Tue, 26 Aug 2008 16:31:14 -0800
|
5
debian/changelog.debian-lenny
vendored
5
debian/changelog.debian-lenny
vendored
@ -1,5 +0,0 @@
|
||||
deluge-torrent (0.6.0-svn3235-1) lenny; urgency=low
|
||||
|
||||
* Daily Build
|
||||
|
||||
-- Andrew Resch (andar) <andrewresch@gmail.com> Tue, 17 Jun 2008 16:31:14 -0800
|
5
debian/changelog.debian-sid
vendored
5
debian/changelog.debian-sid
vendored
@ -1,5 +0,0 @@
|
||||
deluge-torrent (0.6.0-svn3235-1) unstable; urgency=low
|
||||
|
||||
* Daily Build
|
||||
|
||||
-- Andrew Resch (andar) <andrewresch@gmail.com> Tue, 17 Jun 2008 16:31:14 -0800
|
5
debian/changelog.ubuntu-gutsy
vendored
5
debian/changelog.ubuntu-gutsy
vendored
@ -1,5 +0,0 @@
|
||||
deluge-torrent (0.6.0-svn3235-1) gutsy; urgency=low
|
||||
|
||||
* Daily Build
|
||||
|
||||
-- Andrew Resch (andar) <andrewresch@gmail.com> Tue, 17 Jun 2008 16:31:14 -0800
|
5
debian/changelog.ubuntu-hardy
vendored
5
debian/changelog.ubuntu-hardy
vendored
@ -1,5 +0,0 @@
|
||||
deluge-torrent (0.6.0-svn3235-1) hardy; urgency=low
|
||||
|
||||
* Daily Build
|
||||
|
||||
-- Andrew Resch (andar) <andrewresch@gmail.com> Tue, 17 Jun 2008 16:31:14 -0800
|
1
debian/compat
vendored
1
debian/compat
vendored
@ -1 +0,0 @@
|
||||
5
|
21
debian/control
vendored
21
debian/control
vendored
@ -1,21 +0,0 @@
|
||||
Source: deluge-torrent
|
||||
Section: net
|
||||
Priority: optional
|
||||
Maintainer: Andrew Resch (andar) <andrewresch@gmail.com>
|
||||
Build-Depends: debhelper (>= 5.0.37.2), python-all-dev (>= 2.3.5-11), python-all, python-support (>= 0.5.3), libboost-dev (>= 1.34.1), libboost-thread-dev (>= 1.34.1), libboost-date-time-dev (>= 1.34.1), libboost-filesystem-dev (>= 1.34.1), libboost-python-dev (>= 1.34.1), libboost-iostreams-dev (>= 1.34.1), zlib1g-dev, libssl-dev, dpatch, python-setuptools
|
||||
Standards-Version: 3.7.2
|
||||
|
||||
Package: deluge-torrent
|
||||
Architecture: any
|
||||
Depends: ${shlibs:Depends}, ${python:Depends}, python-gtk2, python-glade2, python-xdg, python-notify, notification-daemon | notification-daemon-xfce, python-dbus, librsvg2-common, python-pyopenssl, python-setuptools, python-pkg-resources
|
||||
Conflicts: deluge-torrent-common
|
||||
Replaces: deluge-torrent-common
|
||||
Description: A Bittorrent client written in Python/PyGTK
|
||||
Deluge is a Bittorrent client, created using Python and GTK+.
|
||||
.
|
||||
Deluge is intended to bring a native, full-featured client to Linux GTK
|
||||
desktop environments such as Gnome and XFCE.
|
||||
.
|
||||
It uses Rasterbar's version of libtorrent.
|
||||
.
|
||||
Homepage: http://www.deluge-torrent.org/
|
19
debian/control.debian-lenny
vendored
19
debian/control.debian-lenny
vendored
@ -1,19 +0,0 @@
|
||||
Source: deluge-torrent
|
||||
Section: net
|
||||
Priority: optional
|
||||
Maintainer: Andrew Resch (andar) <andrewresch@gmail.com>
|
||||
Build-Depends: debhelper (>= 5.0.37.2), python-all-dev (>= 2.3.5-11), python-all, python-support (>= 0.5.3), libboost-dev (>= 1.33.1), libboost-thread-dev (>= 1.33.1), libboost-date-time-dev (>= 1.33.1), libboost-filesystem-dev (>= 1.33.1), libboost-serialization-dev (>= 1.33.1), libboost-program-options-dev (>= 1.33.1), libboost-regex-dev (>= 1.33.1), libboost-python-dev (>= 1.33.1), zlib1g-dev, libssl-dev, dpatch, python-setuptools
|
||||
Standards-Version: 3.7.2
|
||||
|
||||
Package: deluge-torrent
|
||||
Architecture: any
|
||||
Depends: ${shlibs:Depends}, ${python:Depends}, python-gtk2, python-glade2, python-xdg, python-notify, notification-daemon | notification-daemon-xfce, python-dbus, librsvg2-common, python-pyopenssl, python-setuptools
|
||||
Description: A Bittorrent client written in Python/PyGTK
|
||||
Deluge is a Bittorrent client, created using Python and GTK+.
|
||||
.
|
||||
Deluge is intended to bring a native, full-featured client to Linux GTK
|
||||
desktop environments such as Gnome and XFCE.
|
||||
.
|
||||
It uses Rasterbar's version of libtorrent.
|
||||
.
|
||||
Homepage: http://www.deluge-torrent.org/
|
19
debian/control.debian-sid
vendored
19
debian/control.debian-sid
vendored
@ -1,19 +0,0 @@
|
||||
Source: deluge-torrent
|
||||
Section: net
|
||||
Priority: optional
|
||||
Maintainer: Andrew Resch (andar) <andrewresch@gmail.com>
|
||||
Build-Depends: debhelper (>= 5.0.37.2), python-all-dev (>= 2.3.5-11), python-all, python-support (>= 0.5.3), libboost1.36-dev (>= 1.36), libboost-thread1.36-dev (>= 1.36), libboost-date-time1.36-dev (>= 1.36), libboost-filesystem1.36-dev (>= 1.36), libboost-serialization1.36-dev (>= 1.36), libboost-program-options1.36-dev (>= 1.36), libboost-regex1.36-dev (>= 1.36), libboost-python1.36-dev (>= 1.36), zlib1g-dev, libssl-dev, dpatch, python-setuptools
|
||||
Standards-Version: 3.7.2
|
||||
|
||||
Package: deluge-torrent
|
||||
Architecture: any
|
||||
Depends: ${shlibs:Depends}, ${python:Depends}, python-gtk2, python-glade2, python-xdg, python-notify, notification-daemon | notification-daemon-xfce, python-dbus, librsvg2-common, python-pyopenssl, python-setuptools
|
||||
Description: A Bittorrent client written in Python/PyGTK
|
||||
Deluge is a Bittorrent client, created using Python and GTK+.
|
||||
.
|
||||
Deluge is intended to bring a native, full-featured client to Linux GTK
|
||||
desktop environments such as Gnome and XFCE.
|
||||
.
|
||||
It uses Rasterbar's version of libtorrent.
|
||||
.
|
||||
Homepage: http://www.deluge-torrent.org/
|
99
debian/copyright
vendored
99
debian/copyright
vendored
@ -1,99 +0,0 @@
|
||||
This package was debianized by Marcos Pinto (markybob) <markybob@gmail.com> on
|
||||
Fri, 31 Jul 2008 22:03:13 +0100.
|
||||
|
||||
It was downloaded from http://www.deluge-torrent.org/
|
||||
|
||||
Upstream Authors & Copyright:
|
||||
Andrew Resch
|
||||
Marcos Pinto
|
||||
Sadrul Habib Chowdhury
|
||||
Martijn Voncken
|
||||
|
||||
License:
|
||||
|
||||
This package is free software; you can 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.
|
||||
|
||||
This package 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 this package; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, 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.
|
||||
|
||||
On Debian systems, the complete text of the GNU General
|
||||
Public License can be found in `/usr/share/common-licenses/GPL'.
|
||||
|
||||
libtorrent is (C) 2003-2008 Arvid Norberg arvid@cs.umu.se and its
|
||||
python bindings were initially written by Daniel Wallin in 2006.
|
||||
It is distributed unders terms of the BSD License below:
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in
|
||||
the documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the author nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
"libtorrent/include/libtorrent/asio*" are (C) 2003-2008 Christopher
|
||||
M. Kohlhoff <chris@kohlhoff.com> and distributed under terms of the Boost
|
||||
Software License, Version 1.0 :
|
||||
|
||||
Permission is hereby granted, free of charge, to any person or organization
|
||||
obtaining a copy of the software and accompanying documentation covered by
|
||||
this license (the "Software") to use, reproduce, display, distribute,
|
||||
execute, and transmit the Software, and to prepare derivative works of the
|
||||
Software, and to permit third-parties to whom the Software is furnished to
|
||||
do so, all subject to the following:
|
||||
The copyright notices in the Software and this entire statement, including
|
||||
the above license grant, this restriction and the following disclaimer,
|
||||
must be included in all copies of the Software, in whole or in part, and
|
||||
all derivative works of the Software, unless such copies or derivative
|
||||
works are solely in the form of machine-executable object code generated by
|
||||
a source language processor.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
"deluge/i18n/*" are (C) 2006 Rosetta Contributors and Canonical Ltd 2006 and distributed
|
||||
under the same license as the deluge software.
|
||||
|
||||
The Debian packaging is (C) 2006-2008, Marcos Pinto (markybob)
|
||||
<markybob@gmail.com> and is licensed under GPL3, see above.
|
2
debian/manpages
vendored
2
debian/manpages
vendored
@ -1,2 +0,0 @@
|
||||
docs/man/deluge.1
|
||||
docs/man/deluged.1
|
3
debian/menu
vendored
3
debian/menu
vendored
@ -1,3 +0,0 @@
|
||||
?package(deluge-torrent): needs="X11" section="Applications/Network/File Transfer" \
|
||||
title="Deluge BitTorrent Client" longtitle="Bittorrent client written in Python/PyGTK" \
|
||||
command="/usr/bin/deluge" icon="/usr/share/pixmaps/deluge.png"
|
0
debian/patches/00list
vendored
0
debian/patches/00list
vendored
1
debian/pyversions
vendored
1
debian/pyversions
vendored
@ -1 +0,0 @@
|
||||
2.5
|
1
debian/pyversions.ubuntu-gutsy
vendored
1
debian/pyversions.ubuntu-gutsy
vendored
@ -1 +0,0 @@
|
||||
2.5
|
1
debian/pyversions.ubuntu-hardy
vendored
1
debian/pyversions.ubuntu-hardy
vendored
@ -1 +0,0 @@
|
||||
2.5
|
75
debian/rules
vendored
75
debian/rules
vendored
@ -1,75 +0,0 @@
|
||||
#!/usr/bin/make -f
|
||||
|
||||
# Uncomment this to turn on verbose mode.
|
||||
#export DH_VERBOSE=1
|
||||
|
||||
# Dpatch targets
|
||||
include /usr/share/dpatch/dpatch.make
|
||||
|
||||
# Available python (using debian/pyversions) and destdir
|
||||
PYVERS = 2.5
|
||||
DESTDIR = $(CURDIR)/debian/deluge-torrent
|
||||
|
||||
# We need to known the target arch to enable/disable amd64 hack
|
||||
ARCH = $(shell dpkg-architecture -qDEB_BUILD_ARCH_CPU)
|
||||
ARCH64 = ia64 amd64 alpha kfreebsd-amd64 ppc64
|
||||
|
||||
CFLAGS = -Wall -g
|
||||
ifneq (,$(findstring noopt,$(DEB_BUILD_OPTIONS)))
|
||||
CFLAGS += -O0
|
||||
else
|
||||
CFLAGS += -O2
|
||||
endif
|
||||
# python-libtorrent need to define AMD64 to work fine on a 64 bits system
|
||||
ifneq (,$(findstring $(ARCH),$(ARCH64)))
|
||||
CFLAGS += -DAMD64
|
||||
endif
|
||||
|
||||
build: patch-stamp $(PYVERS:%=build-stamp%)
|
||||
build-stamp%: patch-stamp
|
||||
dh_testdir
|
||||
CFLAGS="$(CFLAGS)" python$* setup.py build
|
||||
touch $@
|
||||
|
||||
clean: unpatch
|
||||
dh_testdir
|
||||
dh_testroot
|
||||
rm -rf build/
|
||||
find . -name \*.pyc | xargs rm -f
|
||||
rm -rf build-stamp*
|
||||
dh_clean
|
||||
|
||||
install: build install-prereq $(PYVERS:%=install-%) install-finish
|
||||
install-prereq:
|
||||
dh_testdir
|
||||
dh_testroot
|
||||
dh_clean -k
|
||||
dh_installdirs
|
||||
install-%:
|
||||
python$* setup.py install --root=$(DESTDIR) --prefix=/usr --no-compile
|
||||
install-finish:
|
||||
# Desktop menu
|
||||
rm -rf $(DESTDIR)/usr/share/applications
|
||||
install -D -m644 $(CURDIR)/deluge/data/share/applications/deluge.desktop $(DESTDIR)/usr/share/applications/deluge.desktop
|
||||
|
||||
|
||||
binary-indep: build install
|
||||
binary-arch: build install
|
||||
dh_testdir
|
||||
dh_testroot
|
||||
dh_installchangelogs
|
||||
dh_installdocs
|
||||
dh_installmenu
|
||||
dh_strip
|
||||
dh_compress
|
||||
dh_fixperms
|
||||
dh_pysupport
|
||||
dh_desktop
|
||||
dh_installdeb
|
||||
dh_shlibdeps
|
||||
dh_gencontrol
|
||||
dh_md5sums
|
||||
dh_builddeb
|
||||
|
||||
binary: binary-indep binary-arch
|
||||
.PHONY: build clean binary-indep binary-arch binary install
|
@ -45,7 +45,7 @@ supports.
|
||||
|
||||
"""
|
||||
|
||||
REQUIRED_VERSION = "0.14.5.0"
|
||||
REQUIRED_VERSION = "0.14.9.0"
|
||||
|
||||
def check_version(LT):
|
||||
from deluge.common import VersionSplit
|
||||
@ -57,4 +57,4 @@ try:
|
||||
check_version(lt)
|
||||
except ImportError:
|
||||
import libtorrent as lt
|
||||
check_version(lt)
|
||||
check_version(lt)
|
||||
|
164
deluge/common.py
164
deluge/common.py
@ -63,7 +63,24 @@ 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.bindtextdomain("deluge", pkg_resources.resource_filename("deluge", "i18n"))
|
||||
gettext.textdomain("deluge")
|
||||
gettext.install("deluge", pkg_resources.resource_filename("deluge", "i18n"))
|
||||
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 *
|
||||
|
||||
@ -132,6 +149,7 @@ def get_default_config_dir(filename=None):
|
||||
else:
|
||||
return os.path.join(os.environ.get("APPDATA"), "deluge")
|
||||
else:
|
||||
import xdg.BaseDirectory
|
||||
if filename:
|
||||
return os.path.join(xdg.BaseDirectory.save_config_path("deluge"), filename)
|
||||
else:
|
||||
@ -210,7 +228,7 @@ def open_url_in_browser(url):
|
||||
|
||||
:param url: the url to open
|
||||
:type url: string
|
||||
|
||||
|
||||
"""
|
||||
import webbrowser
|
||||
webbrowser.open(url)
|
||||
@ -234,12 +252,12 @@ 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 fpcnt(dec):
|
||||
"""
|
||||
@ -340,21 +358,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 +385,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 +406,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
|
||||
@ -469,13 +462,13 @@ def free_space(path):
|
||||
:type path: string
|
||||
:returns: the free space at path in bytes
|
||||
:rtype: int
|
||||
|
||||
|
||||
:raises InvalidPathError: if the path is not valid
|
||||
|
||||
"""
|
||||
if not os.path.exists(path):
|
||||
raise InvalidPathError("%s is not a valid path" % path)
|
||||
|
||||
|
||||
if windows_check():
|
||||
import win32file
|
||||
sectors, bytes, free, total = map(long, win32file.GetDiskFreeSpace(path))
|
||||
@ -514,47 +507,90 @@ def is_ip(ip):
|
||||
return True
|
||||
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
|
||||
|
||||
class VersionSplit(object):
|
||||
"""
|
||||
Used for comparing version numbers.
|
||||
|
||||
|
||||
:param ver: the version
|
||||
:type ver: string
|
||||
|
||||
|
||||
"""
|
||||
def __init__(self, ver):
|
||||
ver = ver.lower()
|
||||
vs = ver.split("_") if "_" in ver else ver.split("-")
|
||||
self.version = vs[0]
|
||||
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):
|
||||
"""
|
||||
The comparison method.
|
||||
|
||||
|
||||
:param ver: the version to compare with
|
||||
:type ver: VersionSplit
|
||||
|
||||
"""
|
||||
|
||||
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,196 +33,372 @@
|
||||
#
|
||||
#
|
||||
|
||||
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 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:
|
||||
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)
|
||||
self._component_stopping_deferred = d
|
||||
else:
|
||||
d = maybeDeferred(on_stop, None)
|
||||
|
||||
if self._component_state == "Stopping":
|
||||
return self._component_stopping_deferred
|
||||
|
||||
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
|
||||
|
||||
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 = {}
|
||||
self.depend = {}
|
||||
|
||||
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
|
||||
def register(self, obj):
|
||||
"""
|
||||
Registers a component object with the registry. This is done
|
||||
automatically when a Component object is instantiated.
|
||||
|
||||
def get(self, name):
|
||||
"""Returns a reference to the component 'name'"""
|
||||
return self.components[name]
|
||||
:param obj: the Component object
|
||||
:type obj: object
|
||||
|
||||
def start(self):
|
||||
"""Starts all components"""
|
||||
for component in self.components.keys():
|
||||
self.start_component(component)
|
||||
:raises ComponentAlreadyRegistered: if a component with the same name is already registered.
|
||||
|
||||
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)
|
||||
"""
|
||||
name = obj._component_name
|
||||
if name in self.components:
|
||||
raise ComponentAlreadyRegistered(
|
||||
"Component already registered with name %s" % name)
|
||||
|
||||
# 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()
|
||||
self.components[obj._component_name] = obj
|
||||
|
||||
def stop(self):
|
||||
"""Stops all components"""
|
||||
for component in self.components.keys():
|
||||
self.stop_component(component)
|
||||
def deregister(self, name):
|
||||
"""
|
||||
Deregisters a component from the registry. A stop will be
|
||||
issued to the component prior to deregistering it.
|
||||
|
||||
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()
|
||||
:param name: the name of the component
|
||||
:type name: string
|
||||
|
||||
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()
|
||||
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 resume(self):
|
||||
"""Resumes all components. Starts calling update()"""
|
||||
for component in self.components.keys():
|
||||
self.resume_component(component)
|
||||
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.
|
||||
|
||||
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()
|
||||
:param names: a list of Components to start
|
||||
:type names: list
|
||||
|
||||
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()
|
||||
:returns: a Deferred object that will fire once all Components have been sucessfully started
|
||||
:rtype: twisted.internet.defer.Deferred
|
||||
|
||||
return True
|
||||
"""
|
||||
# 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):
|
||||
"""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)
|
||||
"""
|
||||
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 start(component=None):
|
||||
"""Starts all components"""
|
||||
if component == None:
|
||||
_ComponentRegistry.start()
|
||||
else:
|
||||
_ComponentRegistry.start_component(component)
|
||||
def get(name):
|
||||
"""
|
||||
Return a reference to a component.
|
||||
|
||||
def stop(component=None):
|
||||
"""Stops all or specified components"""
|
||||
if component == None:
|
||||
_ComponentRegistry.stop()
|
||||
else:
|
||||
_ComponentRegistry.stop_component(component)
|
||||
:param name: the Component name to get
|
||||
:type name: string
|
||||
|
||||
def pause(component=None):
|
||||
"""Pauses all or specificed components"""
|
||||
if component == None:
|
||||
_ComponentRegistry.pause()
|
||||
else:
|
||||
_ComponentRegistry.pause_component(component)
|
||||
:returns: the Component object
|
||||
:rtype: object
|
||||
|
||||
def resume(component=None):
|
||||
"""Resumes all or specificed components"""
|
||||
if component == None:
|
||||
_ComponentRegistry.resume()
|
||||
else:
|
||||
_ComponentRegistry.resume_component(component)
|
||||
:raises KeyError: if the Component does not exist
|
||||
|
||||
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
|
||||
@ -156,6 +156,9 @@ class Config(object):
|
||||
|
||||
self.load()
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self.__config
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""
|
||||
See
|
||||
@ -345,21 +348,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]
|
||||
@ -368,8 +371,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)
|
||||
|
||||
@ -393,26 +396,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()
|
||||
if self._save_timer and self._save_timer.active():
|
||||
self._save_timer.cancel()
|
||||
return
|
||||
except Exception, e:
|
||||
log.warning("Unable to open config file: %s", filename)
|
||||
|
||||
|
||||
except IOError, 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
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -124,6 +124,7 @@ class AuthManager(component.Component):
|
||||
if line.startswith("#"):
|
||||
# This is a comment line
|
||||
continue
|
||||
line = line.strip()
|
||||
try:
|
||||
lsplit = line.split(":")
|
||||
except Exception, e:
|
||||
|
@ -75,7 +75,11 @@ class AutoAdd(component.Component):
|
||||
|
||||
for filename in os.listdir(self.config["autoadd_location"]):
|
||||
if filename.split(".")[-1] == "torrent":
|
||||
filepath = os.path.join(self.config["autoadd_location"], filename)
|
||||
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:
|
||||
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,6 +42,7 @@ import shutil
|
||||
import threading
|
||||
import pkg_resources
|
||||
import warnings
|
||||
import tempfile
|
||||
|
||||
|
||||
from twisted.internet import reactor, defer
|
||||
@ -238,7 +239,13 @@ class Core(component.Component):
|
||||
log.info("Attempting to add url %s", url)
|
||||
def on_get_file(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):
|
||||
@ -247,7 +254,7 @@ class Core(component.Component):
|
||||
log.error("Reason: %s", failure.getErrorMessage())
|
||||
return failure
|
||||
|
||||
d = download_file(url, url.split("/")[-1], headers=headers)
|
||||
d = download_file(url, tempfile.mkstemp()[1], headers=headers)
|
||||
d.addCallback(on_get_file)
|
||||
d.addErrback(on_get_file_error)
|
||||
return d
|
||||
@ -288,28 +295,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 +392,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 +403,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 +412,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 +475,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 +572,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 +583,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,
|
||||
@ -655,14 +616,21 @@ class Core(component.Component):
|
||||
self.add_torrent_file(os.path.split(target)[1], open(target, "rb").read(), options)
|
||||
|
||||
@export
|
||||
def upload_plugin(self, filename, plugin_data):
|
||||
def upload_plugin(self, filename, filedump):
|
||||
"""This method is used to upload new plugins to the daemon. It is used
|
||||
when connecting to the daemon remotely and installing a new plugin on
|
||||
the client side. 'plugin_data' is a xmlrpc.Binary object of the file data,
|
||||
ie, plugin_file.read()"""
|
||||
|
||||
try:
|
||||
filedump = base64.decodestring(filedump)
|
||||
except Exception, e:
|
||||
log.error("There was an error decoding the filedump string!")
|
||||
log.exception(e)
|
||||
return
|
||||
|
||||
f = open(os.path.join(deluge.configmanager.get_config_dir(), "plugins", filename), "wb")
|
||||
f.write(plugin_data.data)
|
||||
f.write(filedump)
|
||||
f.close()
|
||||
component.get("CorePluginManager").scan_for_plugins()
|
||||
|
||||
@ -780,7 +748,8 @@ 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))
|
||||
@ -790,11 +759,12 @@ class Core(component.Component):
|
||||
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
|
||||
@ -803,7 +773,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):
|
||||
|
@ -105,6 +105,8 @@ class Daemon(object):
|
||||
gettext.install("deluge", pkg_resources.resource_filename("deluge", "i18n"))
|
||||
except Exception, e:
|
||||
log.error("Unable to initialize gettext/locale: %s", e)
|
||||
import __builtin__
|
||||
__builtin__.__dict__["_"] = lambda x: x
|
||||
|
||||
# Twisted catches signals to terminate, so just have it call the shutdown
|
||||
# method.
|
||||
|
@ -84,7 +84,7 @@ def tracker_error_filter(torrent_ids, values):
|
||||
# If this is a tracker_host, then we need to filter on it
|
||||
if values[0] != "Error":
|
||||
for torrent_id in torrent_ids:
|
||||
if values[0] in tm[torrent_id].get_status(["tracker_host"])["tracker_host"]:
|
||||
if values[0] == tm[torrent_id].get_status(["tracker_host"])["tracker_host"]:
|
||||
filtered_torrent_ids.append(torrent_id)
|
||||
return filtered_torrent_ids
|
||||
|
||||
@ -196,6 +196,8 @@ class FilterManager(component.Component):
|
||||
value = status[field]
|
||||
items[field][value] = items[field].get(value, 0) + 1
|
||||
|
||||
items["tracker_host"]["All"] = len(torrent_ids)
|
||||
|
||||
if "tracker_host" in items:
|
||||
items["tracker_host"]["Error"] = len(tracker_error_filter(torrent_ids, ("Error",)))
|
||||
|
||||
|
@ -97,7 +97,7 @@ class OldStateUpgrader:
|
||||
torrent_info = lt.torrent_info(lt.bdecode(_file.read()))
|
||||
_file.close()
|
||||
except (IOError, RuntimeError), e:
|
||||
log.warning("Unable to open %s: %s", filepath, e)
|
||||
log.warning("Unable to open %s: %s", torrent_path, e)
|
||||
|
||||
# Copy the torrent file to the new location
|
||||
import shutil
|
||||
|
@ -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",
|
||||
@ -232,6 +232,11 @@ class PreferencesManager(component.Component):
|
||||
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..")
|
||||
@ -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.
|
||||
@ -131,7 +161,7 @@ class DelugeRPCProtocol(Protocol):
|
||||
try:
|
||||
request = rencode.loads(dobj.decompress(data))
|
||||
except Exception, e:
|
||||
log.debug("Received possible invalid message (%r): %s", data, e)
|
||||
#log.debug("Received possible invalid message (%r): %s", data, e)
|
||||
# This could be cut-off data, so we'll save this in the buffer
|
||||
# and try to prepend it on the next dataReceived()
|
||||
self.__buffer = data
|
||||
@ -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
|
||||
@ -72,7 +73,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 +81,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
|
||||
@ -113,9 +124,6 @@ class Torrent:
|
||||
except RuntimeError:
|
||||
self.torrent_info = None
|
||||
|
||||
# Files dictionary
|
||||
self.files = self.get_files()
|
||||
|
||||
# Default total_uploaded to 0, this may be changed by the state
|
||||
self.total_uploaded = 0
|
||||
|
||||
@ -254,7 +262,7 @@ class Torrent:
|
||||
self.options["move_completed_path"] = move_completed_path
|
||||
|
||||
def set_file_priorities(self, file_priorities):
|
||||
if len(file_priorities) != len(self.files):
|
||||
if len(file_priorities) != len(self.get_files()):
|
||||
log.debug("file_priorities len != num_files")
|
||||
self.options["file_priorities"] = self.handle.file_priorities()
|
||||
return
|
||||
@ -288,12 +296,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
|
||||
@ -302,14 +307,11 @@ class Torrent:
|
||||
log.debug("Setting trackers for %s: %s", self.torrent_id, trackers)
|
||||
tracker_list = []
|
||||
|
||||
if lt.version_minor < 15:
|
||||
for tracker in trackers:
|
||||
new_entry = lt.announce_entry(tracker["url"])
|
||||
new_entry.tier = tracker["tier"]
|
||||
tracker_list.append(new_entry)
|
||||
self.handle.replace_trackers(tracker_list)
|
||||
else:
|
||||
self.handle.replace_trackers(trackers)
|
||||
for tracker in trackers:
|
||||
new_entry = lt.announce_entry(tracker["url"])
|
||||
new_entry.tier = tracker["tier"]
|
||||
tracker_list.append(new_entry)
|
||||
self.handle.replace_trackers(tracker_list)
|
||||
|
||||
# Print out the trackers
|
||||
#for t in self.handle.trackers():
|
||||
@ -337,6 +339,10 @@ class Torrent:
|
||||
LTSTATE = deluge.common.LT_TORRENT_STATE
|
||||
ltstate = int(self.handle.status().state)
|
||||
|
||||
# Set self.state to the ltstate right away just incase we don't hit some
|
||||
# of the logic below
|
||||
self.state = str(ltstate)
|
||||
|
||||
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())
|
||||
|
||||
@ -387,12 +393,11 @@ 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
|
||||
@ -414,9 +419,7 @@ class Torrent:
|
||||
else:
|
||||
status = self.status
|
||||
|
||||
if status.all_time_download > 0:
|
||||
downloaded = status.all_time_download
|
||||
elif status.total_done > 0:
|
||||
if status.total_done > 0:
|
||||
# We use 'total_done' if the downloaded value is 0
|
||||
downloaded = status.total_done
|
||||
else:
|
||||
@ -491,7 +494,7 @@ class Torrent:
|
||||
|
||||
file_progress = self.handle.file_progress()
|
||||
ret = []
|
||||
for i,f in enumerate(self.files):
|
||||
for i,f in enumerate(self.get_files()):
|
||||
try:
|
||||
ret.append(float(file_progress[i]) / float(f["size"]))
|
||||
except ZeroDivisionError:
|
||||
@ -536,8 +539,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():
|
||||
@ -559,7 +575,6 @@ class Torrent:
|
||||
"distributed_copies": distributed_copies,
|
||||
"download_payload_rate": self.status.download_payload_rate,
|
||||
"file_priorities": self.options["file_priorities"],
|
||||
"files": self.files,
|
||||
"hash": self.torrent_id,
|
||||
"is_auto_managed": self.options["auto_managed"],
|
||||
"is_finished": self.is_finished,
|
||||
@ -608,12 +623,29 @@ class Torrent:
|
||||
def ti_name():
|
||||
if self.handle.has_metadata():
|
||||
name = self.torrent_info.file_at(0).path.split("/", 1)[0]
|
||||
if not name:
|
||||
name = self.torrent_info.name()
|
||||
try:
|
||||
return name.decode("utf8", "ignore")
|
||||
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()
|
||||
@ -639,6 +671,7 @@ class Torrent:
|
||||
"comment": ti_comment,
|
||||
"eta": self.get_eta,
|
||||
"file_progress": self.get_file_progress,
|
||||
"files": self.get_files,
|
||||
"is_seed": self.handle.is_seed,
|
||||
"name": ti_name,
|
||||
"num_files": ti_num_files,
|
||||
@ -665,6 +698,24 @@ class Torrent:
|
||||
status_dict[key] = full_status[key]
|
||||
elif key in fns:
|
||||
status_dict[key] = fns[key]()
|
||||
|
||||
session_id = self.rpcserver.get_session_id()
|
||||
if diff:
|
||||
if session_id in self.prev_status:
|
||||
# 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
|
||||
|
||||
@ -709,13 +760,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
|
||||
@ -742,6 +788,14 @@ class Torrent:
|
||||
|
||||
def move_storage(self, dest):
|
||||
"""Move a torrent's storage location"""
|
||||
if not os.path.exists(dest):
|
||||
try:
|
||||
# Try to make the destination path if it doesn't exist
|
||||
os.makedirs(dest)
|
||||
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)
|
||||
return False
|
||||
try:
|
||||
self.handle.move_storage(dest.encode("utf8"))
|
||||
except:
|
||||
@ -762,9 +816,10 @@ class Torrent:
|
||||
self.torrent_id)
|
||||
log.debug("Writing torrent file: %s", path)
|
||||
try:
|
||||
ti = self.handle.get_torrent_info()
|
||||
md = lt.bdecode(ti.metadata())
|
||||
log.debug("md: %s", md)
|
||||
self.torrent_info = self.handle.get_torrent_info()
|
||||
# Regenerate the file priorities
|
||||
self.set_file_priorities([])
|
||||
md = lt.bdecode(self.torrent_info.metadata())
|
||||
torrent_file = {}
|
||||
torrent_file["info"] = md
|
||||
open(path, "wb").write(lt.bencode(torrent_file))
|
||||
@ -816,7 +871,7 @@ class Torrent:
|
||||
"""Renames files in the torrent. 'filenames' should be a list of
|
||||
(index, filename) pairs."""
|
||||
for index, filename in filenames:
|
||||
self.handle.rename_file(index, filename)
|
||||
self.handle.rename_file(index, filename.encode("utf-8"))
|
||||
|
||||
def rename_folder(self, folder, new_folder):
|
||||
"""Renames a folder within a torrent. This basically does a file rename
|
||||
@ -834,5 +889,16 @@ class Torrent:
|
||||
if f["path"].startswith(folder):
|
||||
# Keep a list of filerenames we're waiting on
|
||||
wait_on_folder[2].append(f["index"])
|
||||
self.handle.rename_file(f["index"], f["path"].replace(folder, new_folder, 1))
|
||||
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]
|
||||
|
||||
|
@ -56,6 +56,7 @@ from deluge.configmanager import ConfigManager, get_config_dir
|
||||
from deluge.core.torrent import Torrent
|
||||
from deluge.core.torrent import TorrentOptions
|
||||
import deluge.core.oldstateupgrader
|
||||
from deluge.ui.common import utf8_encoded
|
||||
|
||||
from deluge.log import LOG as log
|
||||
|
||||
@ -132,6 +133,10 @@ class TorrentManager(component.Component):
|
||||
# Get the core config
|
||||
self.config = ConfigManager("core.conf")
|
||||
|
||||
# Make sure the state folder has been created
|
||||
if not os.path.exists(os.path.join(get_config_dir(), "state")):
|
||||
os.makedirs(os.path.join(get_config_dir(), "state"))
|
||||
|
||||
# Create the torrents dict { torrent_id: Torrent }
|
||||
self.torrents = {}
|
||||
|
||||
@ -142,7 +147,7 @@ class TorrentManager(component.Component):
|
||||
|
||||
# self.num_resume_data used to save resume_data in bulk
|
||||
self.num_resume_data = 0
|
||||
|
||||
|
||||
# Keeps track of resume data that needs to be saved to disk
|
||||
self.resume_data = {}
|
||||
|
||||
@ -187,6 +192,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
|
||||
@ -200,34 +207,39 @@ class TorrentManager(component.Component):
|
||||
|
||||
# Save the state every 5 minutes
|
||||
self.save_state_timer = LoopingCall(self.save_state)
|
||||
self.save_state_timer.start(200)
|
||||
self.save_state_timer.start(200, False)
|
||||
self.save_resume_data_timer = LoopingCall(self.save_resume_data)
|
||||
self.save_resume_data_timer.start(190)
|
||||
|
||||
def stop(self):
|
||||
# Stop timers
|
||||
self.save_state_timer.stop()
|
||||
self.save_resume_data_timer.stop()
|
||||
if self.save_state_timer.running:
|
||||
self.save_state_timer.stop()
|
||||
|
||||
if self.save_resume_data_timer.running:
|
||||
self.save_resume_data_timer.stop()
|
||||
|
||||
# Save state on shutdown
|
||||
self.save_state()
|
||||
|
||||
# Make another list just to make sure all paused torrents will be
|
||||
# passed to self.save_resume_data(). With
|
||||
# Make another list just to make sure all paused torrents will be
|
||||
# passed to self.save_resume_data(). With
|
||||
# self.shutdown_torrent_pause_list it is possible to have a case when
|
||||
# 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)
|
||||
self.torrents[key].handle.pause()
|
||||
self.shutdown_torrent_pause_list.append(key)
|
||||
save_resume_data_list.append(key)
|
||||
|
||||
|
||||
self.save_resume_data(save_resume_data_list)
|
||||
|
||||
|
||||
# We have to wait for all torrents to pause and write their resume data
|
||||
wait = True
|
||||
while wait:
|
||||
@ -246,12 +258,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"):
|
||||
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.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 not torrent.options["stop_at_ratio"]:
|
||||
continue
|
||||
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():
|
||||
@ -294,7 +307,7 @@ class TorrentManager(component.Component):
|
||||
|
||||
def legacy_delete_resume_data(self, torrent_id):
|
||||
"""Deletes the .fastresume file"""
|
||||
path = os.path.join(self.config["state_location"],
|
||||
path = os.path.join(get_config_dir(), "state",
|
||||
torrent_id + ".fastresume")
|
||||
log.debug("Deleting fastresume file: %s", path)
|
||||
try:
|
||||
@ -343,24 +356,23 @@ class TorrentManager(component.Component):
|
||||
options["move_completed_path"] = state.move_completed_path
|
||||
options["add_paused"] = state.paused
|
||||
|
||||
if not state.magnet:
|
||||
add_torrent_params["ti"] =\
|
||||
self.get_torrent_info_from_file(
|
||||
os.path.join(get_config_dir(),
|
||||
"state", state.torrent_id + ".torrent"))
|
||||
|
||||
if not add_torrent_params["ti"]:
|
||||
log.error("Unable to add torrent!")
|
||||
return
|
||||
else:
|
||||
ti = self.get_torrent_info_from_file(
|
||||
os.path.join(get_config_dir(),
|
||||
"state", state.torrent_id + ".torrent"))
|
||||
if ti:
|
||||
add_torrent_params["ti"] = ti
|
||||
elif state.magnet:
|
||||
magnet = state.magnet
|
||||
else:
|
||||
log.error("Unable to add torrent!")
|
||||
return
|
||||
|
||||
# Handle legacy case with storing resume data in individual files
|
||||
# for each torrent
|
||||
if resume_data is None:
|
||||
resume_data = self.legacy_get_resume_data_from_file(state.torrent_id)
|
||||
self.legacy_delete_resume_data(state.torrent_id)
|
||||
|
||||
|
||||
add_torrent_params["resume_data"] = resume_data
|
||||
else:
|
||||
# We have a torrent_info object so we're not loading from state.
|
||||
@ -377,7 +389,7 @@ class TorrentManager(component.Component):
|
||||
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)
|
||||
torrent_info.rename_file(index, utf8_encoded(name))
|
||||
|
||||
add_torrent_params["ti"] = torrent_info
|
||||
add_torrent_params["resume_data"] = ""
|
||||
@ -391,14 +403,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
|
||||
@ -469,6 +475,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):
|
||||
@ -508,6 +515,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))
|
||||
|
||||
@ -517,7 +526,7 @@ class TorrentManager(component.Component):
|
||||
except (RuntimeError, KeyError), e:
|
||||
log.warning("Error removing torrent: %s", e)
|
||||
return False
|
||||
|
||||
|
||||
# Remove fastresume data if it is exists
|
||||
resume_data = self.load_resume_data_file()
|
||||
resume_data.pop(torrent_id, None)
|
||||
@ -526,6 +535,24 @@ 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 the torrent from deluge's session
|
||||
try:
|
||||
del self.torrents[torrent_id]
|
||||
@ -537,7 +564,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):
|
||||
@ -618,7 +645,7 @@ class TorrentManager(component.Component):
|
||||
# Pickle the TorrentManagerState object
|
||||
try:
|
||||
log.debug("Saving torrent state file.")
|
||||
state_file = open(os.path.join(get_config_dir(),
|
||||
state_file = open(os.path.join(get_config_dir(),
|
||||
"state", "torrents.state.new"), "wb")
|
||||
cPickle.dump(state, state_file)
|
||||
state_file.flush()
|
||||
@ -645,13 +672,13 @@ class TorrentManager(component.Component):
|
||||
Saves resume data for list of torrent_ids or for all torrents if
|
||||
torrent_ids is None
|
||||
"""
|
||||
|
||||
|
||||
if torrent_ids is None:
|
||||
torrent_ids = self.torrents.keys()
|
||||
|
||||
|
||||
for torrent_id in torrent_ids:
|
||||
self.torrents[torrent_id].save_resume_data()
|
||||
|
||||
|
||||
self.num_resume_data = len(torrent_ids)
|
||||
|
||||
def load_resume_data_file(self):
|
||||
@ -664,35 +691,35 @@ class TorrentManager(component.Component):
|
||||
fastresume_file.close()
|
||||
except (EOFError, IOError, Exception), e:
|
||||
log.warning("Unable to load fastresume file: %s", e)
|
||||
|
||||
|
||||
# If the libtorrent bdecode doesn't happen properly, it will return None
|
||||
# so we need to make sure we return a {}
|
||||
if resume_data is None:
|
||||
return {}
|
||||
|
||||
|
||||
return resume_data
|
||||
|
||||
|
||||
def save_resume_data_file(self, resume_data=None):
|
||||
"""
|
||||
Saves the resume data file with the contents of self.resume_data. If
|
||||
`resume_data` is None, then we grab the resume_data from the file on
|
||||
disk, else, we update `resume_data` with self.resume_data and save
|
||||
that to disk.
|
||||
|
||||
|
||||
:param resume_data: the current resume_data, this will be loaded from disk if not provided
|
||||
:type resume_data: dict
|
||||
|
||||
|
||||
"""
|
||||
# Check to see if we're waiting on more resume data
|
||||
if self.num_resume_data or not self.resume_data:
|
||||
return
|
||||
|
||||
|
||||
path = os.path.join(get_config_dir(), "state", "torrents.fastresume")
|
||||
|
||||
|
||||
# First step is to load the existing file and update the dictionary
|
||||
if resume_data is None:
|
||||
resume_data = self.load_resume_data_file()
|
||||
|
||||
|
||||
resume_data.update(self.resume_data)
|
||||
self.resume_data = {}
|
||||
|
||||
@ -775,20 +802,19 @@ class TorrentManager(component.Component):
|
||||
total_download = torrent.get_status(["total_payload_download"])["total_payload_download"]
|
||||
|
||||
# Move completed download to completed folder if needed
|
||||
if not torrent.is_finished:
|
||||
if not torrent.is_finished and total_download:
|
||||
move_path = None
|
||||
|
||||
if torrent.options["move_completed"] and total_download:
|
||||
if torrent.options["move_completed"]:
|
||||
move_path = torrent.options["move_completed_path"]
|
||||
if torrent.options["download_location"] != move_path and \
|
||||
torrent.options["download_location"] == self.config["download_location"]:
|
||||
if torrent.options["download_location"] != move_path:
|
||||
torrent.move_storage(move_path)
|
||||
|
||||
|
||||
torrent.is_finished = True
|
||||
component.get("EventManager").emit(TorrentFinishedEvent(torrent_id))
|
||||
|
||||
torrent.update_state()
|
||||
|
||||
|
||||
# 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
|
||||
@ -882,6 +908,7 @@ class TorrentManager(component.Component):
|
||||
except:
|
||||
return
|
||||
torrent.set_save_path(alert.handle.save_path())
|
||||
torrent.set_move_completed(False)
|
||||
|
||||
def on_alert_torrent_resumed(self, alert):
|
||||
log.debug("on_alert_torrent_resumed")
|
||||
@ -914,9 +941,9 @@ class TorrentManager(component.Component):
|
||||
|
||||
def on_alert_save_resume_data(self, alert):
|
||||
log.debug("on_alert_save_resume_data")
|
||||
|
||||
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
|
||||
|
||||
try:
|
||||
torrent = self.torrents[torrent_id]
|
||||
except:
|
||||
@ -925,9 +952,9 @@ class TorrentManager(component.Component):
|
||||
# Libtorrent in add_torrent() expects resume_data to be bencoded
|
||||
self.resume_data[torrent_id] = lt.bencode(alert.resume_data)
|
||||
self.num_resume_data -= 1
|
||||
|
||||
|
||||
torrent.waiting_on_resume_data = False
|
||||
|
||||
|
||||
self.save_resume_data_file()
|
||||
|
||||
def on_alert_save_resume_data_failed(self, alert):
|
||||
@ -936,12 +963,12 @@ class TorrentManager(component.Component):
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
except:
|
||||
return
|
||||
|
||||
|
||||
self.num_resume_data -= 1
|
||||
torrent.waiting_on_resume_data = False
|
||||
|
||||
|
||||
self.save_resume_data_file()
|
||||
|
||||
|
||||
|
||||
def on_alert_file_renamed(self, alert):
|
||||
log.debug("on_alert_file_renamed")
|
||||
@ -951,7 +978,6 @@ class TorrentManager(component.Component):
|
||||
except:
|
||||
return
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
torrent.files[alert.index]["path"] = alert.name
|
||||
|
||||
# We need to see if this file index is in a waiting_on_folder list
|
||||
folder_rename = False
|
||||
@ -988,3 +1014,9 @@ class TorrentManager(component.Component):
|
||||
except:
|
||||
return
|
||||
torrent.update_state()
|
||||
|
||||
def on_alert_file_completed(self, alert):
|
||||
log.debug("file_completed_alert: %s", alert.message())
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
component.get("EventManager").emit(
|
||||
TorrentFileCompletedEvent(torrent_id, alert.index))
|
||||
|
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/lock48.png
Normal file
BIN
deluge/data/pixmaps/lock48.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
BIN
deluge/data/pixmaps/magnet.png
Normal file
BIN
deluge/data/pixmaps/magnet.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 906 B |
@ -3,7 +3,7 @@ Version=1.0
|
||||
Name=Deluge BitTorrent Client
|
||||
GenericName=Bittorrent Client
|
||||
Comment=Transfer files using the Bittorrent protocol
|
||||
Exec=deluge
|
||||
Exec=deluge-gtk
|
||||
Icon=deluge
|
||||
Terminal=false
|
||||
Type=Application
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# gtkui.py
|
||||
# decorators.py
|
||||
#
|
||||
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2010 John Garland <johnnybg+deluge@gmail.com>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
@ -33,16 +33,19 @@
|
||||
#
|
||||
#
|
||||
|
||||
import gtk
|
||||
from functools import wraps
|
||||
|
||||
from deluge.log import LOG as log
|
||||
from deluge.ui.client import client
|
||||
from deluge.plugins.pluginbase import GtkPluginBase
|
||||
import deluge.component as component
|
||||
import deluge.common
|
||||
def proxy(proxy_func):
|
||||
"""
|
||||
Factory class which returns a decorator that passes
|
||||
the decorated function to a proxy function
|
||||
|
||||
class GtkUI(GtkPluginBase):
|
||||
def enable(self):
|
||||
pass
|
||||
def disable(self):
|
||||
pass
|
||||
: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
|
@ -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,17 @@ and subsequently emitted to the clients.
|
||||
|
||||
"""
|
||||
|
||||
known_events = {}
|
||||
|
||||
class DelugeEventMetaClass(type):
|
||||
"""
|
||||
This metaclass simply keeps a list of all events classes created.
|
||||
"""
|
||||
def __init__(cls, name, bases, dct):
|
||||
super(DelugeEventMetaClass, cls).__init__(name, bases, dct)
|
||||
if name != "DelugeEvent":
|
||||
known_events[name] = cls
|
||||
|
||||
class DelugeEvent(object):
|
||||
"""
|
||||
The base class for all events.
|
||||
@ -49,6 +60,8 @@ class DelugeEvent(object):
|
||||
:prop args: a list of the attribute values
|
||||
|
||||
"""
|
||||
__metaclass__ = DelugeEventMetaClass
|
||||
|
||||
def _get_name(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
@ -151,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,12 +36,16 @@ 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
|
||||
@ -53,10 +57,14 @@ class HTTPDownloader(client.HTTPDownloader):
|
||||
: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 +76,25 @@ 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:
|
||||
try:
|
||||
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)
|
||||
except Exception, e:
|
||||
log.exception(e)
|
||||
else:
|
||||
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 +104,55 @@ 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
|
||||
|
||||
:raises IOError: when the filename exists
|
||||
"""
|
||||
|
||||
# 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)
|
||||
|
||||
if os.path.exists(filename):
|
||||
raise IOError, "File '%s' already exists!" % 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 +166,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 +185,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,139 +1,290 @@
|
||||
deluge/plugins/label/label/data/label_pref.glade
|
||||
deluge/plugins/label/label/data/label_options.glade
|
||||
deluge/plugins/blocklist/blocklist/data/blocklist_pref.glade
|
||||
deluge/plugins/stats/stats/data/config.glade
|
||||
deluge/plugins/stats/stats/data/tabs.glade
|
||||
deluge/ui/gtkui/glade/add_torrent_dialog.glade
|
||||
deluge/ui/gtkui/glade/filtertree_menu.glade
|
||||
deluge/ui/gtkui/glade/torrent_menu.glade
|
||||
deluge/ui/gtkui/glade/remove_torrent_dialog.glade
|
||||
deluge/ui/gtkui/glade/preferences_dialog.glade
|
||||
deluge/ui/gtkui/glade/edit_trackers.glade
|
||||
deluge/ui/gtkui/glade/queuedtorrents.glade
|
||||
deluge/ui/gtkui/glade/move_storage_dialog.glade
|
||||
deluge/ui/gtkui/glade/connection_manager.glade
|
||||
deluge/ui/gtkui/glade/create_torrent_dialog.glade
|
||||
deluge/ui/gtkui/glade/dgtkpopups.glade
|
||||
deluge/ui/gtkui/glade/tray_menu.glade
|
||||
deluge/ui/gtkui/glade/main_window.glade
|
||||
deluge/bencode.py
|
||||
docs/source/conf.py
|
||||
deluge/core/autoadd.py
|
||||
deluge/core/preferencesmanager.py
|
||||
deluge/core/filtermanager.py
|
||||
deluge/core/torrentmanager.py
|
||||
deluge/core/daemon.py
|
||||
deluge/core/torrent.py
|
||||
deluge/core/pluginmanager.py
|
||||
deluge/core/oldstateupgrader.py
|
||||
deluge/core/__init__.py
|
||||
deluge/core/core.py
|
||||
deluge/core/alertmanager.py
|
||||
deluge/core/rpcserver.py
|
||||
deluge/config.py
|
||||
deluge/countries.py
|
||||
deluge/configmanager.py
|
||||
deluge/httpdownloader.py
|
||||
deluge/error.py
|
||||
deluge/component.py
|
||||
deluge/log.py
|
||||
deluge/metafile.py
|
||||
deluge/config.py
|
||||
deluge/main.py
|
||||
deluge/__init__.py
|
||||
deluge/common.py
|
||||
deluge/bencode.py
|
||||
deluge/pluginmanagerbase.py
|
||||
deluge/plugins/label/label/webui.py
|
||||
deluge/plugins/label/label/test.py
|
||||
deluge/plugins/label/label/gtkui/label_config.py
|
||||
deluge/plugins/label/label/gtkui/sidebar_menu.py
|
||||
deluge/plugins/label/label/gtkui/submenu.py
|
||||
deluge/plugins/label/label/gtkui/__init__.py
|
||||
deluge/plugins/label/label/__init__.py
|
||||
deluge/plugins/label/label/core.py
|
||||
deluge/plugins/label/setup.py
|
||||
deluge/event.py
|
||||
deluge/rencode.py
|
||||
deluge/decorators.py
|
||||
deluge/_libtorrent.py
|
||||
deluge/__rpcapi.py
|
||||
deluge/maketorrent.py
|
||||
deluge/plugins/__init__.py
|
||||
deluge/plugins/pluginbase.py
|
||||
deluge/plugins/init.py
|
||||
deluge/plugins/blocklist/setup.py
|
||||
deluge/plugins/blocklist/blocklist/webui.py
|
||||
deluge/plugins/blocklist/blocklist/gtkui.py
|
||||
deluge/plugins/blocklist/blocklist/text.py
|
||||
deluge/plugins/blocklist/blocklist/peerguardian.py
|
||||
deluge/plugins/blocklist/blocklist/__init__.py
|
||||
deluge/plugins/blocklist/blocklist/core.py
|
||||
deluge/plugins/__init__.py
|
||||
deluge/plugins/webuipluginbase.py
|
||||
deluge/plugins/feeder/setup.py
|
||||
deluge/plugins/feeder/feeder/__init__.py
|
||||
deluge/plugins/feeder/feeder/core.py
|
||||
deluge/plugins/feeder/feeder/webui.py
|
||||
deluge/plugins/feeder/build/lib/feeder/__init__.py
|
||||
deluge/plugins/feeder/build/lib/feeder/core.py
|
||||
deluge/plugins/feeder/build/lib/feeder/webui.py
|
||||
deluge/plugins/label/setup.py
|
||||
deluge/plugins/label/label/test.py
|
||||
deluge/plugins/label/label/__init__.py
|
||||
deluge/plugins/label/label/core.py
|
||||
deluge/plugins/label/label/webui.py
|
||||
deluge/plugins/label/label/gtkui/__init__.py
|
||||
deluge/plugins/label/label/gtkui/label_config.py
|
||||
deluge/plugins/label/label/gtkui/submenu.py
|
||||
deluge/plugins/label/label/gtkui/sidebar_menu.py
|
||||
deluge/plugins/label/label/data/label_pref.glade
|
||||
deluge/plugins/label/label/data/label_options.glade
|
||||
deluge/plugins/label/build/lib/label/test.py
|
||||
deluge/plugins/label/build/lib/label/__init__.py
|
||||
deluge/plugins/label/build/lib/label/core.py
|
||||
deluge/plugins/label/build/lib/label/webui.py
|
||||
deluge/plugins/label/build/lib/label/gtkui/__init__.py
|
||||
deluge/plugins/label/build/lib/label/gtkui/label_config.py
|
||||
deluge/plugins/label/build/lib/label/gtkui/submenu.py
|
||||
deluge/plugins/label/build/lib/label/gtkui/sidebar_menu.py
|
||||
deluge/plugins/label/build/lib/label/data/label_pref.glade
|
||||
deluge/plugins/label/build/lib/label/data/label_options.glade
|
||||
deluge/plugins/autoadd/setup.py
|
||||
deluge/plugins/autoadd/autoadd/__init__.py
|
||||
deluge/plugins/autoadd/autoadd/common.py
|
||||
deluge/plugins/autoadd/autoadd/core.py
|
||||
deluge/plugins/autoadd/autoadd/webui.py
|
||||
deluge/plugins/autoadd/autoadd/gtkui.py
|
||||
deluge/plugins/autoadd/autoadd/data/config.glade
|
||||
deluge/plugins/autoadd/autoadd/data/autoadd_options.glade
|
||||
deluge/plugins/autoadd/build/lib/autoadd/__init__.py
|
||||
deluge/plugins/autoadd/build/lib/autoadd/common.py
|
||||
deluge/plugins/autoadd/build/lib/autoadd/core.py
|
||||
deluge/plugins/autoadd/build/lib/autoadd/webui.py
|
||||
deluge/plugins/autoadd/build/lib/autoadd/gtkui.py
|
||||
deluge/plugins/autoadd/build/lib/autoadd/data/config.glade
|
||||
deluge/plugins/autoadd/build/lib/autoadd/data/autoadd_options.glade
|
||||
deluge/plugins/scheduler/setup.py
|
||||
deluge/plugins/scheduler/scheduler/__init__.py
|
||||
deluge/plugins/scheduler/scheduler/common.py
|
||||
deluge/plugins/scheduler/scheduler/core.py
|
||||
deluge/plugins/scheduler/scheduler/webui.py
|
||||
deluge/plugins/scheduler/scheduler/gtkui.py
|
||||
deluge/plugins/scheduler/build/lib/scheduler/__init__.py
|
||||
deluge/plugins/scheduler/build/lib/scheduler/common.py
|
||||
deluge/plugins/scheduler/build/lib/scheduler/core.py
|
||||
deluge/plugins/scheduler/build/lib/scheduler/webui.py
|
||||
deluge/plugins/scheduler/build/lib/scheduler/gtkui.py
|
||||
deluge/plugins/notifications/setup.py
|
||||
deluge/plugins/notifications/notifications/test.py
|
||||
deluge/plugins/notifications/notifications/__init__.py
|
||||
deluge/plugins/notifications/notifications/common.py
|
||||
deluge/plugins/notifications/notifications/core.py
|
||||
deluge/plugins/notifications/notifications/webui.py
|
||||
deluge/plugins/notifications/notifications/gtkui.py
|
||||
deluge/plugins/notifications/notifications/data/config.glade
|
||||
deluge/plugins/notifications/build/lib/notifications/test.py
|
||||
deluge/plugins/notifications/build/lib/notifications/__init__.py
|
||||
deluge/plugins/notifications/build/lib/notifications/common.py
|
||||
deluge/plugins/notifications/build/lib/notifications/core.py
|
||||
deluge/plugins/notifications/build/lib/notifications/webui.py
|
||||
deluge/plugins/notifications/build/lib/notifications/gtkui.py
|
||||
deluge/plugins/notifications/build/lib/notifications/data/config.glade
|
||||
deluge/plugins/stats/setup.py
|
||||
deluge/plugins/stats/stats/test_total.py
|
||||
deluge/plugins/stats/stats/test.py
|
||||
deluge/plugins/stats/stats/__init__.py
|
||||
deluge/plugins/stats/stats/graph.py
|
||||
deluge/plugins/stats/stats/common.py
|
||||
deluge/plugins/stats/stats/core.py
|
||||
deluge/plugins/stats/stats/webui.py
|
||||
deluge/plugins/stats/stats/gtkui.py
|
||||
deluge/plugins/stats/stats/test.py
|
||||
deluge/plugins/stats/stats/graph.py
|
||||
deluge/plugins/stats/stats/test_total.py
|
||||
deluge/plugins/stats/stats/__init__.py
|
||||
deluge/plugins/stats/stats/core.py
|
||||
deluge/configmanager.py
|
||||
deluge/ui/tracker_icons.py
|
||||
deluge/ui/client.py
|
||||
deluge/plugins/stats/stats/data/tabs.glade
|
||||
deluge/plugins/stats/stats/data/config.glade
|
||||
deluge/plugins/stats/build/lib/stats/test_total.py
|
||||
deluge/plugins/stats/build/lib/stats/test.py
|
||||
deluge/plugins/stats/build/lib/stats/__init__.py
|
||||
deluge/plugins/stats/build/lib/stats/graph.py
|
||||
deluge/plugins/stats/build/lib/stats/common.py
|
||||
deluge/plugins/stats/build/lib/stats/core.py
|
||||
deluge/plugins/stats/build/lib/stats/webui.py
|
||||
deluge/plugins/stats/build/lib/stats/gtkui.py
|
||||
deluge/plugins/stats/build/lib/stats/data/tabs.glade
|
||||
deluge/plugins/stats/build/lib/stats/data/config.glade
|
||||
deluge/plugins/webui/setup.py
|
||||
deluge/plugins/webui/webui/__init__.py
|
||||
deluge/plugins/webui/webui/common.py
|
||||
deluge/plugins/webui/webui/core.py
|
||||
deluge/plugins/webui/webui/gtkui.py
|
||||
deluge/plugins/webui/webui/data/config.glade
|
||||
deluge/plugins/webui/build/lib/webui/__init__.py
|
||||
deluge/plugins/webui/build/lib/webui/common.py
|
||||
deluge/plugins/webui/build/lib/webui/core.py
|
||||
deluge/plugins/webui/build/lib/webui/gtkui.py
|
||||
deluge/plugins/webui/build/lib/webui/data/config.glade
|
||||
deluge/plugins/extractor/setup.py
|
||||
deluge/plugins/extractor/build/lib/extractor/__init__.py
|
||||
deluge/plugins/extractor/build/lib/extractor/common.py
|
||||
deluge/plugins/extractor/build/lib/extractor/core.py
|
||||
deluge/plugins/extractor/build/lib/extractor/webui.py
|
||||
deluge/plugins/extractor/build/lib/extractor/gtkui.py
|
||||
deluge/plugins/extractor/build/lib/extractor/data/extractor_prefs.glade
|
||||
deluge/plugins/extractor/extractor/__init__.py
|
||||
deluge/plugins/extractor/extractor/common.py
|
||||
deluge/plugins/extractor/extractor/core.py
|
||||
deluge/plugins/extractor/extractor/webui.py
|
||||
deluge/plugins/extractor/extractor/gtkui.py
|
||||
deluge/plugins/extractor/extractor/data/extractor_prefs.glade
|
||||
deluge/plugins/execute/setup.py
|
||||
deluge/plugins/execute/build/lib/execute/__init__.py
|
||||
deluge/plugins/execute/build/lib/execute/common.py
|
||||
deluge/plugins/execute/build/lib/execute/core.py
|
||||
deluge/plugins/execute/build/lib/execute/webui.py
|
||||
deluge/plugins/execute/build/lib/execute/gtkui.py
|
||||
deluge/plugins/execute/build/lib/execute/data/execute_prefs.glade
|
||||
deluge/plugins/execute/execute/__init__.py
|
||||
deluge/plugins/execute/execute/common.py
|
||||
deluge/plugins/execute/execute/core.py
|
||||
deluge/plugins/execute/execute/webui.py
|
||||
deluge/plugins/execute/execute/gtkui.py
|
||||
deluge/plugins/execute/execute/data/execute_prefs.glade
|
||||
deluge/plugins/example/setup.py
|
||||
deluge/plugins/example/build/lib/example/__init__.py
|
||||
deluge/plugins/example/build/lib/example/common.py
|
||||
deluge/plugins/example/build/lib/example/core.py
|
||||
deluge/plugins/example/build/lib/example/webui.py
|
||||
deluge/plugins/example/build/lib/example/gtkui.py
|
||||
deluge/plugins/example/example/__init__.py
|
||||
deluge/plugins/example/example/common.py
|
||||
deluge/plugins/example/example/core.py
|
||||
deluge/plugins/example/example/webui.py
|
||||
deluge/plugins/example/example/gtkui.py
|
||||
deluge/plugins/freespace/setup.py
|
||||
deluge/plugins/freespace/build/lib/freespace/__init__.py
|
||||
deluge/plugins/freespace/build/lib/freespace/common.py
|
||||
deluge/plugins/freespace/build/lib/freespace/core.py
|
||||
deluge/plugins/freespace/build/lib/freespace/webui.py
|
||||
deluge/plugins/freespace/build/lib/freespace/gtkui.py
|
||||
deluge/plugins/freespace/build/lib/freespace/data/config.glade
|
||||
deluge/plugins/freespace/freespace/__init__.py
|
||||
deluge/plugins/freespace/freespace/common.py
|
||||
deluge/plugins/freespace/freespace/core.py
|
||||
deluge/plugins/freespace/freespace/webui.py
|
||||
deluge/plugins/freespace/freespace/gtkui.py
|
||||
deluge/plugins/freespace/freespace/data/config.glade
|
||||
deluge/plugins/blocklist/setup.py
|
||||
deluge/plugins/blocklist/build/lib/blocklist/peerguardian.py
|
||||
deluge/plugins/blocklist/build/lib/blocklist/decompressers.py
|
||||
deluge/plugins/blocklist/build/lib/blocklist/detect.py
|
||||
deluge/plugins/blocklist/build/lib/blocklist/readers.py
|
||||
deluge/plugins/blocklist/build/lib/blocklist/__init__.py
|
||||
deluge/plugins/blocklist/build/lib/blocklist/common.py
|
||||
deluge/plugins/blocklist/build/lib/blocklist/core.py
|
||||
deluge/plugins/blocklist/build/lib/blocklist/webui.py
|
||||
deluge/plugins/blocklist/build/lib/blocklist/gtkui.py
|
||||
deluge/plugins/blocklist/build/lib/blocklist/data/blocklist_pref.glade
|
||||
deluge/plugins/blocklist/blocklist/peerguardian.py
|
||||
deluge/plugins/blocklist/blocklist/decompressers.py
|
||||
deluge/plugins/blocklist/blocklist/detect.py
|
||||
deluge/plugins/blocklist/blocklist/readers.py
|
||||
deluge/plugins/blocklist/blocklist/__init__.py
|
||||
deluge/plugins/blocklist/blocklist/common.py
|
||||
deluge/plugins/blocklist/blocklist/core.py
|
||||
deluge/plugins/blocklist/blocklist/webui.py
|
||||
deluge/plugins/blocklist/blocklist/gtkui.py
|
||||
deluge/plugins/blocklist/blocklist/data/blocklist_pref.glade
|
||||
deluge/core/eventmanager.py
|
||||
deluge/core/autoadd.py
|
||||
deluge/core/authmanager.py
|
||||
deluge/core/rpcserver.py
|
||||
deluge/core/torrentmanager.py
|
||||
deluge/core/oldstateupgrader.py
|
||||
deluge/core/__init__.py
|
||||
deluge/core/torrent.py
|
||||
deluge/core/pluginmanager.py
|
||||
deluge/core/core.py
|
||||
deluge/core/daemon.py
|
||||
deluge/core/alertmanager.py
|
||||
deluge/core/preferencesmanager.py
|
||||
deluge/core/filtermanager.py
|
||||
deluge/ui/sessionproxy.py
|
||||
deluge/ui/ui.py
|
||||
deluge/ui/coreconfig.py
|
||||
deluge/ui/console/colors.py
|
||||
deluge/ui/console/commands/resume.py
|
||||
deluge/ui/console/commands/config.py
|
||||
deluge/ui/console/commands/halt.py
|
||||
deluge/ui/console/commands/debug.py
|
||||
deluge/ui/console/commands/__init__.py
|
||||
deluge/ui/console/commands/quit.py
|
||||
deluge/ui/console/commands/connect.py
|
||||
deluge/ui/console/commands/pause.py
|
||||
deluge/ui/console/commands/add.py
|
||||
deluge/ui/console/commands/rm.py
|
||||
deluge/ui/console/commands/info.py
|
||||
deluge/ui/console/commands/help.py
|
||||
deluge/ui/console/main.py
|
||||
deluge/ui/console/__init__.py
|
||||
deluge/ui/gtkui/listview.py
|
||||
deluge/ui/gtkui/options_tab.py
|
||||
deluge/ui/gtkui/statusbar.py
|
||||
deluge/ui/gtkui/status_tab.py
|
||||
deluge/ui/gtkui/addtorrentdialog.py
|
||||
deluge/ui/gtkui/sidebar.py
|
||||
deluge/ui/gtkui/gtkui.py
|
||||
deluge/ui/gtkui/aboutdialog.py
|
||||
deluge/ui/gtkui/systemtray.py
|
||||
deluge/ui/gtkui/files_tab.py
|
||||
deluge/ui/gtkui/menubar.py
|
||||
deluge/ui/gtkui/peers_tab.py
|
||||
deluge/ui/gtkui/notification.py
|
||||
deluge/ui/gtkui/toolbar.py
|
||||
deluge/ui/gtkui/ipcinterface.py
|
||||
deluge/ui/gtkui/filtertreeview.py
|
||||
deluge/ui/gtkui/queuedtorrents.py
|
||||
deluge/ui/gtkui/pluginmanager.py
|
||||
deluge/ui/gtkui/mainwindow.py
|
||||
deluge/ui/gtkui/removetorrentdialog.py
|
||||
deluge/ui/gtkui/common.py
|
||||
deluge/ui/gtkui/torrentdetails.py
|
||||
deluge/ui/gtkui/__init__.py
|
||||
deluge/ui/gtkui/edittrackersdialog.py
|
||||
deluge/ui/gtkui/preferences.py
|
||||
deluge/ui/gtkui/torrentview.py
|
||||
deluge/ui/gtkui/new_release_dialog.py
|
||||
deluge/ui/gtkui/connectionmanager.py
|
||||
deluge/ui/gtkui/createtorrentdialog.py
|
||||
deluge/ui/gtkui/details_tab.py
|
||||
deluge/ui/common.py
|
||||
deluge/ui/session.py
|
||||
deluge/ui/tracker_icons.py
|
||||
deluge/ui/__init__.py
|
||||
deluge/ui/web/auth.py
|
||||
deluge/ui/web/common.py
|
||||
deluge/ui/web/gen_gettext.py
|
||||
deluge/ui/web/__init__.py
|
||||
deluge/ui/web/json_api.py
|
||||
deluge/ui/web/pluginmanager.py
|
||||
deluge/ui/common.py
|
||||
deluge/ui/Win32IconImagePlugin.py
|
||||
deluge/ui/client.py
|
||||
deluge/ui/countries.py
|
||||
deluge/ui/coreconfig.py
|
||||
deluge/ui/web/server.py
|
||||
deluge/ui/web/web.py
|
||||
deluge/common.py
|
||||
deluge/component.py
|
||||
deluge/main.py
|
||||
deluge/error.py
|
||||
deluge/__init__.py
|
||||
deluge/tests/test_signalreceiver.py
|
||||
deluge/tests/test_filters.py
|
||||
deluge/tests/test_plugin_metadata.py
|
||||
deluge/tests/test_tracker_icons.py
|
||||
deluge/tests/test_stats.py
|
||||
deluge/tests/test_client.py
|
||||
deluge/log.py
|
||||
deluge/scripts/deluge_remote.py
|
||||
deluge/scripts/wiki_docgen.py
|
||||
deluge/scripts/create_plugin.py
|
||||
deluge/ui/web/__init__.py
|
||||
deluge/ui/web/common.py
|
||||
deluge/ui/web/pluginmanager.py
|
||||
deluge/ui/web/gen_gettext.py
|
||||
deluge/ui/web/auth.py
|
||||
deluge/ui/web/json_api.py
|
||||
deluge/ui/gtkui/connectionmanager.py
|
||||
deluge/ui/gtkui/torrentdetails.py
|
||||
deluge/ui/gtkui/queuedtorrents.py
|
||||
deluge/ui/gtkui/addtorrentdialog.py
|
||||
deluge/ui/gtkui/__init__.py
|
||||
deluge/ui/gtkui/status_tab.py
|
||||
deluge/ui/gtkui/preferences.py
|
||||
deluge/ui/gtkui/mainwindow.py
|
||||
deluge/ui/gtkui/notification.py
|
||||
deluge/ui/gtkui/ipcinterface.py
|
||||
deluge/ui/gtkui/createtorrentdialog.py
|
||||
deluge/ui/gtkui/torrentview.py
|
||||
deluge/ui/gtkui/listview.py
|
||||
deluge/ui/gtkui/systemtray.py
|
||||
deluge/ui/gtkui/common.py
|
||||
deluge/ui/gtkui/pluginmanager.py
|
||||
deluge/ui/gtkui/menubar.py
|
||||
deluge/ui/gtkui/sidebar.py
|
||||
deluge/ui/gtkui/statusbar.py
|
||||
deluge/ui/gtkui/filtertreeview.py
|
||||
deluge/ui/gtkui/new_release_dialog.py
|
||||
deluge/ui/gtkui/options_tab.py
|
||||
deluge/ui/gtkui/peers_tab.py
|
||||
deluge/ui/gtkui/details_tab.py
|
||||
deluge/ui/gtkui/files_tab.py
|
||||
deluge/ui/gtkui/gtkui.py
|
||||
deluge/ui/gtkui/edittrackersdialog.py
|
||||
deluge/ui/gtkui/removetorrentdialog.py
|
||||
deluge/ui/gtkui/toolbar.py
|
||||
deluge/ui/gtkui/dialogs.py
|
||||
deluge/ui/gtkui/aboutdialog.py
|
||||
deluge/ui/gtkui/glade/filtertree_menu.glade
|
||||
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/connection_manager.glade
|
||||
deluge/ui/gtkui/glade/preferences_dialog.glade
|
||||
deluge/ui/gtkui/glade/torrent_menu.glade
|
||||
deluge/ui/gtkui/glade/queuedtorrents.glade
|
||||
deluge/ui/gtkui/glade/move_storage_dialog.glade
|
||||
deluge/ui/gtkui/glade/add_torrent_dialog.glade
|
||||
deluge/ui/gtkui/glade/dgtkpopups.glade
|
||||
deluge/ui/gtkui/glade/tray_menu.glade
|
||||
deluge/ui/gtkui/glade/edit_trackers.glade
|
||||
deluge/ui/console/colors.py
|
||||
deluge/ui/console/eventlog.py
|
||||
deluge/ui/console/main.py
|
||||
deluge/ui/console/__init__.py
|
||||
deluge/ui/console/statusbars.py
|
||||
deluge/ui/console/screen.py
|
||||
deluge/ui/console/commands/plugin.py
|
||||
deluge/ui/console/commands/info.py
|
||||
deluge/ui/console/commands/recheck.py
|
||||
deluge/ui/console/commands/quit.py
|
||||
deluge/ui/console/commands/connect.py
|
||||
deluge/ui/console/commands/help.py
|
||||
deluge/ui/console/commands/add.py
|
||||
deluge/ui/console/commands/config.py
|
||||
deluge/ui/console/commands/__init__.py
|
||||
deluge/ui/console/commands/cache.py
|
||||
deluge/ui/console/commands/debug.py
|
||||
deluge/ui/console/commands/pause.py
|
||||
deluge/ui/console/commands/rm.py
|
||||
deluge/ui/console/commands/halt.py
|
||||
deluge/ui/console/commands/resume.py
|
||||
|
5957
deluge/i18n/ar.po
5957
deluge/i18n/ar.po
File diff suppressed because it is too large
Load Diff
5874
deluge/i18n/bg.po
5874
deluge/i18n/bg.po
File diff suppressed because it is too large
Load Diff
5411
deluge/i18n/ca.po
5411
deluge/i18n/ca.po
File diff suppressed because it is too large
Load Diff
5469
deluge/i18n/cs.po
5469
deluge/i18n/cs.po
File diff suppressed because it is too large
Load Diff
5810
deluge/i18n/da.po
5810
deluge/i18n/da.po
File diff suppressed because it is too large
Load Diff
5621
deluge/i18n/de.po
5621
deluge/i18n/de.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
5804
deluge/i18n/el.po
5804
deluge/i18n/el.po
File diff suppressed because it is too large
Load Diff
5571
deluge/i18n/en_AU.po
5571
deluge/i18n/en_AU.po
File diff suppressed because it is too large
Load Diff
5778
deluge/i18n/en_CA.po
5778
deluge/i18n/en_CA.po
File diff suppressed because it is too large
Load Diff
5774
deluge/i18n/en_GB.po
5774
deluge/i18n/en_GB.po
File diff suppressed because it is too large
Load Diff
5966
deluge/i18n/es.po
5966
deluge/i18n/es.po
File diff suppressed because it is too large
Load Diff
5211
deluge/i18n/et.po
5211
deluge/i18n/et.po
File diff suppressed because it is too large
Load Diff
5263
deluge/i18n/eu.po
5263
deluge/i18n/eu.po
File diff suppressed because it is too large
Load Diff
5613
deluge/i18n/fi.po
5613
deluge/i18n/fi.po
File diff suppressed because it is too large
Load Diff
5718
deluge/i18n/fr.po
5718
deluge/i18n/fr.po
File diff suppressed because it is too large
Load Diff
5336
deluge/i18n/gl.po
5336
deluge/i18n/gl.po
File diff suppressed because it is too large
Load Diff
5561
deluge/i18n/he.po
5561
deluge/i18n/he.po
File diff suppressed because it is too large
Load Diff
5285
deluge/i18n/hi.po
5285
deluge/i18n/hi.po
File diff suppressed because it is too large
Load Diff
5682
deluge/i18n/hu.po
5682
deluge/i18n/hu.po
File diff suppressed because it is too large
Load Diff
5315
deluge/i18n/id.po
5315
deluge/i18n/id.po
File diff suppressed because it is too large
Load Diff
5803
deluge/i18n/is.po
5803
deluge/i18n/is.po
File diff suppressed because it is too large
Load Diff
5617
deluge/i18n/it.po
5617
deluge/i18n/it.po
File diff suppressed because it is too large
Load Diff
5806
deluge/i18n/ja.po
5806
deluge/i18n/ja.po
File diff suppressed because it is too large
Load Diff
4848
deluge/i18n/kk.po
4848
deluge/i18n/kk.po
File diff suppressed because it is too large
Load Diff
6634
deluge/i18n/ko.po
6634
deluge/i18n/ko.po
File diff suppressed because it is too large
Load Diff
5631
deluge/i18n/lt.po
5631
deluge/i18n/lt.po
File diff suppressed because it is too large
Load Diff
5784
deluge/i18n/lv.po
5784
deluge/i18n/lv.po
File diff suppressed because it is too large
Load Diff
5797
deluge/i18n/ms.po
5797
deluge/i18n/ms.po
File diff suppressed because it is too large
Load Diff
5695
deluge/i18n/nb.po
5695
deluge/i18n/nb.po
File diff suppressed because it is too large
Load Diff
5604
deluge/i18n/nl.po
5604
deluge/i18n/nl.po
File diff suppressed because it is too large
Load Diff
5537
deluge/i18n/pl.po
5537
deluge/i18n/pl.po
File diff suppressed because it is too large
Load Diff
5756
deluge/i18n/pt.po
5756
deluge/i18n/pt.po
File diff suppressed because it is too large
Load Diff
5559
deluge/i18n/pt_BR.po
5559
deluge/i18n/pt_BR.po
File diff suppressed because it is too large
Load Diff
5761
deluge/i18n/ro.po
5761
deluge/i18n/ro.po
File diff suppressed because it is too large
Load Diff
5615
deluge/i18n/ru.po
5615
deluge/i18n/ru.po
File diff suppressed because it is too large
Load Diff
5975
deluge/i18n/sk.po
5975
deluge/i18n/sk.po
File diff suppressed because it is too large
Load Diff
5405
deluge/i18n/sl.po
5405
deluge/i18n/sl.po
File diff suppressed because it is too large
Load Diff
5725
deluge/i18n/sr.po
5725
deluge/i18n/sr.po
File diff suppressed because it is too large
Load Diff
5609
deluge/i18n/sv.po
5609
deluge/i18n/sv.po
File diff suppressed because it is too large
Load Diff
5247
deluge/i18n/th.po
5247
deluge/i18n/th.po
File diff suppressed because it is too large
Load Diff
5629
deluge/i18n/tr.po
5629
deluge/i18n/tr.po
File diff suppressed because it is too large
Load Diff
5588
deluge/i18n/uk.po
5588
deluge/i18n/uk.po
File diff suppressed because it is too large
Load Diff
6867
deluge/i18n/zh_CN.po
6867
deluge/i18n/zh_CN.po
File diff suppressed because it is too large
Load Diff
6152
deluge/i18n/zh_TW.po
6152
deluge/i18n/zh_TW.po
File diff suppressed because it is too large
Load Diff
@ -135,11 +135,11 @@ def start_daemon():
|
||||
help="Port daemon will listen on", action="store", type="int")
|
||||
parser.add_option("-i", "--interface", dest="interface",
|
||||
help="Interface daemon will listen for bittorrent connections on, \
|
||||
this should be an IP address",
|
||||
this should be an IP address", metavar="IFACE",
|
||||
action="store", type="str")
|
||||
parser.add_option("-u", "--ui-interface", dest="ui_interface",
|
||||
help="Interface daemon will listen for UI connections on, this should be\
|
||||
an IP address", action="store", type="str")
|
||||
an IP address", metavar="IFACE", action="store", type="str")
|
||||
parser.add_option("-d", "--do-not-daemonize", dest="donot",
|
||||
help="Do not daemonize", action="store_true", default=False)
|
||||
parser.add_option("-c", "--config", dest="config",
|
||||
@ -197,6 +197,11 @@ this should be an IP address",
|
||||
write_pidfile()
|
||||
|
||||
# Setup the logger
|
||||
try:
|
||||
# Try to make the logfile's directory if it doesn't exist
|
||||
os.makedirs(os.path.abspath(os.path.dirname(options.logfile)))
|
||||
except:
|
||||
pass
|
||||
deluge.log.setupLogger(level=options.loglevel, filename=options.logfile)
|
||||
from deluge.log import LOG as log
|
||||
|
||||
|
@ -100,7 +100,7 @@ def make_meta_file(path, url, piece_length, progress=dummy,
|
||||
if created_by:
|
||||
data['created by'] = created_by.encode("utf8")
|
||||
|
||||
if trackers:
|
||||
if trackers and (len(trackers[0]) > 1 or len(trackers) > 1):
|
||||
data['announce-list'] = trackers
|
||||
|
||||
data["encoding"] = "UTF-8"
|
||||
@ -175,11 +175,12 @@ def makeinfo(path, piece_length, progress, name = None,
|
||||
piece_count += 1
|
||||
done = 0
|
||||
sh = sha()
|
||||
progress(piece_count, num_pieces)
|
||||
progress(piece_count, num_pieces)
|
||||
h.close()
|
||||
if done > 0:
|
||||
pieces.append(sh.digest())
|
||||
|
||||
progress(piece_count, num_pieces)
|
||||
|
||||
if name is not None:
|
||||
assert isinstance(name, unicode)
|
||||
name = to_utf8(name)
|
||||
|
@ -87,7 +87,7 @@ class PluginManagerBase:
|
||||
def disable_plugins(self):
|
||||
# Disable all plugins that are enabled
|
||||
for key in self.plugins.keys():
|
||||
self.plugins[key].disable()
|
||||
self.disable_plugin(key)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.plugins[key]
|
||||
@ -131,15 +131,16 @@ class PluginManagerBase:
|
||||
egg.activate()
|
||||
for name in egg.get_entry_map(self.entry_name):
|
||||
entry_point = egg.get_entry_info(self.entry_name, name)
|
||||
cls = entry_point.load()
|
||||
try:
|
||||
cls = entry_point.load()
|
||||
instance = cls(plugin_name.replace("-", "_"))
|
||||
except Exception, e:
|
||||
log.error("Unable to instantiate plugin!")
|
||||
log.exception(e)
|
||||
continue
|
||||
instance.enable()
|
||||
if self.get_state() == component.COMPONENT_STATE.index("Started"):
|
||||
component.start(instance.plugin.get_component_name())
|
||||
if self._component_state == "Started":
|
||||
component.start([instance.plugin._component_name])
|
||||
plugin_name = plugin_name.replace("-", " ")
|
||||
self.plugins[plugin_name] = instance
|
||||
if plugin_name not in self.config["enabled_plugins"]:
|
||||
@ -152,6 +153,7 @@ class PluginManagerBase:
|
||||
"""Disables a plugin"""
|
||||
try:
|
||||
self.plugins[name].disable()
|
||||
component.deregister(self.plugins[name].plugin._component_name)
|
||||
del self.plugins[name]
|
||||
self.config["enabled_plugins"].remove(name)
|
||||
except KeyError:
|
||||
|
@ -1,8 +1,12 @@
|
||||
#
|
||||
# feeder/__init__.py
|
||||
# __init__.py
|
||||
#
|
||||
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
|
||||
#
|
||||
# Basic plugin template created by:
|
||||
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
||||
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
|
||||
#
|
||||
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
@ -18,9 +22,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
|
||||
@ -32,7 +36,6 @@
|
||||
# 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 deluge.plugins.init import PluginInitBase
|
||||
|
@ -1,7 +1,12 @@
|
||||
#
|
||||
# common.py
|
||||
#
|
||||
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
|
||||
#
|
||||
# Basic plugin template created by:
|
||||
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
||||
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
@ -31,10 +36,7 @@
|
||||
# this exception statement from your version. If you delete this exception
|
||||
# statement from all source files in the program, then also delete it here.
|
||||
#
|
||||
#
|
||||
|
||||
import pkg_resources
|
||||
import os.path
|
||||
|
||||
def get_resource(filename):
|
||||
return pkg_resources.resource_filename("example", os.path.join("data", filename))
|
||||
import pkg_resources, os
|
||||
return pkg_resources.resource_filename("autoadd", os.path.join("data", filename))
|
332
deluge/plugins/autoadd/autoadd/core.py
Normal file
332
deluge/plugins/autoadd/autoadd/core.py
Normal file
@ -0,0 +1,332 @@
|
||||
#
|
||||
# core.py
|
||||
#
|
||||
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
|
||||
#
|
||||
# Basic plugin template created by:
|
||||
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
||||
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2009 Damien Churchill <damoxc@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 deluge._libtorrent import lt
|
||||
import os
|
||||
from deluge.log import LOG as log
|
||||
from deluge.plugins.pluginbase import CorePluginBase
|
||||
import deluge.component as component
|
||||
import deluge.configmanager
|
||||
from deluge.core.rpcserver import export
|
||||
from twisted.internet.task import LoopingCall, deferLater
|
||||
from twisted.internet import reactor
|
||||
from deluge.event import DelugeEvent
|
||||
|
||||
DEFAULT_PREFS = {
|
||||
"watchdirs":{},
|
||||
"next_id":1
|
||||
}
|
||||
|
||||
OPTIONS_AVAILABLE = { #option: builtin
|
||||
"enabled":False,
|
||||
"path":False,
|
||||
"append_extension":False,
|
||||
"abspath":False,
|
||||
"download_location":True,
|
||||
"max_download_speed":True,
|
||||
"max_upload_speed":True,
|
||||
"max_connections":True,
|
||||
"max_upload_slots":True,
|
||||
"prioritize_first_last":True,
|
||||
"auto_managed":True,
|
||||
"stop_at_ratio":True,
|
||||
"stop_ratio":True,
|
||||
"remove_at_ratio":True,
|
||||
"move_completed":True,
|
||||
"move_completed_path":True,
|
||||
"label":False,
|
||||
"add_paused":True,
|
||||
"queue_to_top":False
|
||||
}
|
||||
|
||||
MAX_NUM_ATTEMPTS = 10
|
||||
|
||||
class AutoaddOptionsChangedEvent(DelugeEvent):
|
||||
"""Emitted when the options for the plugin are changed."""
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def CheckInput(cond, message):
|
||||
if not cond:
|
||||
raise Exception(message)
|
||||
|
||||
class Core(CorePluginBase):
|
||||
def enable(self):
|
||||
|
||||
#reduce typing, assigning some values to self...
|
||||
self.config = deluge.configmanager.ConfigManager("autoadd.conf", DEFAULT_PREFS)
|
||||
self.watchdirs = self.config["watchdirs"]
|
||||
self.core_cfg = deluge.configmanager.ConfigManager("core.conf")
|
||||
|
||||
# Dict of Filename:Attempts
|
||||
self.invalid_torrents = {}
|
||||
# Loopingcall timers for each enabled watchdir
|
||||
self.update_timers = {}
|
||||
# If core autoadd folder is enabled, move it to the plugin
|
||||
if self.core_cfg.config.get('autoadd_enable'):
|
||||
# Disable core autoadd
|
||||
self.core_cfg['autoadd_enable'] = False
|
||||
self.core_cfg.save()
|
||||
# Check if core autoadd folder is already added in plugin
|
||||
for watchdir in self.watchdirs:
|
||||
if os.path.abspath(self.core_cfg['autoadd_location']) == watchdir['abspath']:
|
||||
watchdir['enabled'] = True
|
||||
break
|
||||
else:
|
||||
# didn't find core watchdir, add it
|
||||
self.add({'path':self.core_cfg['autoadd_location'], 'enabled':True})
|
||||
deferLater(reactor, 5, self.enable_looping)
|
||||
|
||||
def enable_looping(self):
|
||||
#Enable all looping calls for enabled watchdirs here
|
||||
for watchdir_id, watchdir in self.watchdirs.iteritems():
|
||||
if watchdir['enabled']:
|
||||
self.enable_watchdir(watchdir_id)
|
||||
|
||||
def disable(self):
|
||||
#disable all running looping calls
|
||||
for loopingcall in self.update_timers.itervalues():
|
||||
loopingcall.stop()
|
||||
self.config.save()
|
||||
|
||||
def update(self):
|
||||
pass
|
||||
|
||||
@export()
|
||||
def set_options(self, watchdir_id, options):
|
||||
"""Update the options for a watch folder."""
|
||||
watchdir_id = str(watchdir_id)
|
||||
options = self._make_unicode(options)
|
||||
CheckInput(watchdir_id in self.watchdirs , _("Watch folder does not exist."))
|
||||
if options.has_key('path'):
|
||||
options['abspath'] = os.path.abspath(options['path'])
|
||||
CheckInput(os.path.isdir(options['abspath']), _("Path does not exist."))
|
||||
for w_id, w in self.watchdirs.iteritems():
|
||||
if options['abspath'] == w['abspath'] and watchdir_id != w_id:
|
||||
raise Exception("Path is already being watched.")
|
||||
for key in options.keys():
|
||||
if not key in OPTIONS_AVAILABLE:
|
||||
if not key in [key2+'_toggle' for key2 in OPTIONS_AVAILABLE.iterkeys()]:
|
||||
raise Exception("autoadd: Invalid options key:%s" % key)
|
||||
#disable the watch loop if it was active
|
||||
if watchdir_id in self.update_timers:
|
||||
self.disable_watchdir(watchdir_id)
|
||||
|
||||
self.watchdirs[watchdir_id].update(options)
|
||||
#re-enable watch loop if appropriate
|
||||
if self.watchdirs[watchdir_id]['enabled']:
|
||||
self.enable_watchdir(watchdir_id)
|
||||
self.config.save()
|
||||
component.get("EventManager").emit(AutoaddOptionsChangedEvent())
|
||||
|
||||
def load_torrent(self, filename):
|
||||
try:
|
||||
log.debug("Attempting to open %s for add.", filename)
|
||||
_file = open(filename, "rb")
|
||||
filedump = _file.read()
|
||||
if not filedump:
|
||||
raise RuntimeError, "Torrent is 0 bytes!"
|
||||
_file.close()
|
||||
except IOError, e:
|
||||
log.warning("Unable to open %s: %s", filename, e)
|
||||
raise e
|
||||
|
||||
# Get the info to see if any exceptions are raised
|
||||
info = lt.torrent_info(lt.bdecode(filedump))
|
||||
|
||||
return filedump
|
||||
|
||||
def update_watchdir(self, watchdir_id):
|
||||
"""Check the watch folder for new torrents to add."""
|
||||
watchdir_id = str(watchdir_id)
|
||||
watchdir = self.watchdirs[watchdir_id]
|
||||
if not watchdir['enabled']:
|
||||
# We shouldn't be updating because this watchdir is not enabled
|
||||
self.disable_watchdir(watchdir_id)
|
||||
return
|
||||
|
||||
if not os.path.isdir(watchdir["abspath"]):
|
||||
log.warning("Invalid AutoAdd folder: %s", watchdir["abspath"])
|
||||
self.disable_watchdir(watchdir_id)
|
||||
return
|
||||
|
||||
# Generate options dict for watchdir
|
||||
opts = {}
|
||||
if 'stop_at_ratio_toggle' in watchdir:
|
||||
watchdir['stop_ratio_toggle'] = watchdir['stop_at_ratio_toggle']
|
||||
# We default to True wher reading _toggle values, so a config
|
||||
# without them is valid, and applies all its settings.
|
||||
for option, value in watchdir.iteritems():
|
||||
if OPTIONS_AVAILABLE.get(option):
|
||||
if watchdir.get(option+'_toggle', True):
|
||||
opts[option] = value
|
||||
for filename in os.listdir(watchdir["abspath"]):
|
||||
if filename.split(".")[-1] == "torrent":
|
||||
try:
|
||||
filepath = os.path.join(watchdir["abspath"], filename)
|
||||
except UnicodeDecodeError, e:
|
||||
log.error("Unable to auto add torrent due to inproper filename encoding: %s", e)
|
||||
continue
|
||||
try:
|
||||
filedump = self.load_torrent(filepath)
|
||||
except (RuntimeError, Exception), e:
|
||||
# If the torrent is invalid, we keep track of it so that we
|
||||
# can try again on the next pass. This is because some
|
||||
# torrents may not be fully saved during the pass.
|
||||
log.debug("Torrent is invalid: %s", e)
|
||||
if filename in self.invalid_torrents:
|
||||
self.invalid_torrents[filename] += 1
|
||||
if self.invalid_torrents[filename] >= MAX_NUM_ATTEMPTS:
|
||||
os.rename(filepath, filepath + ".invalid")
|
||||
del self.invalid_torrents[filename]
|
||||
else:
|
||||
self.invalid_torrents[filename] = 1
|
||||
continue
|
||||
|
||||
# The torrent looks good, so lets add it to the session.
|
||||
torrent_id = component.get("TorrentManager").add(filedump=filedump, filename=filename, options=opts)
|
||||
# If the torrent added successfully, set the extra options.
|
||||
if torrent_id:
|
||||
if 'Label' in component.get("CorePluginManager").get_enabled_plugins():
|
||||
if watchdir.get('label_toggle', True) and watchdir.get('label'):
|
||||
label = component.get("CorePlugin.Label")
|
||||
if not watchdir['label'] in label.get_labels():
|
||||
label.add(watchdir['label'])
|
||||
label.set_torrent(torrent_id, watchdir['label'])
|
||||
if watchdir.get('queue_to_top_toggle', True) and 'queue_to_top' in watchdir:
|
||||
if watchdir['queue_to_top']:
|
||||
component.get("TorrentManager").queue_top(torrent_id)
|
||||
else:
|
||||
component.get("TorrentManager").queue_bottom(torrent_id)
|
||||
# Rename or delete the torrent once added to deluge.
|
||||
if watchdir.get('append_extension_toggle'):
|
||||
if not watchdir.get('append_extension'):
|
||||
watchdir['append_extension'] = ".added"
|
||||
os.rename(filepath, filepath + watchdir['append_extension'])
|
||||
else:
|
||||
os.remove(filepath)
|
||||
|
||||
def on_update_watchdir_error(self, failure, watchdir_id):
|
||||
"""Disables any watch folders with unhandled exceptions."""
|
||||
self.disable_watchdir(watchdir_id)
|
||||
log.error("Disabling '%s', error during update: %s" % (self.watchdirs[watchdir_id]["path"], failure))
|
||||
|
||||
@export
|
||||
def enable_watchdir(self, watchdir_id):
|
||||
watchdir_id = str(watchdir_id)
|
||||
# Enable the looping call
|
||||
if watchdir_id not in self.update_timers or not self.update_timers[watchdir_id].running:
|
||||
self.update_timers[watchdir_id] = LoopingCall(self.update_watchdir, watchdir_id)
|
||||
self.update_timers[watchdir_id].start(5).addErrback(self.on_update_watchdir_error, watchdir_id)
|
||||
# Update the config
|
||||
if not self.watchdirs[watchdir_id]['enabled']:
|
||||
self.watchdirs[watchdir_id]['enabled'] = True
|
||||
self.config.save()
|
||||
component.get("EventManager").emit(AutoaddOptionsChangedEvent())
|
||||
|
||||
@export
|
||||
def disable_watchdir(self, watchdir_id):
|
||||
watchdir_id = str(watchdir_id)
|
||||
# Disable the looping call
|
||||
if watchdir_id in self.update_timers:
|
||||
if self.update_timers[watchdir_id].running:
|
||||
self.update_timers[watchdir_id].stop()
|
||||
del self.update_timers[watchdir_id]
|
||||
# Update the config
|
||||
if self.watchdirs[watchdir_id]['enabled']:
|
||||
self.watchdirs[watchdir_id]['enabled'] = False
|
||||
self.config.save()
|
||||
component.get("EventManager").emit(AutoaddOptionsChangedEvent())
|
||||
|
||||
@export
|
||||
def set_config(self, config):
|
||||
"""Sets the config dictionary."""
|
||||
config = self._make_unicode(config)
|
||||
for key in config.keys():
|
||||
self.config[key] = config[key]
|
||||
self.config.save()
|
||||
component.get("EventManager").emit(AutoaddOptionsChangedEvent())
|
||||
|
||||
@export
|
||||
def get_config(self):
|
||||
"""Returns the config dictionary."""
|
||||
return self.config.config
|
||||
|
||||
@export()
|
||||
def get_watchdirs(self):
|
||||
return self.watchdirs.keys()
|
||||
|
||||
def _make_unicode(self, options):
|
||||
opts = {}
|
||||
for key in options:
|
||||
if isinstance(options[key], str):
|
||||
options[key] = unicode(options[key], "utf8")
|
||||
opts[key] = options[key]
|
||||
return opts
|
||||
|
||||
@export()
|
||||
def add(self, options={}):
|
||||
"""Add a watch folder."""
|
||||
options = self._make_unicode(options)
|
||||
abswatchdir = os.path.abspath(options['path'])
|
||||
CheckInput(os.path.isdir(abswatchdir) , _("Path does not exist."))
|
||||
CheckInput(os.access(abswatchdir, os.R_OK|os.W_OK), "You must have read and write access to watch folder.")
|
||||
if abswatchdir in [wd['abspath'] for wd in self.watchdirs.itervalues()]:
|
||||
raise Exception("Path is already being watched.")
|
||||
options.setdefault('enabled', False)
|
||||
options['abspath'] = abswatchdir
|
||||
watchdir_id = self.config['next_id']
|
||||
self.watchdirs[str(watchdir_id)] = options
|
||||
if options.get('enabled'):
|
||||
self.enable_watchdir(watchdir_id)
|
||||
self.config['next_id'] = watchdir_id + 1
|
||||
self.config.save()
|
||||
component.get("EventManager").emit(AutoaddOptionsChangedEvent())
|
||||
return watchdir_id
|
||||
|
||||
@export
|
||||
def remove(self, watchdir_id):
|
||||
"""Remove a watch folder."""
|
||||
watchdir_id = str(watchdir_id)
|
||||
CheckInput(watchdir_id in self.watchdirs, "Unknown Watchdir: %s" % self.watchdirs)
|
||||
if self.watchdirs[watchdir_id]['enabled']:
|
||||
self.disable_watchdir(watchdir_id)
|
||||
del self.watchdirs[watchdir_id]
|
||||
self.config.save()
|
||||
component.get("EventManager").emit(AutoaddOptionsChangedEvent())
|
@ -1,9 +1,9 @@
|
||||
/*
|
||||
Script: example.js
|
||||
The client-side javascript code for the Example plugin.
|
||||
Script: autoadd.js
|
||||
The client-side javascript code for the AutoAdd plugin.
|
||||
|
||||
Copyright:
|
||||
(C) Damien Churchill 2009 <damoxc@gmail.com>
|
||||
(C) GazpachoKing 2009 <damoxc@gmail.com>
|
||||
This program is free software; you can 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, or (at your option)
|
||||
@ -31,21 +31,20 @@ Copyright:
|
||||
statement from all source files in the program, then also delete it here.
|
||||
*/
|
||||
|
||||
ExamplePlugin = Ext.extend(Deluge.Plugin, {
|
||||
AutoAddPlugin = Ext.extend(Deluge.Plugin, {
|
||||
constructor: function(config) {
|
||||
config = Ext.apply({
|
||||
name: "Example"
|
||||
name: "AutoAdd"
|
||||
}, config);
|
||||
ExamplePlugin.superclass.constructor.call(this, config);
|
||||
AutoAddPlugin.superclass.constructor.call(this, config);
|
||||
},
|
||||
|
||||
onDisable: function() {
|
||||
Deluge.Preferences.removePage(this.prefsPage);
|
||||
|
||||
},
|
||||
|
||||
onEnable: function() {
|
||||
this.prefsPage = new ExamplePreferencesPanel();
|
||||
this.prefsPage = Deluge.Preferences.addPage(this.prefsPage);
|
||||
|
||||
}
|
||||
});
|
||||
new ExamplePlugin();
|
||||
new AutoAddPlugin();
|
1126
deluge/plugins/autoadd/autoadd/data/autoadd_options.glade
Normal file
1126
deluge/plugins/autoadd/autoadd/data/autoadd_options.glade
Normal file
File diff suppressed because it is too large
Load Diff
116
deluge/plugins/autoadd/autoadd/data/config.glade
Normal file
116
deluge/plugins/autoadd/autoadd/data/config.glade
Normal file
@ -0,0 +1,116 @@
|
||||
<?xml version="1.0"?>
|
||||
<glade-interface>
|
||||
<!-- interface-requires gtk+ 2.16 -->
|
||||
<!-- interface-naming-policy toplevel-contextual -->
|
||||
<widget class="GtkWindow" id="prefs_window">
|
||||
<child>
|
||||
<widget class="GtkHBox" id="hbox9">
|
||||
<property name="visible">True</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<widget class="GtkAlignment" id="prefs_box_1">
|
||||
<property name="visible">True</property>
|
||||
<child>
|
||||
<widget class="GtkVBox" id="prefs_box">
|
||||
<property name="width_request">340</property>
|
||||
<property name="height_request">390</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="border_width">3</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<signal name="parent_set" handler="on_parent_set"/>
|
||||
<child>
|
||||
<widget class="GtkFrame" id="frame1">
|
||||
<property name="visible">True</property>
|
||||
<property name="label_xalign">0</property>
|
||||
<property name="shadow_type">none</property>
|
||||
<child>
|
||||
<widget class="GtkVBox" id="watchdirs_vbox">
|
||||
<property name="visible">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="homogeneous">True</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</widget>
|
||||
</child>
|
||||
<child>
|
||||
<widget class="GtkLabel" id="label1">
|
||||
<property name="visible">True</property>
|
||||
<property name="label" translatable="yes"><b>Watch Folders:</b></property>
|
||||
<property name="use_markup">True</property>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="type">label_item</property>
|
||||
</packing>
|
||||
</child>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<widget class="GtkHButtonBox" id="hbuttonbox1">
|
||||
<property name="visible">True</property>
|
||||
<child>
|
||||
<widget class="GtkButton" id="add_button">
|
||||
<property name="label" translatable="no">gtk-add</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="can_default">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<signal name="clicked" handler="on_add_button_clicked"/>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<widget class="GtkButton" id="remove_button">
|
||||
<property name="label" translatable="no">gtk-remove</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="can_default">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<signal name="clicked" handler="on_remove_button_clicked"/>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<widget class="GtkButton" id="edit_button">
|
||||
<property name="label" translatable="yes">gtk-edit</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="can_default">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<signal name="clicked" handler="on_edit_button_clicked"/>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</widget>
|
||||
</child>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</widget>
|
||||
</child>
|
||||
</widget>
|
||||
</glade-interface>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user