Compare commits
1151 Commits
deluge-1.3
...
deluge-2.0
Author | SHA1 | Date | |
---|---|---|---|
4dd6308db9 | |||
289730a3e3 | |||
1d34d5f6a5 | |||
64e1ab481b | |||
c92b3debb7 | |||
68c41e2915 | |||
31929c7004 | |||
937419bfcd | |||
9b3ff8f1b8 | |||
a44d86f285 | |||
400bd86749 | |||
31ec8830f4 | |||
2a3eb7b70c | |||
e183e2ff04 | |||
7598312969 | |||
e62f79d9ae | |||
662849c0c2 | |||
10251e460b | |||
6f9d69f9f3 | |||
091b42d317 | |||
463ac0c07e | |||
836178a0da | |||
1d9b550e35 | |||
93d4e345b8 | |||
a7cda7011b | |||
763af17e71 | |||
83df1159c0 | |||
a7c5b9f568 | |||
0da88fc74b | |||
b82b313e32 | |||
36d8c5517f | |||
b08573831e | |||
41e3b01285 | |||
cdfb337bae | |||
31ecaacbc8 | |||
5bcb104a01 | |||
6c75201b2e | |||
54491a9eaf | |||
c11262082e | |||
8c106ce8c4 | |||
b4f5e78a77 | |||
8735fe14a8 | |||
01c501b172 | |||
1f12bab923 | |||
810495264f | |||
754035c722 | |||
fbdda1b3a5 | |||
a992d8685f | |||
763f5de904 | |||
4b99a39779 | |||
5e19fd0122 | |||
823a1f0fc4 | |||
f3722ebc4f | |||
fbeea9159e | |||
0f67dc168b | |||
5cd86aa5bc | |||
8cb55983bb | |||
ffcfc060e9 | |||
6313ff19b3 | |||
61bd8aa154 | |||
637578375c | |||
18bcdd09d3 | |||
cc5ef89139 | |||
1a9506f832 | |||
e7d06ee132 | |||
7647f848e4 | |||
eec820774b | |||
8cbdaffedb | |||
e6267d9411 | |||
8f34e2abdb | |||
d3f0e00356 | |||
7c2725acdc | |||
f47c9186bf | |||
c9b77cbe94 | |||
725230dc81 | |||
73102f1362 | |||
ca1a5d33f4 | |||
3bf023e4df | |||
c556613b9f | |||
6c07b7378c | |||
4520fdd58b | |||
1ac62fce01 | |||
c2d301bf52 | |||
8658be3b05 | |||
2a8b8e93da | |||
ac6785eb1a | |||
848b14605e | |||
2a0048afbb | |||
cc943cea4a | |||
2f3623d430 | |||
fe44a5ba69 | |||
d3a0b9d877 | |||
25efa5437b | |||
67b40a8442 | |||
bcac44bcb4 | |||
58cb9e1c22 | |||
ce99b5f688 | |||
ce76c278ed | |||
bb5dbecbf2 | |||
eed46994da | |||
a5df53249a | |||
331cef16ef | |||
e79c695732 | |||
b4d7e42973 | |||
71183f6c19 | |||
0b5b585992 | |||
7492d48029 | |||
b4903b763e | |||
3d76ab1832 | |||
48cbf0d9b0 | |||
6b9ae264ff | |||
e90e608fdd | |||
6fe350fa52 | |||
c426f998e2 | |||
2187cef14f | |||
6b5cf3396d | |||
4f59a48f57 | |||
d48d3c9c2f | |||
4579886bb5 | |||
bd979da949 | |||
d5e340354e | |||
19bbf5ac8f | |||
b8deea5c76 | |||
88223fc058 | |||
09e43f3e41 | |||
751bc317ea | |||
60f196ff93 | |||
294fc48bd1 | |||
d3d07f7f11 | |||
ca272bb36a | |||
cd41089e49 | |||
b1cf5b9c40 | |||
c37cfdfa2f | |||
84bc2e78bc | |||
0676d7e2dc | |||
dd8dac0574 | |||
d6d1cc5f45 | |||
f016160c62 | |||
7119e0d95f | |||
ffb902ba06 | |||
dc14453f34 | |||
f83e772030 | |||
0c1dd44cd6 | |||
5b2be2d190 | |||
9d28aa9521 | |||
8298a93dcc | |||
a78731c8cd | |||
15492028fc | |||
db5a4f84f6 | |||
4b75ec5b55 | |||
1daae0135d | |||
dd511df194 | |||
e43e4e2ee0 | |||
b67bae31ba | |||
5cb85472c6 | |||
1c865ebeb9 | |||
a18bdcf7cd | |||
0a36baa7d7 | |||
47958f708f | |||
d030850638 | |||
9360378ae9 | |||
a3268dd403 | |||
b34e38df57 | |||
5188750332 | |||
7275cdd3d9 | |||
f6c201f02f | |||
1ec51cec18 | |||
2531271fe2 | |||
9055019cc2 | |||
9829bec390 | |||
183c47f810 | |||
129c09c5a7 | |||
c8384bf304 | |||
5e1caf3746 | |||
532b409a54 | |||
7dd4645a7b | |||
0bf1379cc5 | |||
6f98e1fddb | |||
d3f6616d5d | |||
8cdf914f5d | |||
6c6292135f | |||
75c4135d6e | |||
5927f2fa30 | |||
bd13457f33 | |||
8de9843e7e | |||
a73fb338f1 | |||
ae8751461a | |||
9c0c7b060c | |||
da5140e615 | |||
fa32d4e3f4 | |||
52541df3f9 | |||
39d75ee7d5 | |||
246a8409bf | |||
c554bf9edd | |||
ef522ba292 | |||
3602fb76c5 | |||
8dfc405c3e | |||
567f4e5c3d | |||
57b1820fd7 | |||
6a54c71c94 | |||
2b244f0628 | |||
be23d00042 | |||
d9a2597617 | |||
33e5cad75a | |||
8c9a89bcd2 | |||
0c93d20980 | |||
93a0040b68 | |||
e4a4f0eb4a | |||
78137540f2 | |||
76babd951d | |||
a66bd5e847 | |||
dcd3bc10e1 | |||
08f5841522 | |||
7a55a2e6ce | |||
ac79938c20 | |||
1da24fbeaa | |||
a20b39325e | |||
f7a4951e0d | |||
9abfc5b250 | |||
a41c950b11 | |||
718bf57b5d | |||
11e3a66484 | |||
9a322ed67f | |||
4e1573cb39 | |||
28e36c7edc | |||
bbdf710b52 | |||
3634f457b4 | |||
a956f0a5d7 | |||
265f9f295e | |||
6888c6ef60 | |||
50b84c3e91 | |||
9152d322ac | |||
18091c5ad6 | |||
4a58e339cb | |||
31dd1be090 | |||
3cc97accfc | |||
555717b9a0 | |||
cf58aa780a | |||
935777fb49 | |||
0dbea0ed01 | |||
84425d7786 | |||
3920a93cba | |||
4735a6c49a | |||
a8549ef882 | |||
0df1255ae5 | |||
f6d87c7a7e | |||
1f59b4d2ba | |||
1df173d684 | |||
b2f78786a5 | |||
314b6138d7 | |||
1bc3c293fa | |||
bbf0666539 | |||
2ed60de628 | |||
76546ec176 | |||
0a9a1db942 | |||
d589823f4a | |||
33decd1780 | |||
6269076c7e | |||
506a98aee2 | |||
8d1e4297ec | |||
8678121210 | |||
24d801d18a | |||
2e647c6b41 | |||
a47b2bc715 | |||
e1a3a9e077 | |||
3488a761b8 | |||
822ddc2182 | |||
f5f1f11f61 | |||
864785752f | |||
60e534f59b | |||
c0f76bef1b | |||
920e765790 | |||
cc99279ad1 | |||
48dd049cbd | |||
09c830c6ae | |||
39a896e59e | |||
ca6c647bd2 | |||
d2dc62f0b3 | |||
4983110d50 | |||
00bf1d31a2 | |||
2092a0d090 | |||
d3e70b7f7f | |||
d86168cb41 | |||
5a096768e0 | |||
c2f97356c6 | |||
b5cfbbcdec | |||
6326902287 | |||
a9af9cabb4 | |||
546aa58482 | |||
be79c586da | |||
819377b0bb | |||
b37965de3d | |||
d7391611dd | |||
fa1a1eb939 | |||
5a33e66c2c | |||
3bc25d44ee | |||
ba7e36c719 | |||
e6e0eefaa4 | |||
a750999e0e | |||
40a6b11a1b | |||
471757d6c6 | |||
2373eda462 | |||
a0a18e1036 | |||
04ed96d121 | |||
05758245a1 | |||
a28e40ea35 | |||
14bfa24195 | |||
8a261b26e8 | |||
a2c347a79c | |||
415bc22dd9 | |||
6edd159626 | |||
1a1518ac1d | |||
508dec4858 | |||
80b88bf047 | |||
249e331ae9 | |||
47ba11be1b | |||
163870afd9 | |||
117d29ae72 | |||
8dc5b07818 | |||
acb77213e1 | |||
e68358661a | |||
d9789504ff | |||
98101ea411 | |||
f33a6a68e4 | |||
03fefc279b | |||
e85be7cccb | |||
b8f2a1da1a | |||
b1ce567819 | |||
68db1d4c13 | |||
5796e025e6 | |||
8819ec0575 | |||
a24c679510 | |||
c2b4ccdc77 | |||
8d07a697d3 | |||
459c4aebb9 | |||
010fd165c6 | |||
27f0e86afd | |||
a436eb8aa6 | |||
3f5099bd05 | |||
2625bbc7fd | |||
0f18463df0 | |||
6b8428e262 | |||
a8a24bf0d9 | |||
c30a86e52a | |||
591f9a19e5 | |||
c62547d401 | |||
16f62bbcfe | |||
412d0e7be9 | |||
98c9aaf600 | |||
f5c8968aa6 | |||
149cbae4dc | |||
b689fe1d98 | |||
a6c1bc1d4a | |||
ab6dc2d11f | |||
ced1475233 | |||
1391f20658 | |||
b1439274c6 | |||
6422f11971 | |||
8e7432e71c | |||
5dc6dbf216 | |||
6cb1fd76cc | |||
934a0f6495 | |||
266127bb69 | |||
acecd6d522 | |||
2cdcae8d31 | |||
cb2212d2f3 | |||
9fd527f465 | |||
828b3204b8 | |||
8fdfdc2b25 | |||
bfae766f8a | |||
569dd0c585 | |||
211c27aaae | |||
e2608a0ac9 | |||
7625812c8c | |||
beb35c5c35 | |||
14eb3e51b0 | |||
d62da02bae | |||
61dbd349ab | |||
f102e988c9 | |||
78df634fed | |||
e6e677e7d0 | |||
edca36fa73 | |||
758f4ef920 | |||
30ee7fb170 | |||
8010f2fcc1 | |||
d98eb06f69 | |||
7f88f59272 | |||
f6127e1747 | |||
9ecc9ab7ad | |||
17d12fbaf2 | |||
068ba7bc6f | |||
713f7eff7a | |||
74181469bc | |||
b396b11611 | |||
8503687136 | |||
f0051ee81f | |||
31222a5ab6 | |||
752e5a7a8f | |||
2ecb54c4f7 | |||
7ae912114b | |||
e33d834cc9 | |||
9c0a450a47 | |||
debae00246 | |||
a1949bc020 | |||
968abf9d54 | |||
9053280e14 | |||
6d2e88eeee | |||
f1ddd236ce | |||
be5a0b3dc5 | |||
614b002d8b | |||
a494471ed4 | |||
31ff64b537 | |||
ba75ae4ccc | |||
fd28bf8619 | |||
ae6af18f0d | |||
b47dc73d30 | |||
1696fd1103 | |||
b5c63c4d58 | |||
330019bb3d | |||
4bbf9e2ea6 | |||
6e6f6313a8 | |||
af19e3bc62 | |||
40e4fb9b8e | |||
62c7209558 | |||
f897f03227 | |||
38210ae11e | |||
0ccf0730ea | |||
abc82c1439 | |||
e9239be691 | |||
8452b63d19 | |||
6f77703e29 | |||
2b01ba43cb | |||
eceaa0ae4f | |||
ae9eb15d5c | |||
2793e1ec53 | |||
ec27028f1b | |||
ba60ae09d5 | |||
aa0f41ac17 | |||
4a7876f203 | |||
101ad99c14 | |||
0d3ba7541e | |||
c8718ad643 | |||
da868347cf | |||
f4fab86767 | |||
517addb9f9 | |||
8c1ef7d6af | |||
c87245320d | |||
983c9dad99 | |||
f299be0eb9 | |||
a01f45cc7a | |||
3b1ac4e81f | |||
bebdec9ebb | |||
3a91f87679 | |||
be49fd6a40 | |||
c88ba97531 | |||
aa726f723b | |||
f8651b63c8 | |||
c020d71327 | |||
527d5541d7 | |||
57df1bb7c4 | |||
5ad9ff7333 | |||
7d7e3fad1e | |||
e8eb7a33f0 | |||
8da618a4f2 | |||
1c3d8c214c | |||
bbde86cfb8 | |||
5e36722047 | |||
ee35fe1cad | |||
a83fd1d597 | |||
4dbbb4d676 | |||
6c8e2b48e3 | |||
55892061f5 | |||
ae8ea820ef | |||
d06a0e4f40 | |||
9bfb565354 | |||
132a8f9f0c | |||
75b9fd5cb4 | |||
774e614f7b | |||
9e43956e1b | |||
9d16b50075 | |||
04b8949178 | |||
b69163b57d | |||
006624f568 | |||
4acf548436 | |||
d9193fcc4f | |||
a779a4a7ea | |||
2a3d8ae156 | |||
07d4aff13b | |||
838f9331be | |||
3c3f93db3e | |||
d98231a713 | |||
a50c83c284 | |||
05f30b58c1 | |||
a932767545 | |||
c29d3bb930 | |||
b4a73cabf3 | |||
16bbedaf2b | |||
b2eb5aeb8c | |||
3dcfa5cfd8 | |||
307ffe734a | |||
ead734cbf0 | |||
46ab11961e | |||
58adbe94b9 | |||
7227c97cac | |||
4fcfb677a4 | |||
808ff02130 | |||
08a0a2de99 | |||
fd56ccaabf | |||
cebddf9c79 | |||
e9b602d85f | |||
5b2d37954c | |||
fcc13f454b | |||
15ef668fef | |||
bf145c0715 | |||
192f3d88e5 | |||
d9cf3a8c08 | |||
a41b1357b5 | |||
c3c21dae72 | |||
4daa7e2470 | |||
b301051cdd | |||
456f660878 | |||
f7ce07c68f | |||
9eb85cb6eb | |||
40fd945f70 | |||
78944f47f3 | |||
acb747bfd5 | |||
0c1055511d | |||
f0c327a024 | |||
b81159f295 | |||
ca86aa5714 | |||
fc7fa94319 | |||
c6ee8cf39d | |||
bd7bbc4e33 | |||
312a57aa50 | |||
f87ed6d5a6 | |||
4234311050 | |||
a47da57c0d | |||
13528fe7f8 | |||
99358dcbb0 | |||
16cc8f6eea | |||
a384cd70b3 | |||
0e00aa479b | |||
807bc095b4 | |||
5a81ab3c35 | |||
bad228645c | |||
e016b2106f | |||
f63f247ac5 | |||
0228af6b50 | |||
90fb40b741 | |||
367631c9aa | |||
b36d62be9b | |||
b4cc1d4358 | |||
39ad5a3596 | |||
dbad4684db | |||
12d0e9574b | |||
dd50b7bea1 | |||
4dc4049851 | |||
27a6e398ee | |||
7035b1f166 | |||
a701fddbe8 | |||
b512a664c6 | |||
5bffa3757d | |||
8b6d6e3836 | |||
37b9277c0e | |||
cf891125e6 | |||
f75ec9d484 | |||
9a1ae06033 | |||
55f456d851 | |||
c346687510 | |||
08ee3d8f69 | |||
795f633bc4 | |||
b6596a27bc | |||
b7fd2d1bf1 | |||
0f625943c0 | |||
420447e386 | |||
a79520e3ee | |||
8ae26c368e | |||
981ad6d7d2 | |||
3b5e70580e | |||
71f9ef6499 | |||
7dd54b4b34 | |||
c64ed6adc5 | |||
a82c753ac0 | |||
96b5f617f2 | |||
842734c4e4 | |||
095f4ff20a | |||
ed0b017fe1 | |||
ce9b540b97 | |||
5112ed48d1 | |||
dfa8834db8 | |||
5bc63fa910 | |||
24c945f139 | |||
2542ad9234 | |||
acb4ab44d2 | |||
16fbf27b90 | |||
3397c2487b | |||
66e8b34a54 | |||
59f9d4e5cc | |||
221dea1f1a | |||
4420aae092 | |||
ddc0957e3e | |||
2f71ef4264 | |||
bc56b749ee | |||
34c95a08a3 | |||
9ae19e173f | |||
6672aaba1b | |||
0712fc9dee | |||
07dc9005f3 | |||
274a76ab3b | |||
777993f74a | |||
d1037ae213 | |||
15e9f5f218 | |||
4aab110aaf | |||
8933ac3123 | |||
2e896b520e | |||
16d27b9657 | |||
d3e8afdda1 | |||
b86ba13376 | |||
f736576436 | |||
9d1715405f | |||
ee0d757b0e | |||
32c95fac1e | |||
df3214168c | |||
9e9261e6f8 | |||
087e94f6a1 | |||
abe0031c2b | |||
13db148a11 | |||
84c5078667 | |||
cebdc89b18 | |||
87e767d4c1 | |||
ce406674ec | |||
ac5f9a2828 | |||
6d55c44983 | |||
1557d0da1f | |||
2f785216f6 | |||
8f1730591b | |||
9ec44894d4 | |||
bb981127db | |||
a96aeed706 | |||
f14de6553a | |||
b521b3065b | |||
ea438609bf | |||
0ba51d0e51 | |||
53370e4639 | |||
c70c8ea45d | |||
937b53b355 | |||
27cd89c4ad | |||
c4dbf017a5 | |||
ec74f9aae3 | |||
cfd955a605 | |||
feed806983 | |||
c66637116b | |||
4d4c6404b1 | |||
042ddd2891 | |||
af24542856 | |||
6dc393ed23 | |||
c13eade81c | |||
eb639c3722 | |||
dc514d308c | |||
67b5cde128 | |||
ef98d19ed4 | |||
94a7b2ebf1 | |||
e0443943b5 | |||
dd78a75ca8 | |||
82712c80e1 | |||
a710bcaed4 | |||
3a7c182f83 | |||
d42778afa3 | |||
724025092a | |||
bd43f3c464 | |||
8464a938b2 | |||
b8fad45eaa | |||
b08e90ac2a | |||
13a379ef6c | |||
09e24df4bb | |||
019f2a0619 | |||
2fb874d486 | |||
85b4ceec30 | |||
b0599313bc | |||
974f48380f | |||
b3865d0a7f | |||
79c9dd3076 | |||
edb0c2e71d | |||
1c58dce3c1 | |||
445f3c0123 | |||
eb15c96403 | |||
71f411e458 | |||
856a6cd1ab | |||
99f2dbd178 | |||
0e4747bf22 | |||
81637f4572 | |||
25f086fa85 | |||
6d57a29f1d | |||
9b3f5783d5 | |||
b3492b07a1 | |||
28def22625 | |||
427fe23bdc | |||
da5c5d4b84 | |||
438cbd2238 | |||
2d59b62317 | |||
19f32b1446 | |||
9b812a4eec | |||
e383187796 | |||
6151050ad4 | |||
9a3bf35cdf | |||
6391970fad | |||
0ba0e013b5 | |||
552c898998 | |||
bc5b4d902f | |||
6a8e3f1c49 | |||
81ca9952e9 | |||
74618d5a65 | |||
0c110c2408 | |||
1ac997e7d7 | |||
d4692bef42 | |||
77fc53afc0 | |||
3b676eca40 | |||
ce3ce2c035 | |||
c8735b5cab | |||
cc5f2ffe18 | |||
89b79c76a3 | |||
837c39fdda | |||
110026edbe | |||
3b8ebf68a6 | |||
ffd344d0b5 | |||
9d29ca7b29 | |||
38906468c1 | |||
95d7caf3ac | |||
4044f52f77 | |||
a7bd953169 | |||
8922717ff2 | |||
117d50b728 | |||
04af8965bc | |||
d6f5e5b4ec | |||
1f3a7bf44c | |||
2e62ced811 | |||
95819c79e5 | |||
5ad21303c6 | |||
922e64a07e | |||
30d70d2b9b | |||
a06b350858 | |||
06f025f4bd | |||
d362a6ceba | |||
138b8ae314 | |||
6f3bc5620f | |||
f2249d5803 | |||
f26de83509 | |||
f6826a4f48 | |||
dd3f78bd36 | |||
63d0d0c69b | |||
1be59bb116 | |||
751345fc28 | |||
12ea65d188 | |||
e950cca059 | |||
a063095dad | |||
39978d5ade | |||
9fa8748432 | |||
18b27d4b49 | |||
f41f6ad46a | |||
bb9a8509c8 | |||
6694ac7a58 | |||
81d22eb730 | |||
47a9b18b89 | |||
292929ba59 | |||
cbcf413ffd | |||
4d8b34209b | |||
98a8be7131 | |||
2e68e0181c | |||
e6773dfce1 | |||
f56be66556 | |||
67a4fd49e9 | |||
e992ac3eab | |||
d05352db65 | |||
b1e0dd66eb | |||
897c2f981f | |||
91801e1632 | |||
fa20e49a93 | |||
4432e6e6e3 | |||
c225c045cb | |||
e552c21f66 | |||
89d04a393b | |||
f1730dc4d4 | |||
fb5005e3f6 | |||
51b5b23f76 | |||
78e966946f | |||
936bd925d9 | |||
43e3fe2a1a | |||
6ed3136c8e | |||
8195421c99 | |||
342da12d0c | |||
5296fc7d4c | |||
233e814547 | |||
03325c5f48 | |||
1a6742b1e2 | |||
154688a3e2 | |||
fe12552590 | |||
e63c33c496 | |||
105cb52cb0 | |||
3e0ea26e5f | |||
e44cac0eaa | |||
86a1b801f5 | |||
b3870ad6dd | |||
67ff83360f | |||
b2a16a0240 | |||
e17c035521 | |||
249398489e | |||
d44f59a0e7 | |||
6c99204828 | |||
1794f09b21 | |||
b08a4679de | |||
bfc221fc18 | |||
5ad3a1666c | |||
49d5ed6bde | |||
4b9209674e | |||
b9a688013f | |||
387b746fae | |||
796109649d | |||
d258794517 | |||
98f80c0eb6 | |||
d18becc861 | |||
0503db85ea | |||
bcb636dda4 | |||
5bc304470c | |||
42e1e2fd20 | |||
bb0746c3e8 | |||
19799d74b4 | |||
0d560bcd6f | |||
69b79756f2 | |||
fd248eb1fd | |||
45ccd3b84a | |||
298b85c368 | |||
67add964de | |||
e81a279dc2 | |||
280781ded9 | |||
f30a2858ce | |||
32b41fabd6 | |||
a0f9689664 | |||
08843ccad5 | |||
e0bb8869aa | |||
255af3c485 | |||
f35145b0a6 | |||
f2d560351e | |||
62da60a0e4 | |||
d9c1a56d44 | |||
84f278dbcc | |||
356f298e9c | |||
5fb01dacc0 | |||
8d541ad419 | |||
426eea154e | |||
ccc047848a | |||
11d8332e43 | |||
2193240c66 | |||
e43c532e63 | |||
930addb389 | |||
fab1e8412d | |||
1cce30393b | |||
510c81776f | |||
e7096d9509 | |||
5ae242472f | |||
ee75786e40 | |||
87473f2cde | |||
b2f349c05d | |||
9ac0d62149 | |||
db46a97263 | |||
4c2f9a1a0a | |||
64e38eac20 | |||
499a58f50d | |||
5f0f7204a8 | |||
62f6683730 | |||
60d96c6f20 | |||
d1efe5f1b4 | |||
62421080ef | |||
9e4ea0a671 | |||
b11468c19b | |||
40a5722987 | |||
956ea10a00 | |||
543fcf722c | |||
7f52472e9e | |||
5619991f2a | |||
2b04955128 | |||
e83d540fe4 | |||
4e5d88da82 | |||
10816cb8f4 | |||
5f8eda9204 | |||
b0c561dbbc | |||
1173f1c714 | |||
3da5cd9816 | |||
d9d8762c8e | |||
e16ee523a5 | |||
3db7bcbfc7 | |||
e1a3a431f0 | |||
9a3316f950 | |||
1789e8d03c | |||
837322478b | |||
ce2516ab2c | |||
962bfc3d2c | |||
4ff0fb19ee | |||
7a4006439b | |||
c015c3a57d | |||
8a9e732f95 | |||
0b3c408e64 | |||
d3a61bbda4 | |||
c523958bf6 | |||
06003b3650 | |||
1e0005f572 | |||
4a071ecba1 | |||
77eb1a5f82 | |||
ac8c928a5b | |||
23f64a5440 | |||
98ca371b15 | |||
f8737777b1 | |||
e198ea14e4 | |||
553f35eae5 | |||
376a23e6fd | |||
077f35ec5c | |||
00ab9ff499 | |||
20302021c4 | |||
9e5455793b | |||
7f6a1db89a | |||
cdcab320fb | |||
ea22bb0b10 | |||
554f34a261 | |||
ad2b13eb2c | |||
ce636ccd57 | |||
053700342a | |||
cd7805bfda | |||
79869faa53 | |||
b77f8929d6 | |||
e1d8025309 | |||
183a97785b | |||
f748660cac | |||
cea6c817df | |||
b9ff47e10f | |||
14746bf94d | |||
87f871f40a | |||
9f3ac37f25 | |||
417a9f6e63 | |||
ba6389bcac | |||
9bca1a72b1 | |||
e688b45448 | |||
4c54cfedb9 | |||
88039a0eda | |||
9a54beef78 | |||
b0a0574ae0 | |||
db64745862 | |||
0353a388b3 | |||
b41ebe1b89 | |||
1952357f35 | |||
6c8529b3ba | |||
78ea5c9bd3 | |||
b35875e300 | |||
ad498c6e42 | |||
d1b3aa54ad | |||
d0346a104f | |||
5f888faceb | |||
f6f3a8e084 | |||
5dcc935852 | |||
eba7c2bf17 | |||
00fa074452 | |||
5d46d2aee5 | |||
007dd67ea1 | |||
ff3c3f7148 | |||
68c04acf50 | |||
44676f282a | |||
182ec0cd97 | |||
6f0b1fd7f2 | |||
ba3a093746 | |||
b7e7a4bc49 | |||
ac18ecd1f0 | |||
2f6283ea39 | |||
b30499c6ac | |||
8c12c47d3e | |||
356808b02c | |||
1f800bf49a | |||
d1b4523733 | |||
c00391a852 | |||
5841521133 | |||
7e2411289d | |||
2fa8ca6753 | |||
1c15df8e00 | |||
f282487806 | |||
078ed6ba71 | |||
67ea05921c | |||
0f36a65aaf | |||
90d23ce582 | |||
860457ff48 | |||
e52018bfcd | |||
9bd11ab204 | |||
c164013725 | |||
b9a8bf2409 | |||
4c3d068f0c | |||
c9e4d286c3 | |||
e43146a4ac | |||
1c2eb0c737 | |||
b0d77a4f20 | |||
20635773b3 | |||
f17634ea63 | |||
16f617d240 | |||
1c7676bfe5 | |||
63fa5bf85b | |||
6cefb49f28 | |||
eeed72a977 | |||
49e10ea0cf | |||
26e45dcbc8 | |||
b7bc1fdb1d | |||
3b00a7de59 | |||
14ec9464aa | |||
3d64f0d8da | |||
87e3a5f515 | |||
75e9ff57de | |||
b180d2a900 | |||
1822c2bde9 | |||
40e6777c48 | |||
f88b24d507 | |||
593452ed63 | |||
4197e129fe | |||
0360cbe0b8 | |||
d2f41fe7e5 | |||
0a2e9a5324 | |||
3c302088f6 | |||
3b6bad2f13 | |||
6fd4b298f3 | |||
2beec764c9 | |||
e5760ee341 | |||
45940b9064 | |||
c97f809bdc | |||
ae6837c88c | |||
a827cf6c7a | |||
f52e3c4aa0 | |||
8f7e307f33 | |||
f1f6f137c3 | |||
ff7ff8eac7 | |||
4cb2bcae25 | |||
df95222849 | |||
463fd3ac04 | |||
eb37c91866 | |||
7cd210a59b | |||
eee27868a8 | |||
e5dec3f020 | |||
def1127c78 | |||
847f2c2ebd | |||
fb49aa02a8 | |||
f8dc66b773 | |||
c17b466bae | |||
d9cdff9525 | |||
915db80a55 | |||
350d4d7260 | |||
4b92912577 | |||
a794223d96 | |||
5811d372f9 | |||
0b2f2f2c8a | |||
64022d7bc7 | |||
bf715d90fd | |||
fdbd9e6687 | |||
0a0383d075 | |||
5b1bed5a48 | |||
7ed33192ec | |||
c82ba44be8 | |||
729daf331c | |||
db1835d942 | |||
7d4a316733 | |||
da8629db97 | |||
df573c66c6 | |||
29f61b58fb | |||
15ce2b71f9 | |||
116ccc21fd | |||
dee33745c8 | |||
8586cda4e0 | |||
db9b5580d7 | |||
10aebd600a | |||
f0fe3c7879 | |||
33fd852bda | |||
65c9dc5fa8 | |||
7e2eea46d3 | |||
01773e433f | |||
ca5eaf4270 | |||
7d64f057c7 | |||
48d016e97d | |||
e9ce506d1c | |||
e0eb0bd06a | |||
9f992ec40d | |||
ce8ef4f95b | |||
4d0560eff2 | |||
d49cde1994 | |||
16a1173f1d | |||
333d2f5562 | |||
c7fe1bdef5 | |||
46a967fb8c | |||
ca22e84858 | |||
20bd962e6a | |||
22a1448372 | |||
722a5cd9e1 | |||
efecf38bcd | |||
dfb75d67b9 | |||
961d405921 | |||
e025b6b9db | |||
bc5aa1bf71 | |||
3cd30ea96a | |||
504751424f | |||
37a00a48a7 | |||
2a2f5d90ae | |||
f0920f5638 | |||
43fb998651 | |||
148fcdbe37 | |||
de79bba540 | |||
d5881142aa | |||
494c468da8 | |||
672668ccdb | |||
538aed9147 | |||
d800273891 | |||
94f96c5165 | |||
4b1d60c727 | |||
cfe547b31a | |||
f6195f775f | |||
87879ab3b8 | |||
81a837faed | |||
c06f905702 | |||
f6f9e0234a | |||
e1e1472a8f | |||
b7e1fe1696 | |||
0314d0440f | |||
3226b1819d | |||
4b8a85763c | |||
ae4f2c3bb0 | |||
bc28b83062 | |||
2603c36e7d | |||
81b56cce62 | |||
649a2b6f8e | |||
4caf81ef89 | |||
65c33a37a1 | |||
eff17931eb | |||
b33c2abf82 | |||
ba514f0b0e | |||
7f60867ae9 | |||
71d8836118 | |||
97d6f8ce80 | |||
ca7f009e74 | |||
f08e5176c3 | |||
70161a54fa | |||
a945d0a78d | |||
245b799ccf | |||
0dc6c3ecfd | |||
98f000cc70 | |||
8d4daff068 | |||
79d68a5b9b | |||
412d0ee4f9 | |||
e8788bde08 | |||
815a71fe8b | |||
fce16ba51f | |||
50cfd9c9b1 | |||
369b03bffb |
27
.gitattributes
vendored
27
.gitattributes
vendored
@ -1,9 +1,24 @@
|
||||
/libtorrent export-ignore
|
||||
/win32 export-ignore
|
||||
docs/build export-ignore
|
||||
docs/source export-ignore
|
||||
/tests export-ignore
|
||||
deluge/scripts export-ignore
|
||||
/libtorrent/ export-ignore
|
||||
/win32/ export-ignore
|
||||
/osx/ export-ignore
|
||||
docs/build/ export-ignore
|
||||
docs/source/ export-ignore
|
||||
/tests/ export-ignore
|
||||
deluge/scripts/ export-ignore
|
||||
setup.cfg export-ignore
|
||||
check_glade.sh export-ignore
|
||||
createicons.sh export-ignore
|
||||
create_potfiles_in.py export-ignore
|
||||
gettextize.sh export-ignore
|
||||
deluge/i18n/deluge.pot export-ignore
|
||||
deluge/ui/web/css/*-debug.css export-ignore
|
||||
deluge/ui/web/js/*-debug.js export-ignore
|
||||
deluge/ui/web/js/deluge-all/ export-ignore
|
||||
deluge/ui/web/js/ext-extensions/ export-ignore
|
||||
deluge/ui/web/gen_gettext.py export-ignore
|
||||
deluge/ui/web/build export-ignore
|
||||
deluge/ui/web/docs/ export-ignore
|
||||
|
||||
.gitattributes export-ignore
|
||||
.gitmodules export-ignore
|
||||
.gitignore export-ignore
|
||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -7,3 +7,8 @@ dist
|
||||
*.pyc
|
||||
*.tar.*
|
||||
_trial_temp
|
||||
deluge/i18n/*/
|
||||
*.desktop
|
||||
.build_data*
|
||||
osx/app
|
||||
RELEASE-VERSION
|
||||
|
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
||||
[submodule "libtorrent"]
|
||||
path = libtorrent
|
||||
url = git://deluge-torrent.org/libtorrent
|
797
AUTHORS
Normal file
797
AUTHORS
Normal file
@ -0,0 +1,797 @@
|
||||
Authors:
|
||||
* Andrew Resch ('andar') <andrewresch@gmail.com>
|
||||
* Damien Churchill ('damoxc') <damoxc@gmail.com>
|
||||
|
||||
Main Developers:
|
||||
* Andrew Resch
|
||||
* Damien Churchill
|
||||
* John Garland ('johnnyg') <johnnybg+deluge@gmail.com>
|
||||
* Calum Lind ('cas') <calumlind+deluge@gmail.com>
|
||||
|
||||
libtorrent (http://www.libtorrent.org):
|
||||
* Arvid Norberg
|
||||
|
||||
Contributors (and Past Developers):
|
||||
* Zach Tibbitts <zach@collegegeek.org>
|
||||
* Alon Zakai ('Kripken') <kripkensteiner@gmail.com>
|
||||
* Marcos Pinto ('markybob') <markybob@gmail.com>
|
||||
* Alex Dedul
|
||||
* Sadrul Habib Chowdhury
|
||||
* Ido Abramovich <ido.deluge@gmail.com>
|
||||
* Martijn Voncken <mvoncken@gmail.com>
|
||||
* Mark Stahler ('kramed') <markstahler@gmail.com>
|
||||
* Pedro Algarvio ('s0undt3ch') <ufs@ufsoft.org>
|
||||
* Cristian Greco ('cgreco') <cristian@regolo.cc>
|
||||
* Chase Sterling ('gazpachoKing') <chase.sterling@gmail.com>
|
||||
|
||||
Plugin Developers:
|
||||
* Autoadd : Chase Sterling
|
||||
* Blocklist : John Garland
|
||||
* Execute : Damien Churchill
|
||||
* Extractor : Andrew Resch
|
||||
* Label : Martijn Voncken
|
||||
* Notifications : Pedro Algarvio
|
||||
* Scheduler : Andrew Resch
|
||||
* Webui : Damien Churchill
|
||||
|
||||
Images Authors:
|
||||
|
||||
* files: deluge/ui/data/pixmaps/*.svg, *.png
|
||||
deluge/ui/web/icons/active.png, alert.png, all.png, checking.png, dht.png,
|
||||
downloading.png, inactive.png, queued.png, seeding.png, traffic.png
|
||||
exceptions: deluge/ui/data/pixmaps/deluge.svg and derivatives
|
||||
copyright: Andrew Resch
|
||||
license: GPLv3
|
||||
|
||||
* files: deluge/ui/data/pixmaps/deluge.svg and derivatives
|
||||
deluge/ui/web/icons/apple-pre-*.png, deluge*.png
|
||||
deluge/ui/web/images/deluge*.png
|
||||
copyright: Andrew Wedderburn
|
||||
license: GPLv3
|
||||
|
||||
* files: deluge/plugins/blocklist/blocklist/data/*.png
|
||||
deluge/ui/data/pixmaps/tracker_warning16.png, tracker_all16.png, lock48.png
|
||||
copyright: Gnome Icon Theme
|
||||
license: GPLv2
|
||||
url: http://ftp.acc.umu.se/pub/GNOME/sources/gnome-icon-theme
|
||||
|
||||
* files: deluge/ui/data/pixmaps/magnet.png
|
||||
copyright: Woothemes
|
||||
license: Freeware
|
||||
icon pack: WP Woothemes Ultimate
|
||||
url: http://www.woothemes.com/
|
||||
|
||||
* files: deluge/ui/data/pixmaps/flags/*.png
|
||||
copyright: Mark James <mjames@gmail.com>
|
||||
license: Public Domain
|
||||
url: http://famfamfam.com/lab/icons/flags/
|
||||
|
||||
* files: deluge/ui/web/icons/*.png
|
||||
exceptions: apple-pre-*.png, active.png, alert.png, all.png, deluge.png, dht.png,
|
||||
downloading.png, inactive.png, queued.png, seeding.png, traffic.png
|
||||
copyright: Yusuke Kamiyamane <p@yusukekamiyamane.com>
|
||||
license: Creative Commons Attribution 3.0 License
|
||||
url: http://p.yusukekamiyamane.com/
|
||||
|
||||
* files: deluge/ui/web/images/spinner.gif, spinner-split.gif
|
||||
copyright: Steven Chim
|
||||
license: BSD license
|
||||
url: http://members.upc.nl/j.chim/ext/spinner2/ext-spinner.html
|
||||
|
||||
Translation Contributors:
|
||||
* files: deluge/i18n/*.po
|
||||
|
||||
Aaron Wang Shi
|
||||
abbigss
|
||||
ABCdatos
|
||||
Abcx
|
||||
Actam
|
||||
Adam
|
||||
adaminikisi
|
||||
adi_oporanu
|
||||
Adrian Goll
|
||||
afby
|
||||
Ahmades
|
||||
Ahmad Farghal
|
||||
Ahmad Gharbeia أحمد غربية
|
||||
akira
|
||||
Aki Sivula
|
||||
Alan Pepelko
|
||||
Alberto
|
||||
Alberto Ferrer
|
||||
alcatr4z
|
||||
AlckO
|
||||
Aleksej Korgenkov
|
||||
Alessio Treglia
|
||||
Alexander Ilyashov
|
||||
Alexander Matveev
|
||||
Alexander Saltykov
|
||||
Alexander Taubenkorb
|
||||
Alexander Telenga
|
||||
Alexander Yurtsev
|
||||
Alexandre Martani
|
||||
Alexandre Rosenfeld
|
||||
Alexandre Sapata Carbonell
|
||||
Alexey Osipov
|
||||
Alin Claudiu Radut
|
||||
allah
|
||||
AlSim
|
||||
Alvaro Carrillanca P.
|
||||
A.Matveev
|
||||
Andras Hipsag
|
||||
András Kárász
|
||||
Andrea Ratto
|
||||
Andreas Johansson
|
||||
Andreas Str
|
||||
André F. Oliveira
|
||||
AndreiF
|
||||
andrewh
|
||||
Angel Guzman Maeso
|
||||
Aníbal Deboni Neto
|
||||
animarval
|
||||
Antonio Cono
|
||||
antoniojreyes
|
||||
Anton Shestakov
|
||||
Anton Yakutovich
|
||||
antou
|
||||
Arkadiusz Kalinowski
|
||||
Artin
|
||||
artir
|
||||
Astur
|
||||
Athanasios Lefteris
|
||||
Athmane MOKRAOUI (ButterflyOfFire)
|
||||
Augusta Carla Klug
|
||||
Avoledo Marco
|
||||
axaard
|
||||
AxelRafn
|
||||
Axezium
|
||||
Ayont
|
||||
b3rx
|
||||
Bae Taegil
|
||||
Bajusz Tamás
|
||||
Balaam's Miracle
|
||||
Ballestein
|
||||
Bent Ole Fosse
|
||||
berto89
|
||||
bigx
|
||||
Bjorn Inge Berg
|
||||
blackbird
|
||||
Blackeyed
|
||||
blackmx
|
||||
BlueSky
|
||||
Blutheo
|
||||
bmhm
|
||||
bob00work
|
||||
boenki
|
||||
Bogdan Bădic-Spătariu
|
||||
bonpu
|
||||
Boone
|
||||
boss01
|
||||
Branislav Jovanović
|
||||
bronze
|
||||
brownie
|
||||
Brus46
|
||||
bumper
|
||||
butely
|
||||
BXCracer
|
||||
c0nfidencal
|
||||
Can Kaya
|
||||
Carlos Alexandro Becker
|
||||
cassianoleal
|
||||
Cédric.h
|
||||
César Rubén
|
||||
chaoswizard
|
||||
Chen Tao
|
||||
chicha
|
||||
Chien Cheng Wei
|
||||
Christian Kopac
|
||||
Christian Widell
|
||||
Christoffer Brodd-Reijer
|
||||
christooss
|
||||
CityAceE
|
||||
Clopy
|
||||
Clusty
|
||||
cnu
|
||||
Commandant
|
||||
Constantinos Koniaris
|
||||
Coolmax
|
||||
cosmix
|
||||
Costin Chirvasuta
|
||||
CoVaLiDiTy
|
||||
cow_2001
|
||||
Crispin Kirchner
|
||||
crom
|
||||
Cruster
|
||||
Cybolic
|
||||
Dan Bishop
|
||||
Danek
|
||||
Dani
|
||||
Daniel Demarco
|
||||
Daniel Ferreira
|
||||
Daniel Frank
|
||||
Daniel Holm
|
||||
Daniel Høyer Iversen
|
||||
Daniel Marynicz
|
||||
Daniel Nylander
|
||||
Daniel Patriche
|
||||
Daniel Schildt
|
||||
Daniil Sorokin
|
||||
Dante Díaz
|
||||
Daria Michalska
|
||||
DarkenCZ
|
||||
Darren
|
||||
Daspah
|
||||
David Eurenius
|
||||
davidhjelm
|
||||
David Machakhelidze
|
||||
Dawid Dziurdzia
|
||||
Daya Adianto
|
||||
dcruz
|
||||
Deady
|
||||
Dereck Wonnacott
|
||||
Devgru
|
||||
Devid Antonio FiloniDevilDogTG
|
||||
di0rz`
|
||||
Dialecti Valsamou
|
||||
Diego Medeiros
|
||||
Dkzoffy
|
||||
Dmitrij D. Czarkoff
|
||||
Dmitriy Geels
|
||||
Dmitry Olyenyov
|
||||
Dominik Kozaczko
|
||||
Dominik Lübben
|
||||
doomster
|
||||
Dorota Król
|
||||
Doyen Philippe
|
||||
Dread Knight
|
||||
DreamSonic
|
||||
duan
|
||||
Duong Thanh An
|
||||
DvoglavaZver
|
||||
dwori
|
||||
dylansmrjones
|
||||
Ebuntor
|
||||
Edgar Alejandro Jarquin Flores
|
||||
Eetu
|
||||
ekerazha
|
||||
Elias Julkunen
|
||||
elparia
|
||||
Emberke
|
||||
Emiliano Goday Caneda
|
||||
EndelWar
|
||||
eng.essam
|
||||
enubuntu
|
||||
ercangun
|
||||
Erdal Ronahi
|
||||
ergin üresin
|
||||
Eric
|
||||
Éric Lassauge
|
||||
Erlend Finvåg
|
||||
Errdil
|
||||
ethan shalev
|
||||
Evgeni Spasov
|
||||
ezekielnin
|
||||
Fabian Ordelmans
|
||||
Fabio Mazanatti
|
||||
Fábio Nogueira
|
||||
FaCuZ
|
||||
Felipe Lerena
|
||||
Fernando Pereira
|
||||
fjetland
|
||||
Florian Schäfer
|
||||
FoBoS
|
||||
Folke
|
||||
Force
|
||||
fosk
|
||||
fragarray
|
||||
freddeg
|
||||
Frédéric Perrin
|
||||
Fredrik Kilegran
|
||||
FreeAtMind
|
||||
Fulvio Ciucci
|
||||
Gabor Kelemen
|
||||
Galatsanos Panagiotis
|
||||
Gaussian
|
||||
gdevitis
|
||||
Georg Brzyk
|
||||
George Dumitrescu
|
||||
Georgi Arabadjiev
|
||||
Georg Sieber
|
||||
Gerd Radecke
|
||||
Germán Heusdens
|
||||
Gianni Vialetto
|
||||
Gigih Aji Ibrahim
|
||||
Giorgio Wicklein
|
||||
Giovanni Rapagnani
|
||||
Giuseppe
|
||||
gl
|
||||
glen
|
||||
granjerox
|
||||
Green Fish
|
||||
greentea
|
||||
Greyhound
|
||||
G. U.
|
||||
Guillaume BENOIT
|
||||
Guillaume Pelletier
|
||||
Gustavo Henrique Klug
|
||||
gutocarvalho
|
||||
Guybrush88
|
||||
Hans Rødtang
|
||||
HardDisk
|
||||
Hargas Gábor
|
||||
Heitor Thury Barreiros Barbosa
|
||||
helios91940
|
||||
helix84
|
||||
Helton Rodrigues
|
||||
Hendrik Luup
|
||||
Henrique Ferreiro
|
||||
Henry Goury-Laffont
|
||||
Hezy Amiel
|
||||
hidro
|
||||
hoball
|
||||
hokten
|
||||
Holmsss
|
||||
hristo.num
|
||||
Hubert Życiński
|
||||
Hyo
|
||||
Iarwain
|
||||
ibe
|
||||
ibear
|
||||
Id2ndR
|
||||
Igor Zubarev
|
||||
IKON (Ion)
|
||||
imen
|
||||
Ionuț Jula
|
||||
Isabelle STEVANT
|
||||
István Nyitrai
|
||||
Ivan Petrovic
|
||||
Ivan Prignano
|
||||
IvaSerge
|
||||
jackmc
|
||||
Jacks0nxD
|
||||
Jack Shen
|
||||
Jacky Yeung
|
||||
Jacques Stadler
|
||||
Janek Thomaschewski
|
||||
Jan Kaláb
|
||||
Jan Niklas Hasse
|
||||
Jasper Groenewegen
|
||||
Javi Rodríguez
|
||||
Jayasimha (ಜಯಸಿಂಹ)
|
||||
jeannich
|
||||
Jeff Bailes
|
||||
Jesse Zilstorff
|
||||
Joan Duran
|
||||
João Santos
|
||||
Joar Bagge
|
||||
Joe Anderson
|
||||
Joel Calado
|
||||
Johan Linde
|
||||
John Garland
|
||||
Jojan
|
||||
jollyr0ger
|
||||
Jonas Bo Grimsgaard
|
||||
Jonas Granqvist
|
||||
Jonas Slivka
|
||||
Jonathan Zeppettini
|
||||
Jørgen
|
||||
Jørgen Tellnes
|
||||
josé
|
||||
José Geraldo Gouvêa
|
||||
José Iván León Islas
|
||||
José Lou C.
|
||||
Jose Sun
|
||||
Jr.
|
||||
Jukka Kauppinen
|
||||
Julián Alarcón
|
||||
julietgolf
|
||||
Jusic
|
||||
Justzupi
|
||||
Kaarel
|
||||
Kai Thomsen
|
||||
Kalman Tarnay
|
||||
Kamil Páral
|
||||
Kane_F
|
||||
kaotiks@gmail.com
|
||||
Kateikyoushii
|
||||
kaxhinaz
|
||||
Kazuhiro NISHIYAMA
|
||||
Kerberos
|
||||
Keresztes Ákos
|
||||
kevintyk
|
||||
kiersie
|
||||
Kimbo^
|
||||
Kim Lübbe
|
||||
kitzOgen
|
||||
Kjetil Rydland
|
||||
kluon
|
||||
kmikz
|
||||
Knedlyk
|
||||
koleoptero
|
||||
Kőrösi Krisztián
|
||||
Kouta
|
||||
Krakatos
|
||||
Krešo Kunjas
|
||||
kripken
|
||||
Kristaps
|
||||
Kristian Øllegaard
|
||||
Kristoffer Egil Bonarjee
|
||||
Krzysztof Janowski
|
||||
Krzysztof Zawada
|
||||
Larry Wei Liu
|
||||
laughterwym
|
||||
Laur Mõtus
|
||||
lazka
|
||||
leandrud
|
||||
lê bình
|
||||
Le Coz Florent
|
||||
Leo
|
||||
liorda
|
||||
LKRaider
|
||||
LoLo_SaG
|
||||
Long Tran
|
||||
Lorenz
|
||||
Low Kian Seong
|
||||
Luca Andrea Rossi
|
||||
Luca Ferretti
|
||||
Lucky LIX
|
||||
Luis Gomes
|
||||
Luis Reis
|
||||
Łukasz Wyszyński
|
||||
luojie-dune
|
||||
maaark
|
||||
Maciej Chojnacki
|
||||
Maciej Meller
|
||||
Mads Peter Rommedahl
|
||||
Major Kong
|
||||
Malaki
|
||||
malde
|
||||
Malte Lenz
|
||||
Mantas Kriaučiūnas
|
||||
Mara Sorella
|
||||
Marcin
|
||||
Marcin Falkiewicz
|
||||
marcobra
|
||||
Marco da Silva
|
||||
Marco de Moulin
|
||||
Marco Rodrigues
|
||||
Marcos
|
||||
Marcos Escalier
|
||||
Marcos Pinto
|
||||
Marcus Ekstrom
|
||||
Marek Dębowski
|
||||
Mário Buči
|
||||
Mario Munda
|
||||
Marius Andersen
|
||||
Marius Hudea
|
||||
Marius Mihai
|
||||
Mariusz Cielecki
|
||||
Mark Krapivner
|
||||
marko-markovic
|
||||
Markus Brummer
|
||||
Markus Sutter
|
||||
Martin
|
||||
Martin Dybdal
|
||||
Martin Iglesias
|
||||
Martin Lettner
|
||||
Martin Pihl
|
||||
Masoud Kalali
|
||||
mat02
|
||||
Matej Urbančič
|
||||
Mathias-K
|
||||
Mathieu Arès
|
||||
Mathieu D. (MatToufoutu)
|
||||
Mathijs
|
||||
Matrik
|
||||
Matteo Renzulli
|
||||
Matteo Settenvini
|
||||
Matthew Gadd
|
||||
Matthias Benkard
|
||||
Matthias Mailänder
|
||||
Mattias Ohlsson
|
||||
Mauro de Carvalho
|
||||
Max Molchanov
|
||||
Me
|
||||
MercuryCC
|
||||
Mert Bozkurt
|
||||
Mert Dirik
|
||||
MFX
|
||||
mhietar
|
||||
mibtha
|
||||
Michael Budde
|
||||
Michael Kaliszka
|
||||
Michalis Makaronides
|
||||
Michał Tokarczyk
|
||||
Miguel Pires da Rosa
|
||||
Mihai Capotă
|
||||
Miika Metsälä
|
||||
Mikael Fernblad
|
||||
Mike Sierra
|
||||
mikhalek
|
||||
Milan Prvulović
|
||||
Milo Casagrande
|
||||
Mindaugas
|
||||
Miroslav Matejaš
|
||||
misel
|
||||
mithras
|
||||
Mitja Pagon
|
||||
M.Kitchen
|
||||
Mohamed Magdy
|
||||
moonkey
|
||||
MrBlonde
|
||||
muczy
|
||||
Münir Ekinci
|
||||
Mustafa Temizel
|
||||
mvoncken
|
||||
Mytonn
|
||||
NagyMarton
|
||||
neaion
|
||||
Neil Lin
|
||||
Nemo
|
||||
Nerijus Arlauskas
|
||||
Nicklas Larsson
|
||||
Nicolaj Wyke
|
||||
Nicola Piovesan
|
||||
Nicolas Sabatier
|
||||
Nicolas Velin
|
||||
Nightfall
|
||||
NiKoB
|
||||
Nikolai M. Riabov
|
||||
Niko_Thien
|
||||
niska
|
||||
Nithir
|
||||
noisemonkey
|
||||
nomemohes
|
||||
nosense
|
||||
null
|
||||
Nuno Estêvão
|
||||
Nuno Santos
|
||||
nxxs
|
||||
nyo
|
||||
obo
|
||||
Ojan
|
||||
Olav Andreas Lindekleiv
|
||||
oldbeggar
|
||||
Olivier FAURAX
|
||||
orphe
|
||||
osantana
|
||||
Osman Tosun
|
||||
OssiR
|
||||
otypoks
|
||||
ounn
|
||||
Oz123
|
||||
Özgür BASKIN
|
||||
Pablo Carmona A.
|
||||
Pablo Ledesma
|
||||
Pablo Navarro Castillo
|
||||
Paco Molinero
|
||||
Pål-Eivind Johnsen
|
||||
pano
|
||||
Paolo Naldini
|
||||
Paracelsus
|
||||
Patryk13_03
|
||||
Patryk Skorupa
|
||||
PattogoTehen
|
||||
Paul Lange
|
||||
Pavcio
|
||||
Paweł Wysocki
|
||||
Pedro Brites Moita
|
||||
Pedro Clemente Pereira Neto
|
||||
Pekka "PEXI" Niemistö
|
||||
Penegal
|
||||
Penzo
|
||||
perdido
|
||||
Peter Kotrcka
|
||||
Peter Skov
|
||||
Peter Van den Bosch
|
||||
Petter Eklund
|
||||
Petter Viklund
|
||||
phatsphere
|
||||
Phenomen
|
||||
Philipi
|
||||
Philippides Homer
|
||||
phoenix
|
||||
pidi
|
||||
Pierre Quillery
|
||||
Pierre Rudloff
|
||||
Pierre Slamich
|
||||
Pietrao
|
||||
Piotr Strębski
|
||||
Piotr Wicijowski
|
||||
Pittmann Tamás
|
||||
Playmolas
|
||||
Prescott
|
||||
Prescott_SK
|
||||
pronull
|
||||
Przemysław Kulczycki
|
||||
Pumy
|
||||
pushpika
|
||||
PY
|
||||
qubicllj
|
||||
r21vo
|
||||
Rafał Barański
|
||||
rainofchaos
|
||||
Rajbir
|
||||
ras0ir
|
||||
Rat
|
||||
rd1381
|
||||
Renato
|
||||
Rene Hennig
|
||||
Rene Pärts
|
||||
Ricardo Duarte
|
||||
Richard
|
||||
Robert Hrovat
|
||||
Roberth Sjonøy
|
||||
Robert Lundmark
|
||||
Robin Jakobsson
|
||||
Robin Kåveland
|
||||
Rodrigo Donado
|
||||
Roel Groeneveld
|
||||
rohmaru
|
||||
Rolf Christensen
|
||||
Rolf Leggewie
|
||||
Roni Kantis
|
||||
Ronmi
|
||||
Rostislav Raykov
|
||||
royto
|
||||
RuiAmaro
|
||||
Rui Araújo
|
||||
Rui Moura
|
||||
Rune Svendsen
|
||||
Rusna
|
||||
Rytis
|
||||
Sabirov Mikhail
|
||||
salseeg
|
||||
Sami Koskinen
|
||||
Samir van de Sand
|
||||
Samuel Arroyo Acuña
|
||||
Samuel R. C. Vale
|
||||
Sanel
|
||||
Santi
|
||||
Santi Martínez Cantelli
|
||||
Sardan
|
||||
Sargate Kanogan
|
||||
Sarmad Jari
|
||||
Saša Bodiroža
|
||||
sat0shi
|
||||
Saulius Pranckevičius
|
||||
Savvas Radevic
|
||||
Sebastian Krauß
|
||||
Sebastián Porta
|
||||
Sedir
|
||||
Sefa Denizoğlu
|
||||
sekolands
|
||||
Selim Suerkan
|
||||
semsomi
|
||||
Sergii Golovatiuk
|
||||
setarcos
|
||||
Sheki
|
||||
Shironeko
|
||||
Shlomil
|
||||
silfiriel
|
||||
Simone Tolotti
|
||||
Simone Vendemia
|
||||
sirkubador
|
||||
Sławomir Więch
|
||||
slip
|
||||
slyon
|
||||
smoke
|
||||
Sonja
|
||||
spectral
|
||||
spin_555
|
||||
spitf1r3
|
||||
Spiziuz
|
||||
Spyros Theodoritsis
|
||||
SqUe
|
||||
Squigly
|
||||
srtck
|
||||
Stefan Horning
|
||||
Stefano Maggiolo
|
||||
Stefano Roberto Soleti
|
||||
steinberger
|
||||
Stéphane Travostino
|
||||
Stephan Klein
|
||||
Steven De Winter
|
||||
Stevie
|
||||
Stian24
|
||||
stylius
|
||||
Sukarn Maini
|
||||
Sunjae Park
|
||||
Susana Pereira
|
||||
szymon siglowy
|
||||
takercena
|
||||
TAS
|
||||
Taygeto
|
||||
temy4
|
||||
texxxxxx
|
||||
thamood
|
||||
Thanos Chatziathanassiou
|
||||
Tharawut Paripaiboon
|
||||
Theodoor
|
||||
Théophane Anestis
|
||||
Thor Marius K. Høgås
|
||||
Tiago Silva
|
||||
Tiago Sousa
|
||||
Tikkel
|
||||
tim__b
|
||||
Tim Bordemann
|
||||
Tim Fuchs
|
||||
Tim Kornhammar
|
||||
Timo
|
||||
Timo Jyrinki
|
||||
Timothy Babych
|
||||
TitkosRejtozo
|
||||
Tom
|
||||
Tomas Gustavsson
|
||||
Tomas Valentukevičius
|
||||
Tomasz Dominikowski
|
||||
Tomislav Plavčić
|
||||
Tom Mannerhagen
|
||||
Tommy Mikkelsen
|
||||
Tom Verdaat
|
||||
Tony Manco
|
||||
Tor Erling H. Opsahl
|
||||
Toudi
|
||||
tqm_z
|
||||
Trapanator
|
||||
Tribaal
|
||||
Triton
|
||||
TuniX12
|
||||
Tuomo Sipola
|
||||
turbojugend_gr
|
||||
Turtle.net
|
||||
twilight
|
||||
tymmej
|
||||
Ulrik
|
||||
Umarzuki Mochlis
|
||||
unikob
|
||||
Vadim Gusev
|
||||
Vagi
|
||||
Valentin Bora
|
||||
Valmantas Palikša
|
||||
VASKITTU
|
||||
Vassilis Skoullis
|
||||
vetal17
|
||||
vicedo
|
||||
viki
|
||||
villads hamann
|
||||
Vincent Garibal
|
||||
Vincent Ortalda
|
||||
vinchi007
|
||||
Vinícius de Figueiredo Silva
|
||||
Vinzenz Vietzke
|
||||
virtoo
|
||||
virtual_spirit
|
||||
Vitor Caike
|
||||
Vitor Lamas Gatti
|
||||
Vladimir Lazic
|
||||
Vladimir Sharshov
|
||||
Wanderlust
|
||||
Wander Nauta
|
||||
Ward De Ridder
|
||||
WebCrusader
|
||||
webdr
|
||||
Wentao Tang
|
||||
wilana
|
||||
Wilfredo Ernesto Guerrero Campos
|
||||
Wim Champagne
|
||||
World Sucks
|
||||
Xabi Ezpeleta
|
||||
Xavi de Moner
|
||||
XavierToo
|
||||
XChesser
|
||||
Xiaodong Xu
|
||||
xyb
|
||||
Yaron
|
||||
Yasen Pramatarov
|
||||
YesPoX
|
||||
Yuren Ju
|
||||
Yves MATHIEU
|
||||
zekopeko
|
||||
zhuqin
|
||||
Zissan
|
||||
Γιάννης Κατσαμπίρης
|
||||
Артём Попов
|
||||
Миша
|
||||
Шаймарданов Максим
|
||||
蔡查理
|
408
ChangeLog
408
ChangeLog
@ -1,4 +1,390 @@
|
||||
=== Deluge 1.3.0 (In Development) ===
|
||||
=== Deluge 1.4.0 (In Development) ===
|
||||
* Improved Logging
|
||||
* Removed the AutoAdd feature on the core. It's now handled with the AutoAdd
|
||||
plugin, which is also shipped with Deluge, and it does a better job and
|
||||
now, it even supports multiple users perfectly.
|
||||
* Authentication/Permission exceptions are now sent to clients and recreated
|
||||
there to allow acting upon them.
|
||||
* Enforced the use of the "deluge.plugins" namespace to reduce package
|
||||
names clashing beetween regular packages and deluge plugins.
|
||||
|
||||
==== Core ====
|
||||
* Make the distinction between adding to the session new unmanaged torrents
|
||||
and torrents loaded from state. This will break backwards compatability.
|
||||
* Pass a copy of an event instead of passing the event arguments to the
|
||||
event handlers. This will break backwards compatability.
|
||||
* Allow changing ownership of torrents.
|
||||
* File modifications on the auth file are now detected and when they happen,
|
||||
the file is reloaded. Upon finding an old auth file with an old format, an
|
||||
upgrade to the new format is made, file saved, and reloaded.
|
||||
* Authentication no longer requires a username/password. If one or both of
|
||||
these is missing, an authentication error will be sent to the client
|
||||
which sould then ask the username/password to the user.
|
||||
* Implemented sequential downloads.
|
||||
* #378: Provide information about a torrent's pieces states
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix uncaught exception when closing deluge in classic mode
|
||||
* Allow changing ownership of torrents
|
||||
* Host entries in the Connection Manager UI are now editable. They're
|
||||
now also migrated from the old format were automatic localhost logins were
|
||||
possible, which no longer is, this fixes #1814.
|
||||
* Implemented sequential downloads UI handling.
|
||||
* #378: Allow showing a pieces bar instead of a regular progress bar in a
|
||||
torrent's status tab.
|
||||
* #2093: Make torrent opening compatible with all unicode paths.
|
||||
|
||||
==== Blocklist Plugin ====
|
||||
* #1382: Implemented whitelist support to both core and GTK UI.
|
||||
* Implemented ip filter cleaning before each update. Restarting the deluge
|
||||
daemon is no longer needed.
|
||||
* If "check_after_days" is 0(zero), the timer is not started anymore. It
|
||||
would keep updating one call after the other. If the value changed, the
|
||||
timer is now stopped and restarted using the new value.
|
||||
|
||||
=== Deluge 1.3.7 (In Development) ===
|
||||
==== GtkUI ====
|
||||
* Fix issue with Plugins that add Tab to torrentdetails
|
||||
* Fix the scalable icon install directory
|
||||
|
||||
==== Extractor ====
|
||||
* #2290: Fix dotted filenames being rejected
|
||||
|
||||
=== Deluge 1.3.6 (25 Feburary 2013) ===
|
||||
==== Core ====
|
||||
* Catch & log KeyError when removing a torrent from the queued torrents set
|
||||
* Fix moving/renaming torrents issues when using libtorrent 0.16
|
||||
* Make sure queue order is preserved when restarting
|
||||
* #2160: Disable use of python bindings for libtorrent extensions and replace with session flag
|
||||
* #2163: Fix unable add torrent file with empty (0:) encoding tag
|
||||
* #2201: Fix error in authmanager if auth file has extra newlines
|
||||
* #2109: Fix the Proxy settings not being cleared by setting None
|
||||
* #2110: Fix accepting magnet uris with xt param anywhere within them
|
||||
* #2204: Fix daemon shutdown hang with large numbers of torrents
|
||||
|
||||
==== Client ====
|
||||
* Fix keyerrors after removing torrents from UIs
|
||||
|
||||
==== GtkUI ====
|
||||
* Add move completed option to add torrent dialog
|
||||
* Prevent jitter in torrent view
|
||||
* Fix torrent creation with non-ascii characters
|
||||
* Fix #2100 : Add option not to bring main window to front when adding torrents through ipcinterface
|
||||
* Add Quit Dialog when toggling classic mode in preferences and only show connection manager when not in classic mode.
|
||||
* #2169: Fix 'Download Location' in the Add Torrent Dialog not set correctly when folder typed into Other->Location field
|
||||
* #2171: Fix the Add Peer dialog not responding if empty or invalid values entered
|
||||
* #2104: Fix no title set for the appindicator
|
||||
* #2086: Fix submenus and icons for appindicator
|
||||
* #2146: Fix missing translations in View|Tabs submenu
|
||||
* Fix torrent names on libtorrent 0.16 on windows
|
||||
* #2147: Fix missing translations for plugin preferences page
|
||||
* #1474: Fix the on_show_prefs hook not being executed immediatly after enabling a plugin
|
||||
* #1946: Fix ReactorNotRestartable error when set as startup application
|
||||
* #2130: Fix same name can be given to different files in Add Torrent dialog
|
||||
* #2129: Fix empty filename able to be set in AddTorrent dialog
|
||||
* #2228: Fix Apply-To-All in AddTorrent Dialog copying file renames to other torrents
|
||||
* #2260: Fix the Add Torrent dialog also bringing the main window to active workspace
|
||||
* Fix showing exception error to user in Classic Mode with no libtorrent installed
|
||||
|
||||
==== Console ====
|
||||
* LP#1004793: Enable use of connect command in non-interactive mode
|
||||
* Ensure console commands are executed in order
|
||||
* #2065: Fix crash with missing closing quote
|
||||
* #1397: Add support for -s STATE in info command
|
||||
|
||||
==== WebUI ====
|
||||
* Add move completed option to add torrent dialog
|
||||
* #2112: Fix world readable tmp directory in json_api
|
||||
* #2069: Fix login window layout problem when using larger than default font size
|
||||
* #1890: Fix columns in files and peers view could use some spacing
|
||||
* #2103: Fix sorting by name is case-sensitive [sedulous]
|
||||
* #2120: Fix manually entered values not being saved in spinners
|
||||
* #2212: Fix unable to scroll in proxy preferences page
|
||||
* Fix autoconnecting to the default host
|
||||
* #2046: Fix plugins not enabling properly until after refreshing page
|
||||
* #2125: Fix plugin methods not being available when enabled until restart
|
||||
* #2085: Fix not showing torrents in sidebar for categories other than 'All' in classic mode
|
||||
* #2232: Fix flag icon path in Peers Tab missing deluge.config.base
|
||||
* Fix submenus closing upon mouse click
|
||||
* Add failed login log message, including IP address, to enable use with fail2ban
|
||||
* #2261: Fix Proxy settings not being saved in preferences
|
||||
|
||||
==== Windows OS ====
|
||||
* Hide the cmd windows when running deluged.exe or deluge-web.exe
|
||||
* Add deluged-debug.exe and deluge-web-debug.exe that still show the cmd window
|
||||
* Add gtk locale files to fix untranslated text
|
||||
* Fix the Open Folder option not working with non-ascii paths
|
||||
* Fix the daemon starting with config dir containing spaces
|
||||
* Fix Windows tray submenu items requiring right-click instead of left-click
|
||||
* Fix issue with adding some torrents with illegal characters via url in gtk client
|
||||
* #2240: Fix freespace issue with large capacity drives
|
||||
|
||||
==== OS X ====
|
||||
* Fix Open File/Folder option
|
||||
* Add OS X Menu for GTK Quartz
|
||||
|
||||
==== Execute ====
|
||||
* Fix execute plugin not working with unicode torrent names
|
||||
|
||||
==== Extractor ====
|
||||
* Add Windows support, using 7-zip
|
||||
* Added support for more extensions
|
||||
* Disabled extracting 'Move Completed' torrents due to race condition
|
||||
|
||||
=== Deluge 1.3.5 (09 April 2012) ===
|
||||
==== GtkUI ====
|
||||
* Modified fix for #1957, keyerror with non-acsii columns
|
||||
* Fix translation of items in Sidebar and Torrent Menu
|
||||
* #2052: Fix translation of Progress bar text
|
||||
* #2071: Fix KeyError in gtkui when file priority set to value '3'
|
||||
* #2064: Fix files treeview height in Create Dialog
|
||||
* Fix missing semi-colon in deluge.desktop
|
||||
* Disable setting file priorities for seeding torrents
|
||||
* Bring MainWindow to front when opening another instance
|
||||
|
||||
==== WebUI ====
|
||||
* #2050: Fix 'Up Speed' column not sorting
|
||||
* Hide unused Infohash button in WebUI
|
||||
|
||||
==== Label ====
|
||||
* Disable unusable items for 'All' in sidebar menu
|
||||
* Fix items for translation
|
||||
|
||||
==== Console ====
|
||||
* Fix prefixed space for tab completing commands
|
||||
* Fix missing trailing space for command options with tab complete
|
||||
|
||||
==== Blocklist ====
|
||||
* Use (documented) formatdate over format_date_time
|
||||
|
||||
=== Deluge 1.3.4 (03 March 2012) ===
|
||||
==== Core ====
|
||||
* #1921: Free disk space reporting incorrectly in FreeBSD
|
||||
* #1964: Fix unhandled UnpicklingErrors
|
||||
* #1967: Fix unhandled IndexError when trying to open a non-json conf file
|
||||
* Fix setting daemon listen interface from command line
|
||||
* #2021: Fix share ratio limit not obeyed for seeded torrents added to session
|
||||
* Add optparse custom version to prevent unnecessary loading of libtorrent
|
||||
* #1554: Fix seeding on share ratio failing for partially selected torrents
|
||||
* Add proper process title naming in ps, top etc. (Depends: setproctitle)
|
||||
|
||||
==== GtkUI ====
|
||||
* #1918: Fix Drag'n'Drop not working in Windows
|
||||
* #1941: Increase maximum Cache Size to 999999 (15GiB)
|
||||
* #1940: File & folder renaming issue when using Add Torrent dialog in Windows
|
||||
* LP#821577: Fix UnpicklingError when external selection dragged onto Files Tab
|
||||
* #1934: Fix Unicode error in AddTorrent Dialog
|
||||
* #1957: Fix keyerror when adding columns for non-latin languages
|
||||
* #1969: Fix menu item 'Quit & Shutdown' still available when not connected to daemon
|
||||
* #1895: Fix Files Tab showing wrong files due to torrent_info race condition
|
||||
* #2010: Move speed text in titlebar to the beginning
|
||||
* #2032: Wait for client to shutdown/disconnect before stopping reactor
|
||||
* Fix compatibility with Python 2.5
|
||||
* Fix collapsed treeview in Create Torrent dialog
|
||||
* Ignore unmaximise event when window isn't visible
|
||||
* #1976: Fixed text entry with trailing newline characters causing issues for Move Storage
|
||||
|
||||
==== WebUI ====
|
||||
* Fix Webui files-tab menu setting wrong priority
|
||||
* Update to ExtJS 3.4.0
|
||||
* #1960: Fix statustab showing total_payload_download for upload as well
|
||||
* Remove uneeded Titlebar to save space
|
||||
* Fix clipped Browse button in WebUI
|
||||
* #1915: Fix being unable to stop the status bar from autohiding
|
||||
* Fix password box focus issue in Firefox
|
||||
* Fix plugin uploads from behind a reverse proxy
|
||||
* #2010: Move speed text in titlebar to the beginning
|
||||
* #1936: Fix Referenced before assignment error in json_api
|
||||
* Changes are now applied when clicking OK in Preferences
|
||||
* Added Download,Uploaded,Down Limit, Up Limit & Seeder/Peeds columns
|
||||
* Add magnet uri support to Add Url
|
||||
* Add keymaps for torrents - Ctrl-A (select all) and Delete
|
||||
* #2037: Fix 'Add Torrents' torrents list not scrolling
|
||||
* #2038: Fix Chrome 17 disconnecting from webui
|
||||
|
||||
==== Console ====
|
||||
* #1953: Fix flickering on every update
|
||||
* #1954: Fix 'invalid literal for float' when setting listen interface
|
||||
* #1945: Fix UnicodeDecodeError when using non-ascii chars in info
|
||||
|
||||
==== Label ====
|
||||
* #1961: Add missing 'All' filter option
|
||||
* #2035: Fix label options dialog in webui
|
||||
* #2036: Fix newly added labels not being sorted in torrent right click menu
|
||||
|
||||
==== Notification ====
|
||||
* #1905: Fix no email sent to second email address
|
||||
* #1898: Fix email notifications not including date/time they were sent
|
||||
|
||||
==== Scheduler ====
|
||||
* Add plugin page for WebUi
|
||||
|
||||
==== Execute ====
|
||||
* Commands now run scripts asynchronous to prevent Deluge from hanging
|
||||
|
||||
==== AutoAdd ====
|
||||
* Added watch folder support for '.magnet' text file containing single or multiple magnet uris
|
||||
* Fix glade object issue when re-enabling plugin in same session
|
||||
* Fix plugin not showing as enabled in webui
|
||||
|
||||
=== Deluge 1.3.3 (22 July 2011) ===
|
||||
==== Core ====
|
||||
* Properly show the 'Checking Resume Data' state instead of just 7
|
||||
* #1788: Added ability to use XDG_DOWNLOAD_DIR as default download folder
|
||||
* Fix path error with torrent files prefixed with 'file://' from Firefox
|
||||
* #1869: Fix setting the disk io read/write to bypass OS cache in Windows
|
||||
* #1504: Fix win32 running deluged as not logged in user via runas or service
|
||||
* #890: If added torrent already exists, append extra trackers to it
|
||||
* #1338: Fix Seeds and Peers totals not updating
|
||||
* #1239: Fix translated Tracker Error text not counted in sidebar Error status
|
||||
* Fix httpdownloader error with existing filename
|
||||
* #1505: Add libtorrent info to version output
|
||||
* #1637 Fix UnicodeDecodeError from 'deluge-* --help' with non-english languages
|
||||
* #1714 Fix handling of backslashes when renaming files/folders
|
||||
|
||||
==== GtkUI ====
|
||||
* Show the checking icon for torrents in the 'Checking Resume Data' state
|
||||
* #1195: Fix right-click selecting issue when switching between folders and files
|
||||
* Add F2 key shortcut for renaming filenames in the Files Tab
|
||||
* Increase max piece size to 16 MiB in create torrent dialog
|
||||
* #1475: Fix save and restore Preferences dialog size from config
|
||||
* Add search as you type to the torrent view
|
||||
* #1456: Fix no ETA showing with multiple files
|
||||
* #1560: Fix FilesTab Progress value sorting by int instead of float
|
||||
* #1263: Fix not remembering column widths
|
||||
* #948: New Release Dialog now shows the server version
|
||||
* Fix peers in PeersTab showing non-zero download rate when seeding
|
||||
|
||||
==== AutoAdd ====
|
||||
* #1861: Fix AutoAdd Warning (column number is a boolean)
|
||||
|
||||
==== Label ====
|
||||
* #1246: Fix losing Labels upon restart
|
||||
|
||||
==== Execute ====
|
||||
* #1477: Fix ignore Added events from state file on startup
|
||||
|
||||
==== ConsoleUI ====
|
||||
* #1258: Add support for urls and magnet uris in add command
|
||||
* #1801: Fix unhandled defered error and missing error message upon failed connect
|
||||
|
||||
=== Deluge 1.3.2 (24 May 2011) ===
|
||||
==== Core ====
|
||||
* #1527: Fix Converting unicode to unicode error in move_storage
|
||||
* #1373: Fix creating and moving non-ascii folder names in MS Windows
|
||||
* #1507: Fix temporary file race condition in core/core.py:add_torrent_url
|
||||
* Fix a bug that can occur when upgrading 1.1 config files
|
||||
* #1517: Fix isohunt urls not loading
|
||||
* Handle redirection when adding a torrent by url
|
||||
* #1614: Fix autoadd matching a directory called "torrent"
|
||||
* #1742: Fix failure in Event handler prevents further emissions
|
||||
|
||||
==== GtkUI ====
|
||||
* #1514: Added Indicator Applet
|
||||
* #1494: Add torrent columns Downloaded and Uploaded
|
||||
* #1308: Add torrent column Seeds/Peers ratio
|
||||
* #1646: Add torrent columns for per torrent upload and download speed limits
|
||||
* Add missing icons for Trackers filter
|
||||
* Fix inconsistancies in the text for translation
|
||||
* #1510: Fix cannot create a torrent with only non-zero tier trackers
|
||||
* #1513: Fix unhandled Twisted Error in test_listen_port
|
||||
* #690: Fix renaming folders does not remove old empty folders
|
||||
* #1336: Fix uneeded horizontal scrollbar showing in Files & Peers Tab
|
||||
* #1508: Fix TypeError in cell_data_queue() could not convert argument to correct param type
|
||||
* #1498: Fix double slashes appearing when renaming
|
||||
* #1283: Fix consistent icons for Files tab
|
||||
* #1282: Text for AutoManaged changed to 'On/Off' and localized
|
||||
* Fix Up/Down buttons in Edit Trackers Dialog
|
||||
* Add Key Shortcuts for main menu functions
|
||||
|
||||
==== WebUI ====
|
||||
* #1194: Fix infinite login prompt in web ui through reverse proxy
|
||||
* #1355: Fix slow changing states in webUI
|
||||
* #1536: Fix Edit Trackers window not scrolling and not being resizable
|
||||
* #1799: Fix Missing textbox for "Move completed" in torrent options
|
||||
* #1562: Fix Javascript error in Web UI when re-opening preferences
|
||||
* #1567: Fix js from plugins does not work with different 'base' setting
|
||||
* #1268: Fix torrent errors not displayed in webui
|
||||
* #1323: Fix filter panels not scrollable
|
||||
* Fix file uploads from behind a reverse proxy.
|
||||
* #1333: Fix peer list doesn't update automatically
|
||||
* #1537: Fix editing trackers list, trackers have to be reselected
|
||||
|
||||
==== ConsoleUI ====
|
||||
* #755: Fix can't set listen_ports through console UI
|
||||
* #1500: Fix Console crashes on command longer than terminal width
|
||||
* #1248: Fix deluge-console unicode support on redirected stdout
|
||||
* Fix for deluge-console not adding torrent files on MS Windows
|
||||
* #1450: Fix trailing white space in paths
|
||||
* Misc: Updated help text for deluge-console on MS Windows
|
||||
* #1484: Fix trying to access the screen object when not using interactive mode
|
||||
* #1548: Fix cli argument processing
|
||||
* #1856: Add --sort option to info command
|
||||
* #1857: Add seeding_time, active_time and tracker_status to info command
|
||||
|
||||
==== Scheduler ====
|
||||
* #1506: Fix max speed not restored on a yellow->green transition
|
||||
|
||||
=== Deluge 1.3.1 (31 October 2010) ===
|
||||
==== Core ====
|
||||
* #1369: Fix non-ascii config folders not working in windows
|
||||
|
||||
==== GtkUI ====
|
||||
* #1365: Fix sidebar not updating show/hide trackers
|
||||
* #1247: Fix hang on quit
|
||||
|
||||
==== WebUI ====
|
||||
* #1364: Fix preferences not saving when the web ui plugin is enabled in classic mode
|
||||
* #1377: Fix bug when enabling plugins
|
||||
* #1370: Fix issues with preferences
|
||||
* #1312: Fix deluge-web using 100% CPU
|
||||
|
||||
=== Deluge 1.3.0 (18 September 2010) ===
|
||||
==== Core ====
|
||||
* Fix issue where the save_timer is cancelled when it's not active
|
||||
* Fix unhandled exception when adding a torrent to the session
|
||||
* Moved xdg import so it is not called on Windows, where it is unused. fixes #1343
|
||||
* Fix key error after enabling a plugin that introduces a new status key
|
||||
* Ignore global stop ratio related settings in logic, so per torrent ones are used.
|
||||
* Ensure preferencesmanager only changes intended libtorrent session settings.
|
||||
* Fix issue when adding torrents without a 'session'. This can happen when a plugin adds a torrent, like how the AutoAdd plugin works. The user that adds this torrent will be an empty string.
|
||||
* Add TorrentFileCompleted event
|
||||
|
||||
==== GtkUI ====
|
||||
* Increase max piece size to 8 MiB in create torrent dialog (closes #1358)
|
||||
|
||||
==== Scheduler ====
|
||||
* Add max active downloading and seeding options to scheduler.
|
||||
* Fix scheduler so that it keeps current state, even after global settings change.
|
||||
|
||||
==== AutoAdd ====
|
||||
* AutoAdd plugin can now recover when one of the watchfolders has an unhandled exception.
|
||||
* Fix bug in AutoAdd plugin where watchdirs would not display in gtkui when first enabled.
|
||||
* Fix bugs with unicode torrents in AutoAdd plugin.
|
||||
|
||||
=== Deluge 1.3.0-rc2 (20 August 2010) ===
|
||||
==== Core ====
|
||||
* Fix tracker_icons failing on windows
|
||||
* Fix #1302 an uncaught exception in an state_changed event handler in SessionProxy was preventing the TorrentManager's stop method from properly saving all the resume data
|
||||
* Fix issue with SessionProxy not updating the torrent status correctly when get_torrent_status calls take place within the cache_expiry time
|
||||
|
||||
==== ConsoleUI ====
|
||||
* #1307: Fix not being able to add torrents
|
||||
* #1293: Fix not being able to add paths that contain backslashes
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix uncaught exception when closing deluge in classic mode
|
||||
|
||||
==== Execute ====
|
||||
* #1306: Fix always executing last event
|
||||
|
||||
==== Label ====
|
||||
* Fix being able to remove labels in web ui
|
||||
|
||||
==== WebUI ====
|
||||
* #1319: Fix shift selecting in file trees
|
||||
|
||||
=== Deluge 1.3.0-rc1 (08 May 2010) ===
|
||||
==== Core ====
|
||||
* Implement #1063 option to delete torrent file copy on torrent removal - patch from Ghent
|
||||
* Implement #457 progress bars for folders
|
||||
@ -7,15 +393,33 @@
|
||||
* #1112: Fix renaming files in add torrent dialog
|
||||
* #1247: Fix deluge-gtk from hanging on shutdown
|
||||
* #995: Rewrote tracker_icons
|
||||
* Add AutoAdd plugin
|
||||
* Add Notifications plugin
|
||||
|
||||
==== GtkUI ====
|
||||
* Use new SessionProxy class for caching torrent status client-side
|
||||
* Use torrent status diffs to reduce RPC traffic
|
||||
|
||||
==== Blocklist ====
|
||||
* Implement local blocklist support
|
||||
* #861: Pause transfers until blocklist is imported
|
||||
* Fix redirection not working with relative paths
|
||||
|
||||
==== Execute ====
|
||||
* Fix running commands with the TorrentAdded event
|
||||
* Fix the web interface
|
||||
|
||||
==== Label ====
|
||||
* Fix the web interface (#733)
|
||||
|
||||
==== Web ====
|
||||
* Migrate to ExtJS 3.1
|
||||
* Add gzip compression of HTTP data to the server
|
||||
* Improve the efficiency of the TorrentGrid
|
||||
* Improve the efficiency of the TorrentGrid with lots of torrents (#1026)
|
||||
* Add a base parameter to allow reverse proxying (#1076)
|
||||
* Fix showing all the peers in the details tab (#1054)
|
||||
* Fix uploading torrent files in Opera or IE (#1087)
|
||||
* Complete IE support
|
||||
|
||||
=== Deluge 1.2.0 - "Bursting like an infected kidney" (10 January 2010) ===
|
||||
==== Core ====
|
||||
|
22
DEPENDS
22
DEPENDS
@ -1,30 +1,30 @@
|
||||
=== Core ===
|
||||
* python >= 2.5
|
||||
* python >= 2.6
|
||||
* twisted >= 8.1
|
||||
* twisted-web >= 8.1
|
||||
* pyopenssl
|
||||
* simplejson (if python < 2.6)
|
||||
* setuptools
|
||||
* gettext
|
||||
* intltool
|
||||
* pyxdg
|
||||
* chardet
|
||||
* geoip-database (optional)
|
||||
* setproctitle (optional)
|
||||
* rencode >= 1.0.2 (optional), a Python port is already included
|
||||
|
||||
* libtorrent >= 0.14, or build the included version
|
||||
* libtorrent (rasterbar) >= 0.16.7
|
||||
|
||||
* If building included libtorrent::
|
||||
* boost >= 1.34.1
|
||||
* If building libtorrent:
|
||||
* boost >= 1.40
|
||||
* openssl
|
||||
* zlib
|
||||
|
||||
=== UIs ===
|
||||
* chardet
|
||||
|
||||
=== Gtk ===
|
||||
* python-notify (libnotify python wrapper)
|
||||
* pygame
|
||||
* pygtk >= 2.12
|
||||
* pygtk >= 2.16
|
||||
* librsvg
|
||||
* xdg-utils
|
||||
* python-notify (optional)
|
||||
* pygame (optional)
|
||||
|
||||
=== Web ===
|
||||
* mako
|
||||
|
29
MANIFEST.in
29
MANIFEST.in
@ -1,12 +1,23 @@
|
||||
recursive-include docs/man *
|
||||
recursive-include deluge *
|
||||
recursive-include win32 *
|
||||
include AUTHORS ChangeLog DEPENDS ez_setup.py LICENSE msgfmt.py RELEASE-VERSION version.py
|
||||
graft docs/man
|
||||
|
||||
recursive-exclude deluge *.egg-link
|
||||
exclude deluge/ui/web/gen_gettext.py
|
||||
include deluge/i18n/*.po
|
||||
|
||||
graft deluge/plugins
|
||||
recursive-exclude deluge/plugins create_dev_link.sh *.pyc
|
||||
|
||||
prune deluge/tests
|
||||
graft deluge/ui/data
|
||||
graft deluge/ui/gtkui/glade
|
||||
|
||||
include deluge/ui/web/index.html
|
||||
include deluge/ui/web/gettext.js
|
||||
include deluge/ui/web/css/*.css
|
||||
exclude deluge/ui/web/css/*-debug.css
|
||||
exclude deluge/ui/web/js/build.sh
|
||||
exclude deluge/ui/web/js/Deluge*.js
|
||||
include deluge/ui/web/js/*.js
|
||||
exclude deluge/ui/web/js/*-debug.js
|
||||
prune deluge/ui/web/docs
|
||||
prune deluge/scripts
|
||||
exclude deluge/ui/web/gen_gettext.py
|
||||
graft deluge/ui/web/themes
|
||||
graft deluge/ui/web/render
|
||||
graft deluge/ui/web/icons
|
||||
graft deluge/ui/web/images
|
||||
|
71
README
71
README
@ -1,4 +1,3 @@
|
||||
|
||||
==========================
|
||||
Deluge BitTorrent Client
|
||||
==========================
|
||||
@ -9,86 +8,50 @@ Authors:
|
||||
Andrew Resch
|
||||
Damien Churchill
|
||||
|
||||
For past developers and contributers see: http://dev.deluge-torrent.org/wiki/About
|
||||
|
||||
==========================
|
||||
License
|
||||
==========================
|
||||
Deluge is under the GNU GPLv3 license.
|
||||
Icon data/pixmaps/deluge.svg and derivatives in data/icons are copyright
|
||||
Andrew Wedderburn and are under the GNU GPLv3.
|
||||
All other icons in data/pixmaps are copyright Andrew Resch and are under
|
||||
the GNU GPLv3.
|
||||
|
||||
==========================
|
||||
Contact/Support:
|
||||
==========================
|
||||
|
||||
We have two options available for support:
|
||||
|
||||
Our Forum, at: http://forum.deluge-torrent.org
|
||||
or
|
||||
Our IRC Channel, at #deluge on Freenode: http://freenode.net
|
||||
For contributors and past developers see:
|
||||
AUTHORS
|
||||
|
||||
==========================
|
||||
Installation Instructions:
|
||||
==========================
|
||||
|
||||
For more detailed instructions see: http://dev.deluge-torrent.org/wiki/Installing/Source
|
||||
For detailed instructions see: http://dev.deluge-torrent.org/wiki/Installing/Source
|
||||
|
||||
See: DEPENDS for a full list of dependencies.
|
||||
Ensure build dependencies are installed, see DEPENDS for a full listing.
|
||||
|
||||
First, make sure you have the proper build dependencies installed. On a normal
|
||||
Debian or Ubuntu system:
|
||||
Build and install by running:
|
||||
|
||||
sudo apt-get install g++ make python-all-dev python-all python-dbus \
|
||||
python-gtk2 python-notify librsvg2-common python-xdg python-support \
|
||||
subversion libboost-dev libboost-python-dev \
|
||||
libboost-thread-dev libboost-date-time-dev libboost-filesystem-dev \
|
||||
libssl-dev zlib1g-dev python-setuptools \
|
||||
python-mako python-twisted-web python-chardet python-simplejson
|
||||
$ python setup.py build
|
||||
$ sudo python setup.py install
|
||||
|
||||
The names of the packages may vary depending on your OS / distro.
|
||||
==========================
|
||||
Contact/Support:
|
||||
==========================
|
||||
|
||||
Once you have the needed libraries installed, build and install by running:
|
||||
|
||||
$ python setup.py build
|
||||
$ sudo python setup.py install
|
||||
Forum: http://forum.deluge-torrent.org
|
||||
IRC Channel: #deluge on irc.freenode.net
|
||||
|
||||
==========================
|
||||
FAQ
|
||||
==========================
|
||||
|
||||
For the full FAQ see: http://dev.deluge-torrent.org/wiki/Faq
|
||||
|
||||
How to start the various user-interfaces
|
||||
|
||||
Gtk:
|
||||
deluge-gtk
|
||||
deluge or deluge-gtk
|
||||
Console:
|
||||
deluge-console
|
||||
Web:
|
||||
deluge-web
|
||||
Go to http://localhost:8112/ default-password = "deluge"
|
||||
|
||||
Why is deluge still listed in my system tray even after I close it ?
|
||||
|
||||
You closed the gtk user-interface but you did not close the daemon. Choose "Quit & Shutdown Daemon" to close both Daemon and gtk-ui.
|
||||
|
||||
|
||||
How do I start the daemon?
|
||||
|
||||
deluged
|
||||
|
||||
How do I start the daemon with logging to console?
|
||||
|
||||
deluged -d -L <log level>
|
||||
|
||||
I can't connect to the daemon from another machine
|
||||
|
||||
* Configure the daemon to allow remote connections
|
||||
* Add a user to the auth file located in the config folder: ~/.config/deluge/auth
|
||||
* Restart the daemon.
|
||||
See: http://dev.deluge-torrent.org/wiki/UserGuide/ThinClient
|
||||
|
||||
I upgraded from 0.5 and plugin x is missing
|
||||
|
||||
1.0 is a rewrite, all old 0.5 plugins have to be rewritten.
|
||||
|
||||
For the full FAQ see: http://dev.deluge-torrent.org/wiki/Faq
|
||||
|
21
check_glade.sh
Executable file
21
check_glade.sh
Executable file
@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Fixes glade files which may have set gtk stock labels set to translatable
|
||||
for x in `find . -name '*.glade' |grep -v '.git\|build'` ; do \
|
||||
for y in gtk-add gtk-apply gtk-bold gtk-cancel gtk-cdrom gtk-clear \
|
||||
gtk-close gtk-color-picker gtk-connect gtk-convert gtk-copy gtk-cut \
|
||||
gtk-delete gtk-dialog-error gtk-dialog-info gtk-dialog-question \
|
||||
gtk-dialog-warning gtk-dnd gtk-dnd-multiple gtk-edit gtk-execute gtk-find \
|
||||
gtk-find-and-replace gtk-floppy gtk-goto-bottom gtk-goto-first \
|
||||
gtk-goto-last gtk-goto-top gtk-go-back gtk-go-down gtk-go-forward \
|
||||
gtk-go-up gtk-help gtk-home gtk-index gtk-italic gtk-jump-to \
|
||||
gtk-justify-center gtk-justify-fill gtk-justify-left gtk-missing-image \
|
||||
gtk-new gtk-no gtk-ok gtk-open gtk-paste gtk-preferences gtk-print \
|
||||
gtk-print-preview gtk-properties gtk-quit gtk-redo gtk-refresh \
|
||||
gtk-remove gtk-revert-to-saved gtk-save gtk-save-as gtk-select-color \
|
||||
gtk-select-font gtk-sort-descending gtk-spell-check gtk-stop \
|
||||
gtk-strikethrough gtk-undelete gtk-underline gtk-undo gtk-yes \
|
||||
gtk-zoom-100 gtk-zoom-fit gtk-zoom-in gtk-zoom-out; do \
|
||||
sed -i "s/<property\ name\=\"label\"\ translatable\=\"yes\">$y<\/property>/<property\ name\=\"label\"\ translatable\=\"no\">$y<\/property>/g" $x; \
|
||||
done;\
|
||||
done
|
17
create_potfiles_in.py
Normal file → Executable file
17
create_potfiles_in.py
Normal file → Executable file
@ -1,17 +1,26 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
|
||||
import re
|
||||
import sys
|
||||
# Paths to exclude
|
||||
EXCLUSIONS = [
|
||||
"deluge/scripts"
|
||||
"deluge/scripts",
|
||||
"deluge/i18n",
|
||||
]
|
||||
|
||||
POTFILE_IN = "deluge/i18n/POTFILES.in"
|
||||
|
||||
print "Creating " + POTFILE_IN + " .."
|
||||
pattern = "deluge\/plugins\/.*\/build"
|
||||
compiled = re.compile(pattern)
|
||||
|
||||
sys.stdout.write("Creating " + POTFILE_IN + " ... ")
|
||||
sys.stdout.flush()
|
||||
to_translate = []
|
||||
for (dirpath, dirnames, filenames) in os.walk("deluge"):
|
||||
for filename in filenames:
|
||||
if os.path.splitext(filename)[1] in (".py", ".glade") and dirpath not in EXCLUSIONS:
|
||||
if os.path.splitext(filename)[1] in (".py", ".glade", ".in") \
|
||||
and dirpath not in EXCLUSIONS \
|
||||
and not compiled.match(dirpath):
|
||||
to_translate.append(os.path.join(dirpath, filename))
|
||||
|
||||
f = open(POTFILE_IN, "wb")
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
for size in 16 22 24 32 36 48 64 72 96 128 192 256; do mkdir -p deluge/data/\
|
||||
for size in 16 22 24 32 36 48 64 72 96 128 192 256; do mkdir -p deluge/ui/data/\
|
||||
icons/hicolor/${size}x${size}/apps; rsvg-convert -w ${size} -h ${size} \
|
||||
-o deluge/data/icons/hicolor/${size}x${size}/apps/deluge.png deluge/data/pixmaps\
|
||||
/deluge.svg; mkdir -p deluge/data/icons/scalable/apps/; cp deluge/data/pixmaps/\
|
||||
deluge.svg deluge/data/icons/scalable/apps/deluge.svg; done
|
||||
-o deluge/ui/data/icons/hicolor/${size}x${size}/apps/deluge.png deluge/ui/data/pixmaps\
|
||||
/deluge.svg; mkdir -p deluge/ui/data/icons/scalable/apps/; cp deluge/ui/data/pixmaps/\
|
||||
deluge.svg deluge/ui/data/icons/scalable/apps/deluge.svg; done
|
||||
|
@ -1 +1,4 @@
|
||||
"""Deluge"""
|
||||
# this is a namespace package
|
||||
import pkg_resources
|
||||
pkg_resources.declare_namespace(__name__)
|
||||
|
@ -45,9 +45,9 @@ supports.
|
||||
|
||||
"""
|
||||
|
||||
REQUIRED_VERSION = "0.14.9.0"
|
||||
REQUIRED_VERSION = "0.16.7.0"
|
||||
|
||||
def check_version(LT):
|
||||
def check_version(lt):
|
||||
from deluge.common import VersionSplit
|
||||
if VersionSplit(lt.version) < VersionSplit(REQUIRED_VERSION):
|
||||
raise ImportError("This version of Deluge requires libtorrent >=%s!" % REQUIRED_VERSION)
|
||||
|
393
deluge/common.py
393
deluge/common.py
@ -17,9 +17,9 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with deluge. If not, write to:
|
||||
# The Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor
|
||||
# Boston, MA 02110-1301, USA.
|
||||
# The Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# In addition, as a special exception, the copyright holders give
|
||||
# permission to link the code of portions of this program with the OpenSSL
|
||||
@ -37,16 +37,25 @@
|
||||
"""Common functions for various parts of Deluge to use."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import subprocess
|
||||
import platform
|
||||
import sys
|
||||
import chardet
|
||||
import logging
|
||||
import pkg_resources
|
||||
import gettext
|
||||
import locale
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
from deluge.error import *
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Do a little hack here just in case the user has json-py installed since it
|
||||
# has a different api
|
||||
if not hasattr(json, "dumps"):
|
||||
@ -62,29 +71,6 @@ if not hasattr(json, "dumps"):
|
||||
json.dump = dump
|
||||
json.load = load
|
||||
|
||||
import pkg_resources
|
||||
import xdg, xdg.BaseDirectory
|
||||
import gettext
|
||||
import locale
|
||||
|
||||
# Initialize gettext
|
||||
try:
|
||||
if hasattr(locale, "bindtextdomain"):
|
||||
locale.bindtextdomain("deluge", pkg_resources.resource_filename("deluge", "i18n"))
|
||||
if hasattr(locale, "textdomain"):
|
||||
locale.textdomain("deluge")
|
||||
gettext.bindtextdomain("deluge", pkg_resources.resource_filename("deluge", "i18n"))
|
||||
gettext.textdomain("deluge")
|
||||
gettext.install("deluge", pkg_resources.resource_filename("deluge", "i18n"))
|
||||
except Exception, e:
|
||||
from deluge.log import LOG as log
|
||||
log.error("Unable to initialize gettext/locale!")
|
||||
log.exception(e)
|
||||
import __builtin__
|
||||
__builtin__.__dict__["_"] = lambda x: x
|
||||
|
||||
from deluge.error import *
|
||||
|
||||
LT_TORRENT_STATE = {
|
||||
"Queued": 0,
|
||||
"Checking": 1,
|
||||
@ -104,7 +90,6 @@ LT_TORRENT_STATE = {
|
||||
7: "Checking Resume Data"
|
||||
}
|
||||
|
||||
|
||||
TORRENT_STATE = [
|
||||
"Allocating",
|
||||
"Checking",
|
||||
@ -119,11 +104,15 @@ FILE_PRIORITY = {
|
||||
0: "Do Not Download",
|
||||
1: "Normal Priority",
|
||||
2: "High Priority",
|
||||
5: "Highest Priority",
|
||||
3: "High Priority",
|
||||
4: "High Priority",
|
||||
5: "High Priority",
|
||||
6: "High Priority",
|
||||
7: "Highest Priority",
|
||||
"Do Not Download": 0,
|
||||
"Normal Priority": 1,
|
||||
"High Priority": 2,
|
||||
"Highest Priority": 5
|
||||
"High Priority": 5,
|
||||
"Highest Priority": 7
|
||||
}
|
||||
|
||||
def get_version():
|
||||
@ -144,16 +133,26 @@ def get_default_config_dir(filename=None):
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
|
||||
if windows_check():
|
||||
if filename:
|
||||
return os.path.join(os.environ.get("APPDATA"), "deluge", filename)
|
||||
else:
|
||||
return os.path.join(os.environ.get("APPDATA"), "deluge")
|
||||
def save_config_path(resource):
|
||||
appDataPath = os.environ.get("APPDATA")
|
||||
if not appDataPath:
|
||||
import _winreg
|
||||
hkey = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders")
|
||||
appDataReg = _winreg.QueryValueEx(hkey, "AppData")
|
||||
appDataPath = appDataReg[0]
|
||||
_winreg.CloseKey(hkey)
|
||||
return os.path.join(appDataPath, resource)
|
||||
else:
|
||||
if filename:
|
||||
return os.path.join(xdg.BaseDirectory.save_config_path("deluge"), filename)
|
||||
else:
|
||||
return xdg.BaseDirectory.save_config_path("deluge")
|
||||
from xdg.BaseDirectory import save_config_path
|
||||
if not filename:
|
||||
filename = ''
|
||||
try:
|
||||
return os.path.join(save_config_path("deluge"), filename)
|
||||
except OSError, e:
|
||||
log.error("Unable to use default config directory, exiting... (%s)", e)
|
||||
sys.exit(1)
|
||||
|
||||
def get_default_download_dir():
|
||||
"""
|
||||
@ -162,8 +161,20 @@ def get_default_download_dir():
|
||||
|
||||
"""
|
||||
if windows_check():
|
||||
return os.path.expanduser("~")
|
||||
return os.path.join(os.path.expanduser("~"), 'Downloads')
|
||||
else:
|
||||
from xdg.BaseDirectory import xdg_config_home
|
||||
userdir_file = os.path.join(xdg_config_home, 'user-dirs.dirs')
|
||||
try:
|
||||
for line in open(userdir_file, 'r'):
|
||||
if not line.startswith('#') and 'XDG_DOWNLOAD_DIR' in line:
|
||||
download_dir = os.path.expandvars(\
|
||||
line.partition("=")[2].rstrip().strip('"'))
|
||||
if os.path.isdir(download_dir):
|
||||
return download_dir
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
return os.environ.get("HOME")
|
||||
|
||||
def windows_check():
|
||||
@ -198,7 +209,7 @@ def osx_check():
|
||||
|
||||
def get_pixmap(fname):
|
||||
"""
|
||||
Provides easy access to files in the deluge/data/pixmaps folder within the Deluge egg
|
||||
Provides easy access to files in the deluge/ui/data/pixmaps folder within the Deluge egg
|
||||
|
||||
:param fname: the filename to look for
|
||||
:type fname: string
|
||||
@ -206,8 +217,18 @@ def get_pixmap(fname):
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
return pkg_resources.resource_filename("deluge", os.path.join("data", \
|
||||
"pixmaps", fname))
|
||||
return resource_filename("deluge", os.path.join("ui", "data", "pixmaps", fname))
|
||||
|
||||
def resource_filename(module, path):
|
||||
# While developing, if there's a second deluge package, installed globally
|
||||
# and another in develop mode somewhere else, while pkg_resources.require("Deluge")
|
||||
# returns the proper deluge instance, pkg_resources.resource_filename does
|
||||
# not, it returns the first found on the python path, which is not good
|
||||
# enough.
|
||||
# This is a work-around that.
|
||||
return pkg_resources.require("Deluge>=%s" % get_version())[0].get_resource_filename(
|
||||
pkg_resources._manager, os.path.join(*(module.split('.')+[path]))
|
||||
)
|
||||
|
||||
def open_file(path):
|
||||
"""
|
||||
@ -218,7 +239,9 @@ def open_file(path):
|
||||
|
||||
"""
|
||||
if windows_check():
|
||||
os.startfile("%s" % path)
|
||||
os.startfile(path.decode("utf8"))
|
||||
elif osx_check():
|
||||
subprocess.Popen(["open", "%s" % path])
|
||||
else:
|
||||
subprocess.Popen(["xdg-open", "%s" % path])
|
||||
|
||||
@ -259,6 +282,30 @@ def fsize(fsize_b):
|
||||
fsize_gb = fsize_mb / 1024.0
|
||||
return "%.1f %s" % (fsize_gb, _("GiB"))
|
||||
|
||||
def fsize_short(fsize_b):
|
||||
"""
|
||||
Formats the bytes value into a string with K, M or G units
|
||||
|
||||
:param fsize_b: the filesize in bytes
|
||||
:type fsize_b: int
|
||||
:returns: formatted string in K, M or G units
|
||||
:rtype: string
|
||||
|
||||
**Usage**
|
||||
|
||||
>>> fsize(112245)
|
||||
'109.6 K'
|
||||
|
||||
"""
|
||||
fsize_kb = fsize_b / 1024.0
|
||||
if fsize_kb < 1024:
|
||||
return "%.1f %s" % (fsize_kb, _("K"))
|
||||
fsize_mb = fsize_kb / 1024.0
|
||||
if fsize_mb < 1024:
|
||||
return "%.1f %s" % (fsize_mb, _("M"))
|
||||
fsize_gb = fsize_mb / 1024.0
|
||||
return "%.1f %s" % (fsize_gb, _("G"))
|
||||
|
||||
def fpcnt(dec):
|
||||
"""
|
||||
Formats a string to display a percentage with two decimal places
|
||||
@ -291,7 +338,14 @@ def fspeed(bps):
|
||||
'42.1 KiB/s'
|
||||
|
||||
"""
|
||||
return '%s/s' % (fsize(bps))
|
||||
fspeed_kb = bps / 1024.0
|
||||
if fspeed_kb < 1024:
|
||||
return "%.1f %s" % (fspeed_kb, _("KiB/s"))
|
||||
fspeed_mb = fspeed_kb / 1024.0
|
||||
if fspeed_mb < 1024:
|
||||
return "%.1f %s" % (fspeed_mb, _("MiB/s"))
|
||||
fspeed_gb = fspeed_mb / 1024.0
|
||||
return "%.1f %s" % (fspeed_gb, _("GiB/s"))
|
||||
|
||||
def fpeer(num_peers, total_peers):
|
||||
"""
|
||||
@ -402,7 +456,9 @@ def is_magnet(uri):
|
||||
True
|
||||
|
||||
"""
|
||||
if uri[:20] == "magnet:?xt=urn:btih:":
|
||||
magnet_scheme = 'magnet:?'
|
||||
xt_param = 'xt=urn:btih:'
|
||||
if uri.startswith(magnet_scheme) and xt_param in uri:
|
||||
return True
|
||||
return False
|
||||
|
||||
@ -470,12 +526,11 @@ def free_space(path):
|
||||
raise InvalidPathError("%s is not a valid path" % path)
|
||||
|
||||
if windows_check():
|
||||
import win32file
|
||||
sectors, bytes, free, total = map(long, win32file.GetDiskFreeSpace(path))
|
||||
return (free * sectors * bytes)
|
||||
from win32file import GetDiskFreeSpaceEx
|
||||
return GetDiskFreeSpaceEx(path)[0]
|
||||
else:
|
||||
disk_data = os.statvfs(path)
|
||||
block_size = disk_data.f_bsize
|
||||
disk_data = os.statvfs(path.encode("utf8"))
|
||||
block_size = disk_data.f_frsize
|
||||
return disk_data.f_bavail * block_size
|
||||
|
||||
def is_ip(ip):
|
||||
@ -496,15 +551,23 @@ def is_ip(ip):
|
||||
import socket
|
||||
#first we test ipv4
|
||||
try:
|
||||
if socket.inet_pton(socket.AF_INET, "%s" % (ip)):
|
||||
return True
|
||||
if windows_check():
|
||||
if socket.inet_aton("%s" % (ip)):
|
||||
return True
|
||||
else:
|
||||
if socket.inet_pton(socket.AF_INET, "%s" % (ip)):
|
||||
return True
|
||||
except socket.error:
|
||||
if not socket.has_ipv6:
|
||||
return False
|
||||
#now test ipv6
|
||||
try:
|
||||
if socket.inet_pton(socket.AF_INET6, "%s" % (ip)):
|
||||
if windows_check():
|
||||
log.warning("ipv6 check unavailable on windows")
|
||||
return True
|
||||
else:
|
||||
if socket.inet_pton(socket.AF_INET6, "%s" % (ip)):
|
||||
return True
|
||||
except socket.error:
|
||||
return False
|
||||
|
||||
@ -526,7 +589,7 @@ def path_join(*parts):
|
||||
path += '/' + part
|
||||
return path
|
||||
|
||||
XML_ESCAPES = (
|
||||
XML_ESCAPES = (
|
||||
('&', '&'),
|
||||
('<', '<'),
|
||||
('>', '>'),
|
||||
@ -535,9 +598,9 @@ XML_ESCAPES = (
|
||||
)
|
||||
|
||||
def xml_decode(string):
|
||||
"""
|
||||
"""
|
||||
Unescape a string that was previously encoded for use within xml.
|
||||
|
||||
|
||||
:param string: The string to escape
|
||||
:type string: string
|
||||
:returns: The unescaped version of the string.
|
||||
@ -548,9 +611,9 @@ def xml_decode(string):
|
||||
return string
|
||||
|
||||
def xml_encode(string):
|
||||
"""
|
||||
"""
|
||||
Escape a string for use within an xml element or attribute.
|
||||
|
||||
|
||||
:param string: The string to escape
|
||||
:type string: string
|
||||
:returns: An escaped version of the string.
|
||||
@ -560,6 +623,59 @@ def xml_encode(string):
|
||||
string = string.replace(char, escape)
|
||||
return string
|
||||
|
||||
def decode_string(s, encoding="utf8"):
|
||||
"""
|
||||
Decodes a string and return unicode. If it cannot decode using
|
||||
`:param:encoding` then it will try latin1, and if that fails,
|
||||
try to detect the string encoding. If that fails, decode with
|
||||
ignore.
|
||||
|
||||
:param s: string to decode
|
||||
:type s: string
|
||||
:keyword encoding: the encoding to use in the decoding
|
||||
:type encoding: string
|
||||
:returns: s converted to unicode
|
||||
:rtype: unicode
|
||||
|
||||
"""
|
||||
if not s:
|
||||
return u''
|
||||
elif isinstance(s, unicode):
|
||||
return s
|
||||
|
||||
encodings = [lambda: ("utf8", 'strict'),
|
||||
lambda: ("iso-8859-1", 'strict'),
|
||||
lambda: (chardet.detect(s)["encoding"], 'strict'),
|
||||
lambda: (encoding, 'ignore')]
|
||||
|
||||
if not encoding is "utf8":
|
||||
encodings.insert(0, lambda: (encoding, 'strict'))
|
||||
|
||||
for l in encodings:
|
||||
try:
|
||||
return s.decode(*l())
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
return u''
|
||||
|
||||
def utf8_encoded(s, encoding="utf8"):
|
||||
"""
|
||||
Returns a utf8 encoded string of s
|
||||
|
||||
:param s: (unicode) string to (re-)encode
|
||||
:type s: basestring
|
||||
:keyword encoding: the encoding to use in the decoding
|
||||
:type encoding: string
|
||||
:returns: a utf8 encoded string of s
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
if isinstance(s, str):
|
||||
s = decode_string(s, encoding).encode("utf8")
|
||||
elif isinstance(s, unicode):
|
||||
s = s.encode("utf8")
|
||||
return s
|
||||
|
||||
class VersionSplit(object):
|
||||
"""
|
||||
Used for comparing version numbers.
|
||||
@ -569,14 +685,36 @@ class VersionSplit(object):
|
||||
|
||||
"""
|
||||
def __init__(self, ver):
|
||||
ver = ver.lower()
|
||||
vs = ver.split("_") if "_" in ver else ver.split("-")
|
||||
import re
|
||||
VERSION_RE = re.compile(r'''
|
||||
^
|
||||
(?P<version>\d+\.\d+) # minimum 'N.N'
|
||||
(?P<extraversion>(?:\.\d+)*) # any number of extra '.N' segments
|
||||
(?:
|
||||
(?P<prerel>[abc]|rc) # 'a'=alpha, 'b'=beta, 'c'=release candidate
|
||||
# 'rc'= alias for release candidate
|
||||
(?P<prerelversion>\d+(?:\.\d+)*)
|
||||
)?
|
||||
(?P<postdev>(\.post(?P<post>\d+))?(\.dev(?P<dev>\d+))?)?
|
||||
$''', re.VERBOSE)
|
||||
|
||||
# Check for PEP 386 compliant version
|
||||
match = re.search(VERSION_RE, ver)
|
||||
if match:
|
||||
group = [(x if x is not None else '') for x in match.group(1,2,3,4,8)]
|
||||
vs = [''.join(group[0:2]),''.join(group[2:4]), group[4].lstrip('.')]
|
||||
else:
|
||||
ver = ver.lower()
|
||||
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", "a", "b", "c")):
|
||||
self.suffix = vs[1]
|
||||
if vs[-1].startswith('dev'):
|
||||
self.dev = vs[-1]
|
||||
|
||||
def __cmp__(self, ver):
|
||||
"""
|
||||
@ -586,20 +724,117 @@ class VersionSplit(object):
|
||||
:type ver: VersionSplit
|
||||
|
||||
"""
|
||||
# PEP 386 versions with .devN precede release version
|
||||
if (bool(self.dev) != bool(ver.dev)):
|
||||
if self.dev != 'dev':
|
||||
self.dev = not self.dev
|
||||
if ver.dev != 'dev':
|
||||
ver.dev = not ver.dev
|
||||
|
||||
if self.version > ver.version or (self.suffix and self.suffix[:3] == "dev"):
|
||||
return 1
|
||||
if self.version < ver.version:
|
||||
return -1
|
||||
# If there is no suffix we use z because we want final
|
||||
# to appear after alpha, beta, and rc alphabetically.
|
||||
v1 = [self.version, self.suffix or 'z', self.dev]
|
||||
v2 = [ver.version, ver.suffix or 'z', ver.dev]
|
||||
return cmp(v1, v2)
|
||||
|
||||
if self.version == ver.version:
|
||||
if self.suffix == ver.suffix:
|
||||
return 0
|
||||
if self.suffix is None:
|
||||
return 1
|
||||
if ver.suffix is None:
|
||||
return -1
|
||||
if self.suffix < ver.suffix:
|
||||
return -1
|
||||
if self.suffix > ver.suffix:
|
||||
return 1
|
||||
|
||||
# Common AUTH stuff
|
||||
AUTH_LEVEL_NONE = 0
|
||||
AUTH_LEVEL_READONLY = 1
|
||||
AUTH_LEVEL_NORMAL = 5
|
||||
AUTH_LEVEL_ADMIN = 10
|
||||
AUTH_LEVEL_DEFAULT = AUTH_LEVEL_NORMAL
|
||||
|
||||
def create_auth_file():
|
||||
import stat, configmanager
|
||||
auth_file = configmanager.get_config_dir("auth")
|
||||
# Check for auth file and create if necessary
|
||||
if not os.path.exists(auth_file):
|
||||
fd = open(auth_file, "w")
|
||||
fd.flush()
|
||||
os.fsync(fd.fileno())
|
||||
fd.close()
|
||||
# Change the permissions on the file so only this user can read/write it
|
||||
os.chmod(auth_file, stat.S_IREAD | stat.S_IWRITE)
|
||||
|
||||
def create_localclient_account(append=False):
|
||||
import configmanager, random
|
||||
auth_file = configmanager.get_config_dir("auth")
|
||||
if not os.path.exists(auth_file):
|
||||
create_auth_file()
|
||||
|
||||
try:
|
||||
from hashlib import sha1 as sha_hash
|
||||
except ImportError:
|
||||
from sha import new as sha_hash
|
||||
fd = open(auth_file, "a" if append else "w")
|
||||
fd.write(":".join([
|
||||
"localclient",
|
||||
sha_hash(str(random.random())).hexdigest(),
|
||||
str(AUTH_LEVEL_ADMIN)
|
||||
]) + '\n')
|
||||
fd.flush()
|
||||
os.fsync(fd.fileno())
|
||||
fd.close()
|
||||
|
||||
|
||||
# Initialize gettext
|
||||
def setup_translations(setup_pygtk=False):
|
||||
translations_path = resource_filename("deluge", "i18n")
|
||||
log.info("Setting up translations from %s", translations_path)
|
||||
|
||||
try:
|
||||
if hasattr(locale, "bindtextdomain"):
|
||||
locale.bindtextdomain("deluge", translations_path)
|
||||
if hasattr(locale, "textdomain"):
|
||||
locale.textdomain("deluge")
|
||||
gettext.install("deluge", translations_path, unicode=True)
|
||||
if setup_pygtk:
|
||||
# Even though we're not using glade anymore, let's set it up so that
|
||||
# plugins still using it get properly translated.
|
||||
log.info("Setting up GTK translations from %s", translations_path)
|
||||
import gtk
|
||||
import gtk.glade
|
||||
gtk.glade.bindtextdomain("deluge", translations_path)
|
||||
gtk.glade.textdomain("deluge")
|
||||
except Exception, e:
|
||||
log.error("Unable to initialize gettext/locale!")
|
||||
log.exception(e)
|
||||
import __builtin__
|
||||
__builtin__.__dict__["_"] = lambda x: x
|
||||
|
||||
def unicode_argv():
|
||||
""" Gets sys.argv as list of unicode objects on any platform."""
|
||||
if windows_check():
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
else:
|
||||
# On other platforms, we have to find the likely encoding of the args and decode
|
||||
# First check if sys.stdout or stdin have encoding set
|
||||
encoding = getattr(sys.stdout, "encoding") or getattr(sys.stdin, "encoding")
|
||||
# If that fails, check what the locale is set to
|
||||
encoding = encoding or locale.getpreferredencoding()
|
||||
# As a last resort, just default to utf-8
|
||||
encoding = encoding or "utf-8"
|
||||
|
||||
return [arg.decode(encoding) for arg in sys.argv]
|
||||
|
@ -33,9 +33,12 @@
|
||||
#
|
||||
#
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from twisted.internet.defer import maybeDeferred, succeed, DeferredList, fail
|
||||
from twisted.internet.task import LoopingCall
|
||||
from deluge.log import LOG as log
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class ComponentAlreadyRegistered(Exception):
|
||||
pass
|
||||
@ -96,6 +99,10 @@ class Component(object):
|
||||
self._component_stopping_deferred = None
|
||||
_ComponentRegistry.register(self)
|
||||
|
||||
def __del__(self):
|
||||
if _ComponentRegistry:
|
||||
_ComponentRegistry.deregister(self)
|
||||
|
||||
def _component_start_timer(self):
|
||||
if hasattr(self, "update"):
|
||||
self._component_timer = LoopingCall(self.update)
|
||||
@ -139,11 +146,18 @@ class Component(object):
|
||||
self._component_timer.stop()
|
||||
return True
|
||||
|
||||
def on_stop_fail(result):
|
||||
self._component_state = "Started"
|
||||
self._component_stopping_deferred = None
|
||||
log.error(result)
|
||||
return result
|
||||
|
||||
if self._component_state != "Stopped" and self._component_state != "Stopping":
|
||||
if hasattr(self, "stop"):
|
||||
self._component_state = "Stopping"
|
||||
d = maybeDeferred(self.stop)
|
||||
d.addCallback(on_stop)
|
||||
d.addErrback(on_stop_fail)
|
||||
self._component_stopping_deferred = d
|
||||
else:
|
||||
d = maybeDeferred(on_stop, None)
|
||||
@ -192,6 +206,18 @@ class Component(object):
|
||||
d.addCallback(on_stop)
|
||||
return d
|
||||
|
||||
def start(self):
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
pass
|
||||
|
||||
def update(self):
|
||||
pass
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
class ComponentRegistry(object):
|
||||
"""
|
||||
The ComponentRegistry holds a list of currently registered
|
||||
@ -200,6 +226,8 @@ class ComponentRegistry(object):
|
||||
"""
|
||||
def __init__(self):
|
||||
self.components = {}
|
||||
# Stores all of the components that are dependent on a particular component
|
||||
self.dependents = defaultdict(list)
|
||||
|
||||
def register(self, obj):
|
||||
"""
|
||||
@ -218,23 +246,26 @@ class ComponentRegistry(object):
|
||||
"Component already registered with name %s" % name)
|
||||
|
||||
self.components[obj._component_name] = obj
|
||||
if obj._component_depend:
|
||||
for depend in obj._component_depend:
|
||||
self.dependents[depend].append(name)
|
||||
|
||||
def deregister(self, name):
|
||||
def deregister(self, obj):
|
||||
"""
|
||||
Deregisters a component from the registry. A stop will be
|
||||
issued to the component prior to deregistering it.
|
||||
|
||||
:param name: the name of the component
|
||||
:type name: string
|
||||
:param obj: the Component object
|
||||
:type obj: object
|
||||
|
||||
"""
|
||||
|
||||
if name in self.components:
|
||||
log.debug("Deregistering Component: %s", name)
|
||||
d = self.stop([name])
|
||||
if obj in self.components.values():
|
||||
log.debug("Deregistering Component: %s", obj._component_name)
|
||||
d = self.stop([obj._component_name])
|
||||
def on_stop(result, name):
|
||||
del self.components[name]
|
||||
return d.addCallback(on_stop, name)
|
||||
return d.addCallback(on_stop, obj._component_name)
|
||||
else:
|
||||
return succeed(None)
|
||||
|
||||
@ -292,11 +323,23 @@ class ComponentRegistry(object):
|
||||
elif isinstance(names, str):
|
||||
names = [names]
|
||||
|
||||
def on_dependents_stopped(result, name):
|
||||
return self.components[name]._component_stop()
|
||||
|
||||
stopped_in_deferred = set()
|
||||
deferreds = []
|
||||
|
||||
for name in names:
|
||||
if name in stopped_in_deferred:
|
||||
continue
|
||||
if name in self.components:
|
||||
deferreds.append(self.components[name]._component_stop())
|
||||
if name in self.dependents:
|
||||
# If other components depend on this component, stop them first
|
||||
d = self.stop(self.dependents[name]).addCallback(on_dependents_stopped, name)
|
||||
deferreds.append(d)
|
||||
stopped_in_deferred.update(self.dependents[name])
|
||||
else:
|
||||
deferreds.append(self.components[name]._component_stop())
|
||||
|
||||
return DeferredList(deferreds)
|
||||
|
||||
@ -335,7 +378,7 @@ class ComponentRegistry(object):
|
||||
: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
|
||||
:returns: a Deferred object that will fire once all Components have been successfully resumed
|
||||
:rtype: twisted.internet.defer.Deferred
|
||||
|
||||
"""
|
||||
@ -359,16 +402,14 @@ class ComponentRegistry(object):
|
||||
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
|
||||
:returns: a Deferred object that will fire once all Components have been successfully shut down
|
||||
:rtype: twisted.internet.defer.Deferred
|
||||
|
||||
"""
|
||||
deferreds = []
|
||||
def on_stopped(result):
|
||||
return DeferredList(map(lambda c: c._component_shutdown(), self.components.values()))
|
||||
|
||||
for component in self.components.values():
|
||||
deferreds.append(component._component_shutdown())
|
||||
|
||||
return DeferredList(deferreds)
|
||||
return self.stop(self.components.keys()).addCallback(on_stopped)
|
||||
|
||||
def update(self):
|
||||
"""
|
||||
|
@ -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
|
||||
@ -68,14 +68,16 @@ version as this will be done internally.
|
||||
"""
|
||||
|
||||
import cPickle as pickle
|
||||
import logging
|
||||
import shutil
|
||||
import os
|
||||
|
||||
import deluge.common
|
||||
from deluge.log import LOG as log
|
||||
|
||||
json = deluge.common.json
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def prop(func):
|
||||
"""Function decorator for defining property attributes
|
||||
|
||||
@ -93,13 +95,13 @@ def prop(func):
|
||||
def find_json_objects(s):
|
||||
"""
|
||||
Find json objects in a string.
|
||||
|
||||
|
||||
:param s: the string to find json objects in
|
||||
:type s: string
|
||||
|
||||
|
||||
:returns: a list of tuples containing start and end locations of json objects in the string `s`
|
||||
:rtype: [(start, end), ...]
|
||||
|
||||
|
||||
"""
|
||||
objects = []
|
||||
opens = 0
|
||||
@ -119,8 +121,8 @@ def find_json_objects(s):
|
||||
start = index + offset + 1
|
||||
|
||||
return objects
|
||||
|
||||
|
||||
|
||||
|
||||
class Config(object):
|
||||
"""
|
||||
This class is used to access/create/modify config files
|
||||
@ -146,7 +148,8 @@ class Config(object):
|
||||
self._save_timer = None
|
||||
|
||||
if defaults:
|
||||
self.__config = dict(defaults)
|
||||
for key, value in defaults.iteritems():
|
||||
self.set_item(key, value)
|
||||
|
||||
# Load the config from file in the config_dir
|
||||
if config_dir:
|
||||
@ -187,6 +190,10 @@ what is currently in the config and it could not convert the value
|
||||
5
|
||||
|
||||
"""
|
||||
if isinstance(value, basestring):
|
||||
value = deluge.common.utf8_encoded(value)
|
||||
|
||||
|
||||
if not self.__config.has_key(key):
|
||||
self.__config[key] = value
|
||||
log.debug("Setting '%s' to %s of %s", key, value, type(value))
|
||||
@ -200,7 +207,10 @@ what is currently in the config and it could not convert the value
|
||||
|
||||
if value is not None and oldtype != type(None) and oldtype != newtype:
|
||||
try:
|
||||
value = oldtype(value)
|
||||
if oldtype == unicode:
|
||||
value = oldtype(value, "utf8")
|
||||
else:
|
||||
value = oldtype(value)
|
||||
except ValueError:
|
||||
log.warning("Type '%s' invalid for '%s'", newtype, key)
|
||||
raise
|
||||
@ -250,7 +260,38 @@ what is currently in the config and it could not convert the value
|
||||
5
|
||||
|
||||
"""
|
||||
return self.__config[key]
|
||||
if isinstance(self.__config[key], str):
|
||||
try:
|
||||
return self.__config[key].decode("utf8")
|
||||
except UnicodeDecodeError:
|
||||
return self.__config[key]
|
||||
else:
|
||||
return self.__config[key]
|
||||
|
||||
def __delitem__(self, key):
|
||||
"""
|
||||
See
|
||||
:meth:`del_item`
|
||||
"""
|
||||
self.del_item(key)
|
||||
|
||||
def del_item(self, key):
|
||||
"""
|
||||
Deletes item with a specific key from the configuration.
|
||||
|
||||
:param key: the item which you wish to delete.
|
||||
:raises KeyError: if 'key' is not in the config dictionary
|
||||
|
||||
**Usage**
|
||||
>>> config = Config("test.conf", defaults={"test": 5})
|
||||
>>> del config["test"]
|
||||
"""
|
||||
del self.__config[key]
|
||||
# We set the save_timer for 5 seconds if not already set
|
||||
from twisted.internet import reactor
|
||||
if not self._save_timer or not self._save_timer.active():
|
||||
self._save_timer = reactor.callLater(5, self.save)
|
||||
|
||||
|
||||
def register_change_callback(self, callback):
|
||||
"""
|
||||
@ -348,21 +389,21 @@ what is currently in the config and it could not convert the value
|
||||
return
|
||||
|
||||
objects = find_json_objects(data)
|
||||
|
||||
|
||||
if not len(objects):
|
||||
# No json objects found, try depickling it
|
||||
try:
|
||||
self.__config.update(pickle.loads(data))
|
||||
except Exception, e:
|
||||
log.exception(e)
|
||||
log.warning("Unable to load config file: %s", filename)
|
||||
log.warning("Unable to load config file: %s", filename)
|
||||
elif len(objects) == 1:
|
||||
start, end = objects[0]
|
||||
try:
|
||||
self.__config.update(json.loads(data[start:end]))
|
||||
except Exception, e:
|
||||
log.exception(e)
|
||||
log.warning("Unable to load config file: %s", filename)
|
||||
log.warning("Unable to load config file: %s", filename)
|
||||
elif len(objects) == 2:
|
||||
try:
|
||||
start, end = objects[0]
|
||||
@ -371,8 +412,8 @@ what is currently in the config and it could not convert the value
|
||||
self.__config.update(json.loads(data[start:end]))
|
||||
except Exception, e:
|
||||
log.exception(e)
|
||||
log.warning("Unable to load config file: %s", filename)
|
||||
|
||||
log.warning("Unable to load config file: %s", filename)
|
||||
|
||||
log.debug("Config %s version: %s.%s loaded: %s", filename,
|
||||
self.__version["format"], self.__version["file"], self.__config)
|
||||
|
||||
@ -396,26 +437,24 @@ what is currently in the config and it could not convert the value
|
||||
version = json.loads(data[start:end])
|
||||
start, end = objects[1]
|
||||
loaded_data = json.loads(data[start:end])
|
||||
|
||||
if self.__config == loaded_data and self.__version == version:
|
||||
# The config has not changed so lets just return
|
||||
self._save_timer.cancel()
|
||||
return
|
||||
except Exception, e:
|
||||
log.warning("Unable to open config file: %s", filename)
|
||||
|
||||
|
||||
if self._save_timer and self._save_timer.active():
|
||||
self._save_timer.cancel()
|
||||
return True
|
||||
except (IOError, IndexError), e:
|
||||
log.warning("Unable to open config file: %s because: %s", filename, e)
|
||||
|
||||
# Save the new config and make sure it's written to disk
|
||||
try:
|
||||
log.debug("Saving new config file %s", filename + ".new")
|
||||
f = open(filename + ".new", "wb")
|
||||
json.dump(self.__version, f, indent=2)
|
||||
json.dump(self.__version, f, indent=2)
|
||||
json.dump(self.__config, f, indent=2)
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
f.close()
|
||||
except Exception, e:
|
||||
except IOError, e:
|
||||
log.error("Error writing new config file: %s", e)
|
||||
return False
|
||||
|
||||
@ -424,7 +463,7 @@ what is currently in the config and it could not convert the value
|
||||
log.debug("Backing up old config file to %s~", filename)
|
||||
shutil.move(filename, filename + "~")
|
||||
except Exception, e:
|
||||
log.error("Error backing up old config..")
|
||||
log.warning("Unable to backup old config...")
|
||||
|
||||
# The new config file has been written successfully, so let's move it over
|
||||
# the existing one.
|
||||
|
@ -34,11 +34,14 @@
|
||||
#
|
||||
|
||||
import os
|
||||
import logging
|
||||
|
||||
import deluge.common
|
||||
from deluge.log import LOG as log
|
||||
import deluge.log
|
||||
from deluge.config import Config
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class _ConfigManager:
|
||||
def __init__(self):
|
||||
log.debug("ConfigManager started..")
|
||||
@ -52,7 +55,6 @@ class _ConfigManager:
|
||||
return self.__config_directory
|
||||
|
||||
def __del__(self):
|
||||
log.debug("ConfigManager stopping..")
|
||||
del self.config_files
|
||||
|
||||
def set_config_dir(self, directory):
|
||||
@ -86,6 +88,7 @@ class _ConfigManager:
|
||||
# to reload based on the new config directory
|
||||
self.save()
|
||||
self.config_files = {}
|
||||
deluge.log.tweak_logging_levels()
|
||||
|
||||
return True
|
||||
|
||||
|
@ -41,12 +41,14 @@ This should typically only be used by the Core. Plugins should utilize the
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
from twisted.internet import reactor
|
||||
|
||||
import deluge.component as component
|
||||
from deluge._libtorrent import lt
|
||||
from deluge.common import decode_string
|
||||
|
||||
from deluge.log import LOG as log
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class AlertManager(component.Component):
|
||||
def __init__(self):
|
||||
@ -65,16 +67,17 @@ class AlertManager(component.Component):
|
||||
|
||||
# handlers is a dictionary of lists {"alert_type": [handler1,h2,..]}
|
||||
self.handlers = {}
|
||||
|
||||
self.delayed_calls = []
|
||||
self.wait_on_handler = False
|
||||
|
||||
def update(self):
|
||||
self.delayed_calls = [dc for dc in self.delayed_calls if dc.active()]
|
||||
self.handle_alerts()
|
||||
self.handle_alerts(wait=self.wait_on_handler)
|
||||
|
||||
def stop(self):
|
||||
for dc in self.delayed_calls:
|
||||
dc.cancel()
|
||||
if dc.active():
|
||||
dc.cancel()
|
||||
self.delayed_calls = []
|
||||
|
||||
def register_handler(self, alert_type, handler):
|
||||
@ -115,12 +118,13 @@ class AlertManager(component.Component):
|
||||
:param wait: bool, if True then the handler functions will be run right
|
||||
away and waited to return before processing the next alert
|
||||
"""
|
||||
alert = self.session.pop_alert()
|
||||
alerts = self.session.pop_alerts()
|
||||
# Loop through all alerts in the queue
|
||||
while alert is not None:
|
||||
for alert in alerts:
|
||||
alert_type = type(alert).__name__
|
||||
# Display the alert message
|
||||
log.debug("%s: %s", alert_type, alert.message())
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug("%s: %s", alert_type, decode_string(alert.message()))
|
||||
# Call any handlers for this alert type
|
||||
if alert_type in self.handlers:
|
||||
for handler in self.handlers[alert_type]:
|
||||
@ -128,5 +132,3 @@ class AlertManager(component.Component):
|
||||
self.delayed_calls.append(reactor.callLater(0, handler, alert))
|
||||
else:
|
||||
handler(alert)
|
||||
|
||||
alert = self.session.pop_alert()
|
||||
|
@ -2,6 +2,7 @@
|
||||
# authmanager.py
|
||||
#
|
||||
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
@ -36,27 +37,56 @@
|
||||
import os
|
||||
import random
|
||||
import stat
|
||||
import shutil
|
||||
import logging
|
||||
|
||||
import deluge.component as component
|
||||
import deluge.configmanager as configmanager
|
||||
import deluge.error
|
||||
from deluge.common import (AUTH_LEVEL_ADMIN, AUTH_LEVEL_NONE, AUTH_LEVEL_NORMAL,
|
||||
AUTH_LEVEL_READONLY, AUTH_LEVEL_DEFAULT,
|
||||
create_localclient_account)
|
||||
|
||||
from deluge.log import LOG as log
|
||||
from deluge.error import AuthManagerError, AuthenticationRequired, BadLoginError
|
||||
|
||||
AUTH_LEVEL_NONE = 0
|
||||
AUTH_LEVEL_READONLY = 1
|
||||
AUTH_LEVEL_NORMAL = 5
|
||||
AUTH_LEVEL_ADMIN = 10
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
AUTH_LEVEL_DEFAULT = AUTH_LEVEL_NORMAL
|
||||
AUTH_LEVELS_MAPPING = {
|
||||
'NONE': AUTH_LEVEL_NONE,
|
||||
'READONLY': AUTH_LEVEL_READONLY,
|
||||
'DEFAULT': AUTH_LEVEL_NORMAL,
|
||||
'NORMAL': AUTH_LEVEL_DEFAULT,
|
||||
'ADMIN': AUTH_LEVEL_ADMIN
|
||||
}
|
||||
|
||||
AUTH_LEVELS_MAPPING_REVERSE = {}
|
||||
for key, value in AUTH_LEVELS_MAPPING.iteritems():
|
||||
AUTH_LEVELS_MAPPING_REVERSE[value] = key
|
||||
|
||||
class Account(object):
|
||||
__slots__ = ('username', 'password', 'authlevel')
|
||||
def __init__(self, username, password, authlevel):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.authlevel = authlevel
|
||||
|
||||
def data(self):
|
||||
return {
|
||||
'username': self.username,
|
||||
'password': self.password,
|
||||
'authlevel': AUTH_LEVELS_MAPPING_REVERSE[self.authlevel],
|
||||
'authlevel_int': self.authlevel
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return ('<Account username="%(username)s" authlevel=%(authlevel)s>' %
|
||||
self.__dict__)
|
||||
|
||||
class BadLoginError(deluge.error.DelugeError):
|
||||
pass
|
||||
|
||||
class AuthManager(component.Component):
|
||||
def __init__(self):
|
||||
component.Component.__init__(self, "AuthManager")
|
||||
component.Component.__init__(self, "AuthManager", interval=10)
|
||||
self.__auth = {}
|
||||
self.__auth_modification_time = None
|
||||
|
||||
def start(self):
|
||||
self.__load_auth_file()
|
||||
@ -67,6 +97,19 @@ class AuthManager(component.Component):
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def update(self):
|
||||
auth_file = configmanager.get_config_dir("auth")
|
||||
# Check for auth file and create if necessary
|
||||
if not os.path.exists(auth_file):
|
||||
log.info("Authfile not found, recreating it.")
|
||||
self.__load_auth_file()
|
||||
return
|
||||
|
||||
auth_file_modification_time = os.stat(auth_file).st_mtime
|
||||
if self.__auth_modification_time != auth_file_modification_time:
|
||||
log.info("Auth file changed, reloading it!")
|
||||
self.__load_auth_file()
|
||||
|
||||
def authorize(self, username, password):
|
||||
"""
|
||||
Authorizes users based on username and password
|
||||
@ -76,55 +119,127 @@ class AuthManager(component.Component):
|
||||
:returns: int, the auth level for this user
|
||||
:rtype: int
|
||||
|
||||
:raises BadLoginError: if the username does not exist or password does not match
|
||||
:raises AuthenticationRequired: if aditional details are required to
|
||||
authenticate.
|
||||
:raises BadLoginError: if the username does not exist or password does
|
||||
not match.
|
||||
|
||||
"""
|
||||
if not username:
|
||||
raise AuthenticationRequired(
|
||||
"Username and Password are required.", username
|
||||
)
|
||||
|
||||
if username not in self.__auth:
|
||||
# Let's try to re-load the file.. Maybe it's been updated
|
||||
self.__load_auth_file()
|
||||
if username not in self.__auth:
|
||||
raise BadLoginError("Username does not exist")
|
||||
raise BadLoginError("Username does not exist", username)
|
||||
|
||||
if self.__auth[username][0] == password:
|
||||
if self.__auth[username].password == password:
|
||||
# Return the users auth level
|
||||
return int(self.__auth[username][1])
|
||||
return self.__auth[username].authlevel
|
||||
elif not password and self.__auth[username].password:
|
||||
raise AuthenticationRequired("Password is required", username)
|
||||
else:
|
||||
raise BadLoginError("Password does not match")
|
||||
raise BadLoginError("Password does not match", username)
|
||||
|
||||
def __create_localclient_account(self):
|
||||
def has_account(self, username):
|
||||
return username in self.__auth
|
||||
|
||||
def get_known_accounts(self):
|
||||
"""
|
||||
Returns the string.
|
||||
Returns a list of known deluge usernames.
|
||||
"""
|
||||
# We create a 'localclient' account with a random password
|
||||
self.__load_auth_file()
|
||||
return [account.data() for account in self.__auth.values()]
|
||||
|
||||
def create_account(self, username, password, authlevel):
|
||||
if username in self.__auth:
|
||||
raise AuthManagerError("Username in use.", username)
|
||||
try:
|
||||
from hashlib import sha1 as sha_hash
|
||||
except ImportError:
|
||||
from sha import new as sha_hash
|
||||
return "localclient:" + sha_hash(str(random.random())).hexdigest() + ":" + str(AUTH_LEVEL_ADMIN) + "\n"
|
||||
self.__auth[username] = Account(username, password,
|
||||
AUTH_LEVELS_MAPPING[authlevel])
|
||||
self.write_auth_file()
|
||||
return True
|
||||
except Exception, err:
|
||||
log.exception(err)
|
||||
raise err
|
||||
|
||||
def __load_auth_file(self):
|
||||
auth_file = configmanager.get_config_dir("auth")
|
||||
# Check for auth file and create if necessary
|
||||
if not os.path.exists(auth_file):
|
||||
localclient = self.__create_localclient_account()
|
||||
fd = open(auth_file, "w")
|
||||
fd.write(localclient)
|
||||
def update_account(self, username, password, authlevel):
|
||||
if username not in self.__auth:
|
||||
raise AuthManagerError("Username not known", username)
|
||||
try:
|
||||
self.__auth[username].username = username
|
||||
self.__auth[username].password = password
|
||||
self.__auth[username].authlevel = AUTH_LEVELS_MAPPING[authlevel]
|
||||
self.write_auth_file()
|
||||
return True
|
||||
except Exception, err:
|
||||
log.exception(err)
|
||||
raise err
|
||||
|
||||
def remove_account(self, username):
|
||||
if username not in self.__auth:
|
||||
raise AuthManagerError("Username not known", username)
|
||||
elif username == component.get("RPCServer").get_session_user():
|
||||
raise AuthManagerError(
|
||||
"You cannot delete your own account while logged in!", username
|
||||
)
|
||||
|
||||
del self.__auth[username]
|
||||
self.write_auth_file()
|
||||
return True
|
||||
|
||||
def write_auth_file(self):
|
||||
old_auth_file = configmanager.get_config_dir("auth")
|
||||
new_auth_file = old_auth_file + '.new'
|
||||
bak_auth_file = old_auth_file + '.bak'
|
||||
# Let's first create a backup
|
||||
if os.path.exists(old_auth_file):
|
||||
shutil.copy2(old_auth_file, bak_auth_file)
|
||||
|
||||
try:
|
||||
fd = open(new_auth_file, "w")
|
||||
for account in self.__auth.values():
|
||||
fd.write(
|
||||
"%(username)s:%(password)s:%(authlevel_int)s\n" %
|
||||
account.data()
|
||||
)
|
||||
fd.flush()
|
||||
os.fsync(fd.fileno())
|
||||
fd.close()
|
||||
# Change the permissions on the file so only this user can read/write it
|
||||
os.chmod(auth_file, stat.S_IREAD | stat.S_IWRITE)
|
||||
f = [localclient]
|
||||
else:
|
||||
# Load the auth file into a dictionary: {username: password, ...}
|
||||
f = open(auth_file, "r").readlines()
|
||||
os.rename(new_auth_file, old_auth_file)
|
||||
except:
|
||||
# Something failed, let's restore the previous file
|
||||
if os.path.exists(bak_auth_file):
|
||||
os.rename(bak_auth_file, old_auth_file)
|
||||
|
||||
self.__load_auth_file()
|
||||
|
||||
def __load_auth_file(self):
|
||||
save_and_reload = False
|
||||
auth_file = configmanager.get_config_dir("auth")
|
||||
# Check for auth file and create if necessary
|
||||
if not os.path.exists(auth_file):
|
||||
create_localclient_account()
|
||||
return self.__load_auth_file()
|
||||
|
||||
auth_file_modification_time = os.stat(auth_file).st_mtime
|
||||
if self.__auth_modification_time is None:
|
||||
self.__auth_modification_time = auth_file_modification_time
|
||||
elif self.__auth_modification_time == auth_file_modification_time:
|
||||
# File didn't change, no need for re-parsing's
|
||||
return
|
||||
|
||||
# Load the auth file into a dictionary: {username: Account(...)}
|
||||
f = open(auth_file, "r").readlines()
|
||||
|
||||
for line in f:
|
||||
if line.startswith("#"):
|
||||
# This is a comment line
|
||||
continue
|
||||
line = line.strip()
|
||||
if line.startswith("#") or not line:
|
||||
# This line is a comment or empty
|
||||
continue
|
||||
try:
|
||||
lsplit = line.split(":")
|
||||
except Exception, e:
|
||||
@ -132,15 +247,43 @@ class AuthManager(component.Component):
|
||||
continue
|
||||
if len(lsplit) == 2:
|
||||
username, password = lsplit
|
||||
log.warning("Your auth entry for %s contains no auth level, using AUTH_LEVEL_DEFAULT(%s)..", username, AUTH_LEVEL_DEFAULT)
|
||||
level = AUTH_LEVEL_DEFAULT
|
||||
log.warning("Your auth entry for %s contains no auth level, "
|
||||
"using AUTH_LEVEL_DEFAULT(%s)..", username,
|
||||
AUTH_LEVEL_DEFAULT)
|
||||
if username == 'localclient':
|
||||
authlevel = AUTH_LEVEL_ADMIN
|
||||
else:
|
||||
authlevel = AUTH_LEVEL_DEFAULT
|
||||
# This is probably an old auth file
|
||||
save_and_reload = True
|
||||
elif len(lsplit) == 3:
|
||||
username, password, level = lsplit
|
||||
username, password, authlevel = lsplit
|
||||
else:
|
||||
log.error("Your auth file is malformed: Incorrect number of fields!")
|
||||
log.error("Your auth file is malformed: "
|
||||
"Incorrect number of fields!")
|
||||
continue
|
||||
|
||||
self.__auth[username.strip()] = (password.strip(), level)
|
||||
username = username.strip()
|
||||
password = password.strip()
|
||||
try:
|
||||
authlevel = int(authlevel)
|
||||
except ValueError:
|
||||
try:
|
||||
authlevel = AUTH_LEVELS_MAPPING[authlevel]
|
||||
except KeyError:
|
||||
log.error("Your auth file is malformed: %r is not a valid auth "
|
||||
"level" % authlevel)
|
||||
continue
|
||||
|
||||
self.__auth[username] = Account(username, password, authlevel)
|
||||
|
||||
if "localclient" not in self.__auth:
|
||||
open(auth_file, "a").write(self.__create_localclient_account())
|
||||
create_localclient_account(True)
|
||||
return self.__load_auth_file()
|
||||
|
||||
|
||||
if save_and_reload:
|
||||
log.info("Re-writing auth file (upgrade)")
|
||||
self.write_auth_file()
|
||||
self.__auth_modification_time = auth_file_modification_time
|
||||
|
||||
|
@ -1,135 +0,0 @@
|
||||
#
|
||||
# autoadd.py
|
||||
#
|
||||
# Copyright (C) 2008 Andrew Resch <andrewresch@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.
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from deluge._libtorrent import lt
|
||||
|
||||
import deluge.component as component
|
||||
from deluge.configmanager import ConfigManager
|
||||
from deluge.log import LOG as log
|
||||
|
||||
MAX_NUM_ATTEMPTS = 10
|
||||
|
||||
class AutoAdd(component.Component):
|
||||
def __init__(self):
|
||||
component.Component.__init__(self, "AutoAdd", depend=["TorrentManager"], interval=5)
|
||||
# Get the core config
|
||||
self.config = ConfigManager("core.conf")
|
||||
|
||||
# A list of filenames
|
||||
self.invalid_torrents = []
|
||||
# Filename:Attempts
|
||||
self.attempts = {}
|
||||
|
||||
# Register set functions
|
||||
self.config.register_set_function("autoadd_enable",
|
||||
self._on_autoadd_enable, apply_now=True)
|
||||
self.config.register_set_function("autoadd_location",
|
||||
self._on_autoadd_location)
|
||||
|
||||
def update(self):
|
||||
if not self.config["autoadd_enable"]:
|
||||
# We shouldn't be updating because autoadd is not enabled
|
||||
component.pause("AutoAdd")
|
||||
return
|
||||
|
||||
# Check the auto add folder for new torrents to add
|
||||
if not os.path.isdir(self.config["autoadd_location"]):
|
||||
log.warning("Invalid AutoAdd folder: %s", self.config["autoadd_location"])
|
||||
component.pause("AutoAdd")
|
||||
return
|
||||
|
||||
for filename in os.listdir(self.config["autoadd_location"]):
|
||||
if filename.split(".")[-1] == "torrent":
|
||||
try:
|
||||
filepath = os.path.join(self.config["autoadd_location"], filename)
|
||||
except UnicodeDecodeError, e:
|
||||
log.error("Unable to auto add torrent due to inproper filename encoding: %s", e)
|
||||
continue
|
||||
try:
|
||||
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.attempts[filename] += 1
|
||||
if self.attempts[filename] >= MAX_NUM_ATTEMPTS:
|
||||
os.rename(filepath, filepath + ".invalid")
|
||||
del self.attempts[filename]
|
||||
self.invalid_torrents.remove(filename)
|
||||
else:
|
||||
self.invalid_torrents.append(filename)
|
||||
self.attempts[filename] = 1
|
||||
continue
|
||||
|
||||
# The torrent looks good, so lets add it to the session
|
||||
component.get("TorrentManager").add(filedump=filedump, filename=filename)
|
||||
|
||||
os.remove(filepath)
|
||||
|
||||
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 _on_autoadd_enable(self, key, value):
|
||||
log.debug("_on_autoadd_enable")
|
||||
if value:
|
||||
component.resume("AutoAdd")
|
||||
else:
|
||||
component.pause("AutoAdd")
|
||||
|
||||
def _on_autoadd_location(self, key, value):
|
||||
log.debug("_on_autoadd_location")
|
||||
# We need to resume the component just incase it was paused due to
|
||||
# an invalid autoadd location.
|
||||
if self.config["autoadd_enable"]:
|
||||
component.resume("AutoAdd")
|
@ -2,6 +2,7 @@
|
||||
# core.py
|
||||
#
|
||||
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
@ -38,36 +39,34 @@ from deluge._libtorrent import lt
|
||||
import os
|
||||
import glob
|
||||
import base64
|
||||
import shutil
|
||||
import logging
|
||||
import threading
|
||||
import pkg_resources
|
||||
import warnings
|
||||
import tempfile
|
||||
from urlparse import urljoin
|
||||
|
||||
|
||||
from twisted.internet import reactor, defer
|
||||
from twisted.internet.task import LoopingCall
|
||||
import twisted.web.client
|
||||
import twisted.web.error
|
||||
|
||||
from deluge.httpdownloader import download_file
|
||||
from deluge.log import LOG as log
|
||||
|
||||
|
||||
|
||||
import deluge.configmanager
|
||||
import deluge.common
|
||||
import deluge.component as component
|
||||
from deluge.event import *
|
||||
from deluge.error import *
|
||||
from deluge.core.authmanager import AUTH_LEVEL_ADMIN, AUTH_LEVEL_NONE
|
||||
from deluge.core.authmanager import AUTH_LEVELS_MAPPING, AUTH_LEVELS_MAPPING_REVERSE
|
||||
from deluge.core.torrentmanager import TorrentManager
|
||||
from deluge.core.pluginmanager import PluginManager
|
||||
from deluge.core.alertmanager import AlertManager
|
||||
from deluge.core.filtermanager import FilterManager
|
||||
from deluge.core.preferencesmanager import PreferencesManager
|
||||
from deluge.core.autoadd import AutoAdd
|
||||
from deluge.core.authmanager import AuthManager
|
||||
from deluge.core.eventmanager import EventManager
|
||||
from deluge.core.rpcserver import export
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class Core(component.Component):
|
||||
def __init__(self, listen_interface=None):
|
||||
log.debug("Core init..")
|
||||
@ -77,27 +76,41 @@ class Core(component.Component):
|
||||
log.info("Starting libtorrent %s session..", lt.version)
|
||||
|
||||
# Create the client fingerprint
|
||||
version = [int(value.split("-")[0]) for value in deluge.common.get_version().split(".")]
|
||||
version = deluge.common.VersionSplit(deluge.common.get_version()).version
|
||||
while len(version) < 4:
|
||||
version.append(0)
|
||||
|
||||
self.session = lt.session(lt.fingerprint("DE", *version), flags=0)
|
||||
# Note: All libtorrent python bindings to set plugins/extensions need to be disabled
|
||||
# due to GIL issue. https://code.google.com/p/libtorrent/issues/detail?id=369
|
||||
# Setting session flags to 1 enables all libtorrent default plugins
|
||||
self.session = lt.session(lt.fingerprint("DE", *version), flags=1)
|
||||
|
||||
# Load the session state if available
|
||||
self.__load_session_state()
|
||||
|
||||
# Set the user agent
|
||||
self.settings = lt.session_settings()
|
||||
self.settings.user_agent = "Deluge %s" % deluge.common.get_version()
|
||||
self.settings.user_agent = "Deluge/%(deluge_version)s Libtorrent/%(lt_version)s" % \
|
||||
{ 'deluge_version': deluge.common.get_version(),
|
||||
'lt_version': self.get_libtorrent_version().rpartition(".")[0] }
|
||||
# Increase the alert queue size so that alerts don't get lost
|
||||
self.settings.alert_queue_size = 10000
|
||||
|
||||
# Set session settings
|
||||
self.settings.send_redundant_have = True
|
||||
if deluge.common.windows_check():
|
||||
self.settings.disk_io_write_mode = \
|
||||
lt.io_buffer_mode_t.disable_os_cache
|
||||
self.settings.disk_io_read_mode = \
|
||||
lt.io_buffer_mode_t.disable_os_cache
|
||||
self.session.set_settings(self.settings)
|
||||
|
||||
# Load metadata extension
|
||||
self.session.add_extension(lt.create_metadata_plugin)
|
||||
self.session.add_extension(lt.create_ut_metadata_plugin)
|
||||
self.session.add_extension(lt.create_smart_ban_plugin)
|
||||
# Note: All libtorrent python bindings to set plugins/extensions need to be disabled
|
||||
# due to GIL issue. https://code.google.com/p/libtorrent/issues/detail?id=369
|
||||
# self.session.add_extension(lt.create_metadata_plugin)
|
||||
# self.session.add_extension(lt.create_ut_metadata_plugin)
|
||||
# self.session.add_extension(lt.create_smart_ban_plugin)
|
||||
|
||||
# Create the components
|
||||
self.eventmanager = EventManager()
|
||||
@ -106,7 +119,6 @@ class Core(component.Component):
|
||||
self.pluginmanager = PluginManager(self)
|
||||
self.torrentmanager = TorrentManager()
|
||||
self.filtermanager = FilterManager(self)
|
||||
self.autoadd = AutoAdd()
|
||||
self.authmanager = AuthManager()
|
||||
|
||||
# New release check information
|
||||
@ -114,6 +126,7 @@ class Core(component.Component):
|
||||
|
||||
# Get the core config
|
||||
self.config = deluge.configmanager.ConfigManager("core.conf")
|
||||
self.config.save()
|
||||
|
||||
# If there was an interface value from the command line, use it, but
|
||||
# store the one in the config so we can restore it on shutdown
|
||||
@ -128,9 +141,12 @@ class Core(component.Component):
|
||||
self.__new_release = None
|
||||
|
||||
def stop(self):
|
||||
log.debug("Core stopping...")
|
||||
|
||||
# Save the DHT state if necessary
|
||||
if self.config["dht"]:
|
||||
self.save_dht_state()
|
||||
|
||||
# Save the libtorrent session state
|
||||
self.__save_session_state()
|
||||
|
||||
@ -147,16 +163,16 @@ class Core(component.Component):
|
||||
def __save_session_state(self):
|
||||
"""Saves the libtorrent session state"""
|
||||
try:
|
||||
open(deluge.configmanager.get_config_dir("session.state"), "wb").write(
|
||||
lt.bencode(self.session.state()))
|
||||
session_state = deluge.configmanager.get_config_dir("session.state")
|
||||
open(session_state, "wb").write(lt.bencode(self.session.save_state()))
|
||||
except Exception, e:
|
||||
log.warning("Failed to save lt state: %s", e)
|
||||
|
||||
def __load_session_state(self):
|
||||
"""Loads the libtorrent session state"""
|
||||
try:
|
||||
self.session.load_state(lt.bdecode(
|
||||
open(deluge.configmanager.get_config_dir("session.state"), "rb").read()))
|
||||
session_state = deluge.configmanager.get_config_dir("session.state")
|
||||
self.session.load_state(lt.bdecode(open(session_state, "rb").read()))
|
||||
except Exception, e:
|
||||
log.warning("Failed to load lt state: %s", e)
|
||||
|
||||
@ -212,7 +228,9 @@ class Core(component.Component):
|
||||
log.exception(e)
|
||||
|
||||
try:
|
||||
torrent_id = self.torrentmanager.add(filedump=filedump, options=options, filename=filename)
|
||||
torrent_id = self.torrentmanager.add(
|
||||
filedump=filedump, options=options, filename=filename
|
||||
)
|
||||
except Exception, e:
|
||||
log.error("There was an error adding the torrent file %s", filename)
|
||||
log.exception(e)
|
||||
@ -236,20 +254,44 @@ class Core(component.Component):
|
||||
:returns: a Deferred which returns the torrent_id as a str or None
|
||||
"""
|
||||
log.info("Attempting to add url %s", url)
|
||||
def on_get_file(filename):
|
||||
def on_download_success(filename):
|
||||
# We got the file, so add it to the session
|
||||
data = open(filename, "rb").read()
|
||||
return self.add_torrent_file(filename, base64.encodestring(data), options)
|
||||
f = open(filename, "rb")
|
||||
data = f.read()
|
||||
f.close()
|
||||
try:
|
||||
os.remove(filename)
|
||||
except Exception, e:
|
||||
log.warning("Couldn't remove temp file: %s", e)
|
||||
return self.add_torrent_file(
|
||||
filename, base64.encodestring(data), options
|
||||
)
|
||||
|
||||
def on_get_file_error(failure):
|
||||
# Log the error and pass the failure onto the client
|
||||
log.error("Error occured downloading torrent from %s", url)
|
||||
log.error("Reason: %s", failure.getErrorMessage())
|
||||
return failure
|
||||
def on_download_fail(failure):
|
||||
if failure.check(twisted.web.error.PageRedirect):
|
||||
new_url = urljoin(url, failure.getErrorMessage().split(" to ")[1])
|
||||
result = download_file(
|
||||
new_url, tempfile.mkstemp()[1], headers=headers,
|
||||
force_filename=True
|
||||
)
|
||||
result.addCallbacks(on_download_success, on_download_fail)
|
||||
elif failure.check(twisted.web.client.PartialDownloadError):
|
||||
result = download_file(
|
||||
url, tempfile.mkstemp()[1], headers=headers,
|
||||
force_filename=True, allow_compression=False
|
||||
)
|
||||
result.addCallbacks(on_download_success, on_download_fail)
|
||||
else:
|
||||
# Log the error and pass the failure onto the client
|
||||
log.error("Error occured downloading torrent from %s", url)
|
||||
log.error("Reason: %s", failure.getErrorMessage())
|
||||
result = failure
|
||||
return result
|
||||
|
||||
d = download_file(url, url.split("/")[-1], headers=headers)
|
||||
d.addCallback(on_get_file)
|
||||
d.addErrback(on_get_file_error)
|
||||
d = download_file(
|
||||
url, tempfile.mkstemp()[1], headers=headers, force_filename=True
|
||||
)
|
||||
d.addCallbacks(on_download_success, on_download_fail)
|
||||
return d
|
||||
|
||||
@export
|
||||
@ -384,16 +426,24 @@ class Core(component.Component):
|
||||
for torrent_id in torrent_ids:
|
||||
self.torrentmanager[torrent_id].resume()
|
||||
|
||||
def create_torrent_status(self, torrent_id, torrent_keys, plugin_keys, diff=False, update=False):
|
||||
try:
|
||||
status = self.torrentmanager[torrent_id].get_status(torrent_keys, diff, update=update)
|
||||
except KeyError:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
# Torrent was probaly removed meanwhile
|
||||
return {}
|
||||
|
||||
# Ask the plugin manager to fill in the plugin keys
|
||||
if len(plugin_keys) > 0:
|
||||
status.update(self.pluginmanager.get_status(torrent_id, plugin_keys))
|
||||
return status
|
||||
|
||||
@export
|
||||
def get_torrent_status(self, torrent_id, keys, diff=False):
|
||||
# Build the status dictionary
|
||||
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()))
|
||||
if len(leftover_fields) > 0:
|
||||
status.update(self.pluginmanager.get_status(torrent_id, leftover_fields))
|
||||
return status
|
||||
torrent_keys, plugin_keys = self.torrentmanager.separate_keys(keys, [torrent_id])
|
||||
return self.create_torrent_status(torrent_id, torrent_keys, plugin_keys, diff=diff, update=True)
|
||||
|
||||
@export
|
||||
def get_torrents_status(self, filter_dict, keys, diff=False):
|
||||
@ -402,12 +452,17 @@ class Core(component.Component):
|
||||
"""
|
||||
torrent_ids = self.filtermanager.filter_torrent_ids(filter_dict)
|
||||
status_dict = {}.fromkeys(torrent_ids)
|
||||
d = self.torrentmanager.torrents_status_update(torrent_ids, keys, diff=diff)
|
||||
|
||||
# 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, diff)
|
||||
|
||||
return status_dict
|
||||
def add_plugin_fields(args):
|
||||
status_dict, plugin_keys = args
|
||||
# Ask the plugin manager to fill in the plugin keys
|
||||
if len(plugin_keys) > 0:
|
||||
for key in status_dict.keys():
|
||||
status_dict[key].update(self.pluginmanager.get_status(key, plugin_keys))
|
||||
return status_dict
|
||||
d.addCallback(add_plugin_fields)
|
||||
return d
|
||||
|
||||
@export
|
||||
def get_filter_tree(self , show_zero_hits=True, hide_cat=None):
|
||||
@ -454,7 +509,7 @@ class Core(component.Component):
|
||||
"""Set the config with values from dictionary"""
|
||||
# Load all the values into the configuration
|
||||
for key in config.keys():
|
||||
if isinstance(config[key], unicode) or isinstance(config[key], str):
|
||||
if isinstance(config[key], basestring):
|
||||
config[key] = config[key].encode("utf8")
|
||||
self.config[key] = config[key]
|
||||
|
||||
@ -535,6 +590,11 @@ class Core(component.Component):
|
||||
"""Sets a higher priority to the first and last pieces"""
|
||||
return self.torrentmanager[torrent_id].set_prioritize_first_last(value)
|
||||
|
||||
@export
|
||||
def set_torrent_sequential_download(self, torrent_id, value):
|
||||
"""Toggle sequencial pieces download"""
|
||||
return self.torrentmanager[torrent_id].set_sequential_download(value)
|
||||
|
||||
@export
|
||||
def set_torrent_auto_managed(self, torrent_id, value):
|
||||
"""Sets the auto managed flag for queueing purposes"""
|
||||
@ -565,6 +625,32 @@ 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(AUTH_LEVEL_ADMIN)
|
||||
def set_torrents_owner(self, torrent_ids, username):
|
||||
"""Set's the torrent owner.
|
||||
|
||||
:param torrent_id: the torrent_id of the torrent to remove
|
||||
:type torrent_id: string
|
||||
:param username: the new owner username
|
||||
:type username: string
|
||||
|
||||
:raises DelugeError: if the username is not known
|
||||
"""
|
||||
if not self.authmanager.has_account(username):
|
||||
raise DelugeError("Username \"%s\" is not known." % username)
|
||||
if isinstance(torrent_ids, basestring):
|
||||
torrent_ids = [torrent_ids]
|
||||
for torrent_id in torrent_ids:
|
||||
self.torrentmanager[torrent_id].set_owner(username)
|
||||
return None
|
||||
|
||||
@export
|
||||
def set_torrents_shared(self, torrent_ids, shared):
|
||||
if isinstance(torrent_ids, basestring):
|
||||
torrent_ids = [torrent_ids]
|
||||
for torrent_id in torrent_ids:
|
||||
self.torrentmanager[torrent_id].set_options({"shared": shared})
|
||||
|
||||
@export
|
||||
def get_path_size(self, path):
|
||||
"""Returns the size of the file or folder 'path' and -1 if the path is
|
||||
@ -679,7 +765,8 @@ class Core(component.Component):
|
||||
@export
|
||||
def queue_top(self, torrent_ids):
|
||||
log.debug("Attempting to queue %s to top", torrent_ids)
|
||||
for torrent_id in torrent_ids:
|
||||
# torrent_ids must be sorted in reverse before moving to preserve order
|
||||
for torrent_id in sorted(torrent_ids, key=self.torrentmanager.get_queue_position, reverse=True):
|
||||
try:
|
||||
# If the queue method returns True, then we should emit a signal
|
||||
if self.torrentmanager.queue_top(torrent_id):
|
||||
@ -690,35 +777,48 @@ class Core(component.Component):
|
||||
@export
|
||||
def queue_up(self, torrent_ids):
|
||||
log.debug("Attempting to queue %s to up", torrent_ids)
|
||||
torrents = ((self.torrentmanager.get_queue_position(torrent_id), torrent_id) for torrent_id in torrent_ids)
|
||||
torrent_moved = True
|
||||
prev_queue_position = None
|
||||
#torrent_ids must be sorted before moving.
|
||||
torrent_ids = list(torrent_ids)
|
||||
torrent_ids.sort(key = lambda id: self.torrentmanager.torrents[id].get_queue_position())
|
||||
for torrent_id in torrent_ids:
|
||||
try:
|
||||
# If the queue method returns True, then we should emit a signal
|
||||
if self.torrentmanager.queue_up(torrent_id):
|
||||
component.get("EventManager").emit(TorrentQueueChangedEvent())
|
||||
except KeyError:
|
||||
log.warning("torrent_id: %s does not exist in the queue", torrent_id)
|
||||
for queue_position, torrent_id in sorted(torrents):
|
||||
# Move the torrent if and only if there is space (by not moving it we preserve the order)
|
||||
if torrent_moved or queue_position - prev_queue_position > 1:
|
||||
try:
|
||||
torrent_moved = self.torrentmanager.queue_up(torrent_id)
|
||||
except KeyError:
|
||||
log.warning("torrent_id: %s does not exist in the queue", torrent_id)
|
||||
# If the torrent moved, then we should emit a signal
|
||||
if torrent_moved:
|
||||
component.get("EventManager").emit(TorrentQueueChangedEvent())
|
||||
else:
|
||||
prev_queue_position = queue_position
|
||||
|
||||
@export
|
||||
def queue_down(self, torrent_ids):
|
||||
log.debug("Attempting to queue %s to down", torrent_ids)
|
||||
torrents = ((self.torrentmanager.get_queue_position(torrent_id), torrent_id) for torrent_id in torrent_ids)
|
||||
torrent_moved = True
|
||||
prev_queue_position = None
|
||||
#torrent_ids must be sorted before moving.
|
||||
torrent_ids = list(torrent_ids)
|
||||
torrent_ids.sort(key = lambda id: -self.torrentmanager.torrents[id].get_queue_position())
|
||||
for torrent_id in torrent_ids:
|
||||
try:
|
||||
# If the queue method returns True, then we should emit a signal
|
||||
if self.torrentmanager.queue_down(torrent_id):
|
||||
component.get("EventManager").emit(TorrentQueueChangedEvent())
|
||||
except KeyError:
|
||||
log.warning("torrent_id: %s does not exist in the queue", torrent_id)
|
||||
for queue_position, torrent_id in sorted(torrents, reverse=True):
|
||||
# Move the torrent if and only if there is space (by not moving it we preserve the order)
|
||||
if torrent_moved or prev_queue_position - queue_position > 1:
|
||||
try:
|
||||
torrent_moved = self.torrentmanager.queue_down(torrent_id)
|
||||
except KeyError:
|
||||
log.warning("torrent_id: %s does not exist in the queue", torrent_id)
|
||||
# If the torrent moved, then we should emit a signal
|
||||
if torrent_moved:
|
||||
component.get("EventManager").emit(TorrentQueueChangedEvent())
|
||||
else:
|
||||
prev_queue_position = queue_position
|
||||
|
||||
@export
|
||||
def queue_bottom(self, torrent_ids):
|
||||
log.debug("Attempting to queue %s to bottom", torrent_ids)
|
||||
for torrent_id in torrent_ids:
|
||||
# torrent_ids must be sorted before moving to preserve order
|
||||
for torrent_id in sorted(torrent_ids, key=self.torrentmanager.get_queue_position):
|
||||
try:
|
||||
# If the queue method returns True, then we should emit a signal
|
||||
if self.torrentmanager.queue_bottom(torrent_id):
|
||||
@ -747,7 +847,11 @@ class Core(component.Component):
|
||||
def on_get_page(result):
|
||||
return bool(int(result))
|
||||
|
||||
def logError(failure):
|
||||
log.warning("Error testing listen port: %s", failure)
|
||||
|
||||
d.addCallback(on_get_page)
|
||||
d.addErrback(logError)
|
||||
|
||||
return d
|
||||
|
||||
@ -768,7 +872,10 @@ class Core(component.Component):
|
||||
"""
|
||||
if not path:
|
||||
path = self.config["download_location"]
|
||||
return deluge.common.free_space(path)
|
||||
try:
|
||||
return deluge.common.free_space(path)
|
||||
except InvalidPathError:
|
||||
return 0
|
||||
|
||||
@export
|
||||
def get_libtorrent_version(self):
|
||||
@ -780,3 +887,23 @@ class Core(component.Component):
|
||||
|
||||
"""
|
||||
return lt.version
|
||||
|
||||
@export(AUTH_LEVEL_ADMIN)
|
||||
def get_known_accounts(self):
|
||||
return self.authmanager.get_known_accounts()
|
||||
|
||||
@export(AUTH_LEVEL_NONE)
|
||||
def get_auth_levels_mappings(self):
|
||||
return (AUTH_LEVELS_MAPPING, AUTH_LEVELS_MAPPING_REVERSE)
|
||||
|
||||
@export(AUTH_LEVEL_ADMIN)
|
||||
def create_account(self, username, password, authlevel):
|
||||
return self.authmanager.create_account(username, password, authlevel)
|
||||
|
||||
@export(AUTH_LEVEL_ADMIN)
|
||||
def update_account(self, username, password, authlevel):
|
||||
return self.authmanager.update_account(username, password, authlevel)
|
||||
|
||||
@export(AUTH_LEVEL_ADMIN)
|
||||
def remove_account(self, username):
|
||||
return self.authmanager.remove_account(username)
|
||||
|
@ -33,9 +33,7 @@
|
||||
#
|
||||
|
||||
import os
|
||||
import gettext
|
||||
import locale
|
||||
import pkg_resources
|
||||
import logging
|
||||
from twisted.internet import reactor
|
||||
import twisted.internet.error
|
||||
|
||||
@ -43,16 +41,19 @@ import deluge.component as component
|
||||
import deluge.configmanager
|
||||
import deluge.common
|
||||
from deluge.core.rpcserver import RPCServer, export
|
||||
from deluge.log import LOG as log
|
||||
import deluge.error
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class Daemon(object):
|
||||
def __init__(self, options=None, args=None, classic=False):
|
||||
# Check for another running instance of the daemon
|
||||
if os.path.isfile(deluge.configmanager.get_config_dir("deluged.pid")):
|
||||
# Get the PID and the port of the supposedly running daemon
|
||||
try:
|
||||
(pid, port) = open(deluge.configmanager.get_config_dir("deluged.pid")).read().strip().split(";")
|
||||
(pid, port) = open(
|
||||
deluge.configmanager.get_config_dir("deluged.pid")
|
||||
).read().strip().split(";")
|
||||
pid = int(pid)
|
||||
port = int(port)
|
||||
except ValueError:
|
||||
@ -62,13 +63,8 @@ class Daemon(object):
|
||||
|
||||
def process_running(pid):
|
||||
if deluge.common.windows_check():
|
||||
# Do some fancy WMI junk to see if the PID exists in Windows
|
||||
from win32com.client import GetObject
|
||||
def get_proclist():
|
||||
WMI = GetObject('winmgmts:')
|
||||
processes = WMI.InstancesOf('Win32_Process')
|
||||
return [process.Properties_('ProcessID').Value for process in processes]
|
||||
return pid in get_proclist()
|
||||
import win32process
|
||||
return pid in win32process.EnumProcesses()
|
||||
else:
|
||||
# We can just use os.kill on UNIX to test if the process is running
|
||||
try:
|
||||
@ -91,26 +87,14 @@ class Daemon(object):
|
||||
else:
|
||||
# This is a deluged!
|
||||
s.close()
|
||||
raise deluge.error.DaemonRunningError("There is a deluge daemon running with this config directory!")
|
||||
|
||||
# Initialize gettext
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, '')
|
||||
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:
|
||||
log.error("Unable to initialize gettext/locale: %s", e)
|
||||
import __builtin__
|
||||
__builtin__.__dict__["_"] = lambda x: x
|
||||
raise deluge.error.DaemonRunningError(
|
||||
"There is a deluge daemon running with this config "
|
||||
"directory!"
|
||||
)
|
||||
|
||||
# Twisted catches signals to terminate, so just have it call the shutdown
|
||||
# method.
|
||||
reactor.addSystemEventTrigger("after", "shutdown", self.shutdown)
|
||||
reactor.addSystemEventTrigger("before", "shutdown", self._shutdown)
|
||||
|
||||
# Catch some Windows specific signals
|
||||
if deluge.common.windows_check():
|
||||
@ -120,7 +104,7 @@ class Daemon(object):
|
||||
def win_handler(ctrl_type):
|
||||
log.debug("ctrl_type: %s", ctrl_type)
|
||||
if ctrl_type == CTRL_CLOSE_EVENT or ctrl_type == CTRL_SHUTDOWN_EVENT:
|
||||
self.__shutdown()
|
||||
self._shutdown()
|
||||
return 1
|
||||
SetConsoleCtrlHandler(win_handler)
|
||||
|
||||
@ -133,9 +117,14 @@ class Daemon(object):
|
||||
if options and options.config:
|
||||
deluge.configmanager.set_config_dir(options.config)
|
||||
|
||||
if options and options.listen_interface:
|
||||
listen_interface = options.listen_interface
|
||||
else:
|
||||
listen_interface = ""
|
||||
|
||||
from deluge.core.core import Core
|
||||
# Start the core as a thread and join it until it's done
|
||||
self.core = Core()
|
||||
self.core = Core(listen_interface=listen_interface)
|
||||
|
||||
port = self.core.config["daemon_port"]
|
||||
if options and options.port:
|
||||
@ -177,26 +166,16 @@ class Daemon(object):
|
||||
reactor.callLater(0, reactor.stop)
|
||||
|
||||
def _shutdown(self, *args, **kwargs):
|
||||
try:
|
||||
os.remove(deluge.configmanager.get_config_dir("deluged.pid"))
|
||||
except Exception, e:
|
||||
log.exception(e)
|
||||
log.error("Error removing deluged.pid!")
|
||||
if os.path.exists(deluge.configmanager.get_config_dir("deluged.pid")):
|
||||
try:
|
||||
os.remove(deluge.configmanager.get_config_dir("deluged.pid"))
|
||||
except Exception, e:
|
||||
log.exception(e)
|
||||
log.error("Error removing deluged.pid!")
|
||||
|
||||
component.shutdown()
|
||||
try:
|
||||
reactor.stop()
|
||||
except twisted.internet.error.ReactorNotRunning:
|
||||
log.debug("Tried to stop the reactor but it is not running..")
|
||||
|
||||
@export()
|
||||
def info(self):
|
||||
"""
|
||||
Returns some info from the daemon.
|
||||
|
||||
:returns: str, the version number
|
||||
"""
|
||||
return deluge.common.get_version()
|
||||
log.info("Waiting for components to shutdown..")
|
||||
d = component.shutdown()
|
||||
return d
|
||||
|
||||
@export()
|
||||
def get_method_list(self):
|
||||
@ -204,3 +183,18 @@ class Daemon(object):
|
||||
Returns a list of the exported methods.
|
||||
"""
|
||||
return self.rpcserver.get_method_list()
|
||||
|
||||
@export(1)
|
||||
def authorized_call(self, rpc):
|
||||
"""
|
||||
Returns True if authorized to call rpc.
|
||||
|
||||
:param rpc: a rpc, eg, "core.get_torrents_status"
|
||||
:type rpc: string
|
||||
|
||||
"""
|
||||
if not rpc in self.get_method_list():
|
||||
return False
|
||||
|
||||
auth_level = self.rpcserver.get_session_auth_level()
|
||||
return auth_level >= self.rpcserver.get_rpc_auth_level()
|
||||
|
@ -33,8 +33,10 @@
|
||||
#
|
||||
#
|
||||
|
||||
import logging
|
||||
import deluge.component as component
|
||||
from deluge.log import LOG as log
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class EventManager(component.Component):
|
||||
def __init__(self):
|
||||
@ -53,7 +55,10 @@ class EventManager(component.Component):
|
||||
if event.name in self.handlers:
|
||||
for handler in self.handlers[event.name]:
|
||||
#log.debug("Running handler %s for event %s with args: %s", event.name, handler, event.args)
|
||||
handler(*event.args)
|
||||
try:
|
||||
handler(*event.args)
|
||||
except Exception, e:
|
||||
log.error("Event handler %s failed in %s with exception %s", event.name, handler, e)
|
||||
|
||||
def register_event_handler(self, event, handler):
|
||||
"""
|
||||
|
@ -33,12 +33,13 @@
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
import logging
|
||||
import deluge.component as component
|
||||
from deluge.log import LOG as log
|
||||
|
||||
STATE_SORT = ["All", "Downloading", "Seeding", "Active", "Paused", "Queued"]
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
#special purpose filters:
|
||||
def filter_keywords(torrent_ids, values):
|
||||
#cleanup.
|
||||
@ -77,6 +78,27 @@ def filter_one_keyword(torrent_ids, keyword):
|
||||
yield torrent_id
|
||||
break
|
||||
|
||||
def filter_by_name(torrent_ids, search_string):
|
||||
all_torrents = component.get("TorrentManager").torrents
|
||||
try:
|
||||
search_string, match_case = search_string[0].split('::match')
|
||||
except ValueError:
|
||||
search_string = search_string[0]
|
||||
match_case = False
|
||||
|
||||
if match_case is False:
|
||||
search_string = search_string.lower()
|
||||
|
||||
for torrent_id in torrent_ids:
|
||||
torrent_name = all_torrents[torrent_id].get_name()
|
||||
if match_case is False:
|
||||
torrent_name = all_torrents[torrent_id].get_name().lower()
|
||||
else:
|
||||
torrent_name = all_torrents[torrent_id].get_name()
|
||||
|
||||
if search_string in torrent_name:
|
||||
yield torrent_id
|
||||
|
||||
def tracker_error_filter(torrent_ids, values):
|
||||
filtered_torrent_ids = []
|
||||
tm = component.get("TorrentManager")
|
||||
@ -91,9 +113,8 @@ def tracker_error_filter(torrent_ids, values):
|
||||
# Check all the torrent's tracker_status for 'Error:' and only return torrent_ids
|
||||
# that have this substring in their tracker_status
|
||||
for torrent_id in torrent_ids:
|
||||
if "Error:" in tm[torrent_id].get_status(["tracker_status"])["tracker_status"]:
|
||||
if _("Error") + ":" in tm[torrent_id].get_status(["tracker_host"])["tracker_host"]:
|
||||
filtered_torrent_ids.append(torrent_id)
|
||||
|
||||
return filtered_torrent_ids
|
||||
|
||||
class FilterManager(component.Component):
|
||||
@ -107,6 +128,7 @@ class FilterManager(component.Component):
|
||||
self.torrents = core.torrentmanager
|
||||
self.registered_filters = {}
|
||||
self.register_filter("keyword", filter_keywords)
|
||||
self.register_filter("name", filter_by_name)
|
||||
self.tree_fields = {}
|
||||
|
||||
self.register_tree_field("state", self._init_state_tree)
|
||||
@ -116,6 +138,10 @@ class FilterManager(component.Component):
|
||||
|
||||
self.register_filter("tracker_host", tracker_error_filter)
|
||||
|
||||
def _init_users_tree():
|
||||
return {"": 0}
|
||||
self.register_tree_field("owner", _init_users_tree)
|
||||
|
||||
def filter_torrent_ids(self, filter_dict):
|
||||
"""
|
||||
returns a list of torrent_id's matching filter_dict.
|
||||
@ -126,12 +152,11 @@ class FilterManager(component.Component):
|
||||
|
||||
#sanitize input: filter-value must be a list of strings
|
||||
for key, value in filter_dict.items():
|
||||
if isinstance(value, str):
|
||||
filter_dict[key] = [value]
|
||||
|
||||
if isinstance(value, basestring):
|
||||
filter_dict[key] = [value]
|
||||
|
||||
if "id"in filter_dict: #optimized filter for id:
|
||||
torrent_ids = filter_dict["id"]
|
||||
torrent_ids = list(filter_dict["id"])
|
||||
del filter_dict["id"]
|
||||
else:
|
||||
torrent_ids = self.torrents.get_torrent_list()
|
||||
@ -166,13 +191,11 @@ class FilterManager(component.Component):
|
||||
|
||||
#leftover filter arguments:
|
||||
#default filter on status fields.
|
||||
status_func = self.core.get_torrent_status #premature optimalisation..
|
||||
for torrent_id in list(torrent_ids):
|
||||
status = status_func(torrent_id, filter_dict.keys()) #status={key:value}
|
||||
status = self.torrents[torrent_id].get_status(filter_dict.keys()) #status={key:value}
|
||||
for field, values in filter_dict.iteritems():
|
||||
if (not status[field] in values) and torrent_id in torrent_ids:
|
||||
torrent_ids.remove(torrent_id)
|
||||
|
||||
return torrent_ids
|
||||
|
||||
def get_filter_tree(self, show_zero_hits=True, hide_cat=None):
|
||||
@ -181,24 +204,22 @@ class FilterManager(component.Component):
|
||||
for use in sidebar.
|
||||
"""
|
||||
torrent_ids = self.torrents.get_torrent_list()
|
||||
status_func = self.core.get_torrent_status #premature optimalisation..
|
||||
tree_keys = list(self.tree_fields.keys())
|
||||
if hide_cat:
|
||||
for cat in hide_cat:
|
||||
tree_keys.remove(cat)
|
||||
|
||||
items = dict( (field, self.tree_fields[field]()) for field in tree_keys)
|
||||
torrent_keys, plugin_keys = self.torrents.separate_keys(tree_keys, torrent_ids)
|
||||
items = dict((field, self.tree_fields[field]()) for field in tree_keys)
|
||||
|
||||
#count status fields.
|
||||
for torrent_id in list(torrent_ids):
|
||||
status = status_func(torrent_id, tree_keys) #status={key:value}
|
||||
status = self.core.create_torrent_status(torrent_id, torrent_keys, plugin_keys) #status={key:value}
|
||||
for field in tree_keys:
|
||||
value = status[field]
|
||||
items[field][value] = items[field].get(value, 0) + 1
|
||||
|
||||
items["tracker_host"]["All"] = len(torrent_ids)
|
||||
|
||||
if "tracker_host" in items:
|
||||
items["tracker_host"]["All"] = len(torrent_ids)
|
||||
items["tracker_host"]["Error"] = len(tracker_error_filter(torrent_ids, ("Error",)))
|
||||
|
||||
if "state" in tree_keys and not show_zero_hits:
|
||||
@ -239,9 +260,8 @@ class FilterManager(component.Component):
|
||||
del self.tree_fields[field]
|
||||
|
||||
def filter_state_active(self, torrent_ids):
|
||||
get_status = self.core.get_torrent_status
|
||||
for torrent_id in list(torrent_ids):
|
||||
status = get_status(torrent_id, ["download_payload_rate", "upload_payload_rate"])
|
||||
status = self.torrents[torrent_id].get_status(["download_payload_rate", "upload_payload_rate"])
|
||||
if status["download_payload_rate"] or status["upload_payload_rate"]:
|
||||
pass #ok
|
||||
else:
|
||||
|
@ -39,12 +39,14 @@ import os.path
|
||||
import pickle
|
||||
import cPickle
|
||||
import shutil
|
||||
import logging
|
||||
|
||||
from deluge._libtorrent import lt
|
||||
|
||||
from deluge.configmanager import ConfigManager, get_config_dir
|
||||
import deluge.core.torrentmanager
|
||||
from deluge.log import LOG as log
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
#start : http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/286203
|
||||
def makeFakeClass(module, name):
|
||||
|
@ -36,13 +36,13 @@
|
||||
|
||||
"""PluginManager for Core"""
|
||||
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet.task import LoopingCall
|
||||
import logging
|
||||
|
||||
from deluge.event import PluginEnabledEvent, PluginDisabledEvent
|
||||
import deluge.pluginmanagerbase
|
||||
import deluge.component as component
|
||||
from deluge.log import LOG as log
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class PluginManager(deluge.pluginmanagerbase.PluginManagerBase,
|
||||
component.Component):
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# preferencesmanager.py
|
||||
#
|
||||
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2008-2010 Andrew Resch <andrewresch@gmail.com>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
@ -34,10 +34,9 @@
|
||||
#
|
||||
|
||||
|
||||
import os.path
|
||||
import os
|
||||
import logging
|
||||
import threading
|
||||
import pkg_resources
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet.task import LoopingCall
|
||||
|
||||
from deluge._libtorrent import lt
|
||||
@ -46,7 +45,8 @@ from deluge.event import *
|
||||
import deluge.configmanager
|
||||
import deluge.common
|
||||
import deluge.component as component
|
||||
from deluge.log import LOG as log
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_PREFS = {
|
||||
"send_info": False,
|
||||
@ -62,6 +62,7 @@ DEFAULT_PREFS = {
|
||||
"torrentfiles_location": deluge.common.get_default_download_dir(),
|
||||
"plugins_location": os.path.join(deluge.configmanager.get_config_dir(), "plugins"),
|
||||
"prioritize_first_last_pieces": False,
|
||||
"sequential_download": False,
|
||||
"random_port": True,
|
||||
"dht": True,
|
||||
"upnp": True,
|
||||
@ -85,8 +86,6 @@ DEFAULT_PREFS = {
|
||||
"max_upload_speed_per_torrent": -1,
|
||||
"max_download_speed_per_torrent": -1,
|
||||
"enabled_plugins": [],
|
||||
"autoadd_location": deluge.common.get_default_download_dir(),
|
||||
"autoadd_enable": False,
|
||||
"add_paused": False,
|
||||
"max_active_seeding": 5,
|
||||
"max_active_downloading": 3,
|
||||
@ -140,7 +139,9 @@ DEFAULT_PREFS = {
|
||||
"rate_limit_ip_overhead": True,
|
||||
"geoip_db_location": "/usr/share/GeoIP/GeoIP.dat",
|
||||
"cache_size": 512,
|
||||
"cache_expiry": 60
|
||||
"cache_expiry": 60,
|
||||
"auto_manage_prefer_seeds": False,
|
||||
"shared": False
|
||||
}
|
||||
|
||||
class PreferencesManager(component.Component):
|
||||
@ -148,92 +149,40 @@ class PreferencesManager(component.Component):
|
||||
component.Component.__init__(self, "PreferencesManager")
|
||||
|
||||
self.config = deluge.configmanager.ConfigManager("core.conf", DEFAULT_PREFS)
|
||||
if 'public' in self.config:
|
||||
log.debug("Updating configuration file: Renamed torrent's public "
|
||||
"attribute to shared.")
|
||||
self.config["shared"] = self.config["public"]
|
||||
del self.config["public"]
|
||||
|
||||
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",
|
||||
self._on_set_torrentfiles_location)
|
||||
self.config.register_set_function("listen_ports",
|
||||
self._on_set_listen_ports)
|
||||
self.config.register_set_function("listen_interface",
|
||||
self._on_set_listen_interface)
|
||||
self.config.register_set_function("random_port",
|
||||
self._on_set_random_port)
|
||||
self.config.register_set_function("outgoing_ports",
|
||||
self._on_set_outgoing_ports)
|
||||
self.config.register_set_function("random_outgoing_ports",
|
||||
self._on_set_random_outgoing_ports)
|
||||
self.config.register_set_function("peer_tos",
|
||||
self._on_set_peer_tos)
|
||||
self.config.register_set_function("dht", self._on_set_dht)
|
||||
self.config.register_set_function("upnp", self._on_set_upnp)
|
||||
self.config.register_set_function("natpmp", self._on_set_natpmp)
|
||||
self.config.register_set_function("utpex", self._on_set_utpex)
|
||||
self.config.register_set_function("lsd", self._on_set_lsd)
|
||||
self.config.register_set_function("enc_in_policy",
|
||||
self._on_set_encryption)
|
||||
self.config.register_set_function("enc_out_policy",
|
||||
self._on_set_encryption)
|
||||
self.config.register_set_function("enc_level",
|
||||
self._on_set_encryption)
|
||||
self.config.register_set_function("enc_prefer_rc4",
|
||||
self._on_set_encryption)
|
||||
self.config.register_set_function("max_connections_global",
|
||||
self._on_set_max_connections_global)
|
||||
self.config.register_set_function("max_upload_speed",
|
||||
self._on_set_max_upload_speed)
|
||||
self.config.register_set_function("max_download_speed",
|
||||
self._on_set_max_download_speed)
|
||||
self.config.register_set_function("max_upload_slots_global",
|
||||
self._on_set_max_upload_slots_global)
|
||||
self.config.register_set_function("max_half_open_connections",
|
||||
self._on_set_max_half_open_connections)
|
||||
self.config.register_set_function("max_connections_per_second",
|
||||
self._on_set_max_connections_per_second)
|
||||
self.config.register_set_function("ignore_limits_on_local_network",
|
||||
self._on_ignore_limits_on_local_network)
|
||||
self.config.register_set_function("share_ratio_limit",
|
||||
self._on_set_share_ratio_limit)
|
||||
self.config.register_set_function("seed_time_ratio_limit",
|
||||
self._on_set_seed_time_ratio_limit)
|
||||
self.config.register_set_function("seed_time_limit",
|
||||
self._on_set_seed_time_limit)
|
||||
self.config.register_set_function("max_active_downloading",
|
||||
self._on_set_max_active_downloading)
|
||||
self.config.register_set_function("max_active_seeding",
|
||||
self._on_set_max_active_seeding)
|
||||
self.config.register_set_function("max_active_limit",
|
||||
self._on_set_max_active_limit)
|
||||
self.config.register_set_function("dont_count_slow_torrents",
|
||||
self._on_set_dont_count_slow_torrents)
|
||||
self.config.register_set_function("send_info",
|
||||
self._on_send_info)
|
||||
self.config.register_set_function("proxies",
|
||||
self._on_set_proxies)
|
||||
self.new_release_timer = None
|
||||
self.config.register_set_function("new_release_check",
|
||||
self._on_new_release_check)
|
||||
self.config.register_set_function("rate_limit_ip_overhead",
|
||||
self._on_rate_limit_ip_overhead)
|
||||
self.config.register_set_function("geoip_db_location",
|
||||
self._on_geoip_db_location)
|
||||
self.config.register_set_function("cache_size",
|
||||
self._on_cache_size)
|
||||
self.config.register_set_function("cache_expiry",
|
||||
self._on_cache_expiry)
|
||||
|
||||
# Set the initial preferences on start-up
|
||||
for key in DEFAULT_PREFS:
|
||||
self.do_config_set_func(key, self.config[key])
|
||||
|
||||
self.config.register_change_callback(self._on_config_value_change)
|
||||
|
||||
def stop(self):
|
||||
if self.new_release_timer:
|
||||
if self.new_release_timer and self.new_release_timer.running:
|
||||
self.new_release_timer.stop()
|
||||
|
||||
# Config set functions
|
||||
def do_config_set_func(self, key, value):
|
||||
on_set_func = getattr(self, "_on_set_" + key, None)
|
||||
if on_set_func:
|
||||
on_set_func(key, value)
|
||||
|
||||
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):
|
||||
self.do_config_set_func(key, value)
|
||||
component.get("EventManager").emit(ConfigValueChangedEvent(key, value))
|
||||
|
||||
def _on_set_torrentfiles_location(self, key, value):
|
||||
@ -247,7 +196,9 @@ class PreferencesManager(component.Component):
|
||||
# Only set the listen ports if random_port is not true
|
||||
if self.config["random_port"] is not True:
|
||||
log.debug("listen port range set to %s-%s", value[0], value[1])
|
||||
self.session.listen_on(value[0], value[1], str(self.config["listen_interface"]))
|
||||
self.session.listen_on(
|
||||
value[0], value[1], str(self.config["listen_interface"])
|
||||
)
|
||||
|
||||
def _on_set_listen_interface(self, key, value):
|
||||
# Call the random_port callback since it'll do what we need
|
||||
@ -269,13 +220,15 @@ class PreferencesManager(component.Component):
|
||||
# Set the listen ports
|
||||
log.debug("listen port range set to %s-%s", listen_ports[0],
|
||||
listen_ports[1])
|
||||
self.session.listen_on(listen_ports[0], listen_ports[1], str(self.config["listen_interface"]))
|
||||
self.session.listen_on(
|
||||
listen_ports[0], listen_ports[1],
|
||||
str(self.config["listen_interface"])
|
||||
)
|
||||
|
||||
def _on_set_outgoing_ports(self, key, value):
|
||||
if not self.config["random_outgoing_ports"]:
|
||||
log.debug("outgoing port range set to %s-%s", value[0], value[1])
|
||||
self.settings.outgoing_ports = value[0], value[1]
|
||||
self.session.set_settings(self.settings)
|
||||
self.session_set_setting("outgoing_ports", (value[0], value[1]))
|
||||
|
||||
def _on_set_random_outgoing_ports(self, key, value):
|
||||
if value:
|
||||
@ -284,13 +237,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")
|
||||
@ -337,7 +288,22 @@ class PreferencesManager(component.Component):
|
||||
def _on_set_utpex(self, key, value):
|
||||
log.debug("utpex value set to %s", value)
|
||||
if value:
|
||||
self.session.add_extension(lt.create_ut_pex_plugin)
|
||||
# Note: All libtorrent python bindings to set plugins/extensions need to be disabled
|
||||
# due to GIL issue. https://code.google.com/p/libtorrent/issues/detail?id=369
|
||||
#self.session.add_extension(lt.create_ut_pex_plugin)
|
||||
pass
|
||||
|
||||
def _on_set_enc_in_policy(self, key, value):
|
||||
self._on_set_encryption(key, value)
|
||||
|
||||
def _on_set_enc_out_policy(self, key, value):
|
||||
self._on_set_encryption(key, value)
|
||||
|
||||
def _on_set_enc_level(self, key, value):
|
||||
self._on_set_encryption(key, value)
|
||||
|
||||
def _on_set_enc_prefer_rc4(self, key, value):
|
||||
self._on_set_encryption(key, value)
|
||||
|
||||
def _on_set_encryption(self, key, value):
|
||||
log.debug("encryption value %s set to %s..", key, value)
|
||||
@ -387,53 +353,41 @@ 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)
|
||||
def _on_set_ignore_limits_on_local_network(self, key, value):
|
||||
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):
|
||||
def _on_set_send_info(self, key, value):
|
||||
log.debug("Sending anonymous stats..")
|
||||
"""sends anonymous stats home"""
|
||||
class Send_Info_Thread(threading.Thread):
|
||||
@ -463,18 +417,18 @@ class PreferencesManager(component.Component):
|
||||
if value:
|
||||
Send_Info_Thread(self.config).start()
|
||||
|
||||
def _on_new_release_check(self, key, value):
|
||||
def _on_set_new_release_check(self, key, value):
|
||||
if value:
|
||||
log.debug("Checking for new release..")
|
||||
threading.Thread(target=self.core.get_new_release).start()
|
||||
if self.new_release_timer:
|
||||
if self.new_release_timer and self.new_release_timer.running:
|
||||
self.new_release_timer.stop()
|
||||
# Set a timer to check for a new release every 3 days
|
||||
self.new_release_timer = LoopingCall(
|
||||
self._on_new_release_check, "new_release_check", True)
|
||||
self._on_set_new_release_check, "new_release_check", True)
|
||||
self.new_release_timer.start(72 * 60 * 60, False)
|
||||
else:
|
||||
if self.new_release_timer:
|
||||
if self.new_release_timer and self.new_release_timer.running:
|
||||
self.new_release_timer.stop()
|
||||
|
||||
def _on_set_proxies(self, key, value):
|
||||
@ -489,19 +443,20 @@ class PreferencesManager(component.Component):
|
||||
log.debug("setting %s proxy settings", k)
|
||||
getattr(self.session, "set_%s_proxy" % k)(proxy_settings)
|
||||
|
||||
def _on_rate_limit_ip_overhead(self, key, value):
|
||||
def _on_set_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):
|
||||
def _on_set_geoip_db_location(self, key, value):
|
||||
log.debug("%s: %s", key, value)
|
||||
# Load the GeoIP DB for country look-ups if available
|
||||
geoip_db = ""
|
||||
if os.path.exists(value):
|
||||
geoip_db = value
|
||||
elif os.path.exists(pkg_resources.resource_filename("deluge", os.path.join("data", "GeoIP.dat"))):
|
||||
geoip_db = pkg_resources.resource_filename("deluge", os.path.join("data", "GeoIP.dat"))
|
||||
elif os.path.exists(deluge.common.resource_filename("deluge", os.path.join("data", "GeoIP.dat"))):
|
||||
geoip_db = deluge.common.resource_filename(
|
||||
"deluge", os.path.join("data", "GeoIP.dat")
|
||||
)
|
||||
else:
|
||||
log.warning("Unable to find GeoIP database file!")
|
||||
|
||||
@ -512,12 +467,14 @@ class PreferencesManager(component.Component):
|
||||
log.error("Unable to load geoip database!")
|
||||
log.exception(e)
|
||||
|
||||
def _on_cache_size(self, key, value):
|
||||
def _on_set_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):
|
||||
def _on_set_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)
|
||||
|
||||
def _on_auto_manage_prefer_seeds(self, key, value):
|
||||
log.debug("%s set to %s..", key, value)
|
||||
self.session_set_setting("auto_manage_prefer_seeds", value)
|
||||
|
@ -39,25 +39,35 @@ import sys
|
||||
import zlib
|
||||
import os
|
||||
import stat
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
from twisted.internet.protocol import Factory, Protocol
|
||||
from twisted.internet import ssl, reactor, defer
|
||||
from twisted.internet import reactor, defer
|
||||
|
||||
from OpenSSL import crypto, SSL
|
||||
from types import FunctionType
|
||||
|
||||
import deluge.rencode as rencode
|
||||
from deluge.log import LOG as log
|
||||
try:
|
||||
import rencode
|
||||
except ImportError:
|
||||
import deluge.rencode as rencode
|
||||
|
||||
import deluge.component as component
|
||||
import deluge.configmanager
|
||||
from deluge.core.authmanager import AUTH_LEVEL_NONE, AUTH_LEVEL_DEFAULT
|
||||
from deluge.core.authmanager import (AUTH_LEVEL_NONE, AUTH_LEVEL_DEFAULT,
|
||||
AUTH_LEVEL_ADMIN)
|
||||
from deluge.error import (DelugeError, NotAuthorizedError, WrappedException,
|
||||
_ClientSideRecreateError, IncompatibleClient)
|
||||
|
||||
from deluge.transfer import DelugeTransferProtocol
|
||||
|
||||
RPC_RESPONSE = 1
|
||||
RPC_ERROR = 2
|
||||
RPC_EVENT = 3
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def export(auth_level=AUTH_LEVEL_DEFAULT):
|
||||
"""
|
||||
Decorator function to register an object's method as an RPC. The object
|
||||
@ -90,13 +100,13 @@ def export(auth_level=AUTH_LEVEL_DEFAULT):
|
||||
def format_request(call):
|
||||
"""
|
||||
Format the RPCRequest message for debug printing
|
||||
|
||||
|
||||
:param call: the request
|
||||
:type call: a RPCRequest
|
||||
|
||||
|
||||
:returns: a formatted string for printing
|
||||
:rtype: str
|
||||
|
||||
|
||||
"""
|
||||
try:
|
||||
s = call[1] + "("
|
||||
@ -111,12 +121,6 @@ def format_request(call):
|
||||
return "UnicodeEncodeError, call: %s" % call
|
||||
else:
|
||||
return s
|
||||
|
||||
class DelugeError(Exception):
|
||||
pass
|
||||
|
||||
class NotAuthorizedError(DelugeError):
|
||||
pass
|
||||
|
||||
class ServerContextFactory(object):
|
||||
def getContext(self):
|
||||
@ -132,53 +136,34 @@ class ServerContextFactory(object):
|
||||
ctx.use_privatekey_file(os.path.join(ssl_dir, "daemon.pkey"))
|
||||
return ctx
|
||||
|
||||
class DelugeRPCProtocol(Protocol):
|
||||
__buffer = None
|
||||
class DelugeRPCProtocol(DelugeTransferProtocol):
|
||||
|
||||
def dataReceived(self, data):
|
||||
def message_received(self, request):
|
||||
"""
|
||||
This method is called whenever data is received from a client. The
|
||||
This method is called whenever a message is received from a client. The
|
||||
only message that a client sends to the server is a RPC Request message.
|
||||
If the RPC Request message is valid, then the method is called in
|
||||
If the RPC Request message is valid, then the method is called in
|
||||
:meth:`dispatch`.
|
||||
|
||||
:param data: the data from the client. It should be a zlib compressed
|
||||
rencoded string.
|
||||
:type data: str
|
||||
|
||||
:param request: the request from the client.
|
||||
:type data: tuple
|
||||
|
||||
"""
|
||||
if self.__buffer:
|
||||
# We have some data from the last dataReceived() so lets prepend it
|
||||
data = self.__buffer + data
|
||||
self.__buffer = None
|
||||
if type(request) is not tuple:
|
||||
log.debug("Received invalid message: type is not tuple")
|
||||
return
|
||||
|
||||
while data:
|
||||
dobj = zlib.decompressobj()
|
||||
try:
|
||||
request = rencode.loads(dobj.decompress(data))
|
||||
except Exception, 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
|
||||
return
|
||||
else:
|
||||
data = dobj.unused_data
|
||||
if len(request) < 1:
|
||||
log.debug("Received invalid message: there are no items")
|
||||
return
|
||||
|
||||
if type(request) is not tuple:
|
||||
log.debug("Received invalid message: type is not tuple")
|
||||
return
|
||||
|
||||
if len(request) < 1:
|
||||
log.debug("Received invalid message: there are no items")
|
||||
return
|
||||
|
||||
for call in request:
|
||||
if len(call) != 4:
|
||||
log.debug("Received invalid rpc request: number of items in request is %s", len(call))
|
||||
continue
|
||||
#log.debug("RPCRequest: %s", format_request(call))
|
||||
reactor.callLater(0, self.dispatch, *call)
|
||||
for call in request:
|
||||
if len(call) != 4:
|
||||
log.debug("Received invalid rpc request: number of items "
|
||||
"in request is %s", len(call))
|
||||
continue
|
||||
#log.debug("RPCRequest: %s", format_request(call))
|
||||
reactor.callLater(0, self.dispatch, *call)
|
||||
|
||||
def sendData(self, data):
|
||||
"""
|
||||
@ -187,16 +172,17 @@ 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)))
|
||||
self.transfer_message(data)
|
||||
|
||||
def connectionMade(self):
|
||||
"""
|
||||
This method is called when a new client connects.
|
||||
"""
|
||||
peer = self.transport.getPeer()
|
||||
log.info("Deluge Client connection made from: %s:%s", peer.host, peer.port)
|
||||
log.info("Deluge Client connection made from: %s:%s",
|
||||
peer.host, peer.port)
|
||||
# Set the initial auth level of this session to AUTH_LEVEL_NONE
|
||||
self.factory.authorized_sessions[self.transport.sessionno] = AUTH_LEVEL_NONE
|
||||
|
||||
@ -218,6 +204,9 @@ class DelugeRPCProtocol(Protocol):
|
||||
|
||||
log.info("Deluge client disconnected: %s", reason.value)
|
||||
|
||||
def valid_session(self):
|
||||
return self.transport.sessionno in self.factory.authorized_sessions
|
||||
|
||||
def dispatch(self, request_id, method, args, kwargs):
|
||||
"""
|
||||
This method is run when a RPC Request is made. It will run the local method
|
||||
@ -239,33 +228,55 @@ class DelugeRPCProtocol(Protocol):
|
||||
Sends an error response with the contents of the exception that was raised.
|
||||
"""
|
||||
exceptionType, exceptionValue, exceptionTraceback = sys.exc_info()
|
||||
formated_tb = "".join(traceback.format_tb(exceptionTraceback))
|
||||
try:
|
||||
self.sendData((
|
||||
RPC_ERROR,
|
||||
request_id,
|
||||
exceptionType.__name__,
|
||||
exceptionValue._args,
|
||||
exceptionValue._kwargs,
|
||||
formated_tb
|
||||
))
|
||||
except Exception, err:
|
||||
# This most likely not a deluge exception, let's wrap it
|
||||
log.error("An exception occurred while sending RPC_ERROR to "
|
||||
"client. Wrapping it and resending. Error to "
|
||||
"send(causing exception goes next):\n%s", formated_tb)
|
||||
log.exception(err)
|
||||
try:
|
||||
raise WrappedException(str(exceptionValue), exceptionType.__name__, formated_tb)
|
||||
except:
|
||||
sendError()
|
||||
|
||||
self.sendData((
|
||||
RPC_ERROR,
|
||||
request_id,
|
||||
(exceptionType.__name__,
|
||||
exceptionValue.args[0] if len(exceptionValue.args) == 1 else "",
|
||||
"".join(traceback.format_tb(exceptionTraceback)))
|
||||
))
|
||||
|
||||
if method == "daemon.login":
|
||||
if method == "daemon.info":
|
||||
# This is a special case and used in the initial connection process
|
||||
self.sendData((RPC_RESPONSE, request_id, deluge.common.get_version()))
|
||||
return
|
||||
elif method == "daemon.login":
|
||||
# This is a special case and used in the initial connection process
|
||||
# We need to authenticate the user here
|
||||
log.debug("RPC dispatch daemon.login")
|
||||
try:
|
||||
client_version = kwargs.pop('client_version', None)
|
||||
if client_version is None:
|
||||
raise IncompatibleClient(deluge.common.get_version())
|
||||
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()
|
||||
log.exception(e)
|
||||
if not isinstance(e, _ClientSideRecreateError):
|
||||
log.exception(e)
|
||||
else:
|
||||
self.sendData((RPC_RESPONSE, request_id, (ret)))
|
||||
if not ret:
|
||||
self.transport.loseConnection()
|
||||
finally:
|
||||
return
|
||||
elif method == "daemon.set_event_interest" and self.transport.sessionno in self.factory.authorized_sessions:
|
||||
elif method == "daemon.set_event_interest" and self.valid_session():
|
||||
log.debug("RPC dispatch daemon.set_event_interest")
|
||||
# This special case is to allow clients to set which events they are
|
||||
# interested in receiving.
|
||||
# We are expecting a sequence from the client.
|
||||
@ -280,21 +291,24 @@ class DelugeRPCProtocol(Protocol):
|
||||
finally:
|
||||
return
|
||||
|
||||
if method in self.factory.methods and self.transport.sessionno in self.factory.authorized_sessions:
|
||||
if method in self.factory.methods and self.valid_session():
|
||||
log.debug("RPC dispatch %s", method)
|
||||
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))
|
||||
log.debug("Session %s is trying to call a method it is not "
|
||||
"authorized to call!", self.transport.sessionno)
|
||||
raise NotAuthorizedError(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()
|
||||
# Don't bother printing out DelugeErrors, because they are just for the client
|
||||
# Don't bother printing out DelugeErrors, because they are just
|
||||
# for the client
|
||||
if not isinstance(e, DelugeError):
|
||||
log.exception("Exception calling RPC request: %s", e)
|
||||
else:
|
||||
@ -338,7 +352,7 @@ class RPCServer(component.Component):
|
||||
self.factory = Factory()
|
||||
self.factory.protocol = DelugeRPCProtocol
|
||||
self.factory.session_id = -1
|
||||
|
||||
|
||||
# Holds the registered methods
|
||||
self.factory.methods = {}
|
||||
# Holds the session_ids and auth levels
|
||||
@ -348,6 +362,7 @@ class RPCServer(component.Component):
|
||||
# Holds the interested event list for the sessions
|
||||
self.factory.interested_events = {}
|
||||
|
||||
self.listen = listen
|
||||
if not listen:
|
||||
return
|
||||
|
||||
@ -391,6 +406,17 @@ class RPCServer(component.Component):
|
||||
log.debug("Registering method: %s", name + "." + d)
|
||||
self.factory.methods[name + "." + d] = getattr(obj, d)
|
||||
|
||||
def deregister_object(self, obj):
|
||||
"""
|
||||
Deregisters an objects exported rpc methods.
|
||||
|
||||
:param obj: the object that was previously registered
|
||||
|
||||
"""
|
||||
for key, value in self.factory.methods.items():
|
||||
if value.im_self == obj:
|
||||
del self.factory.methods[key]
|
||||
|
||||
def get_object_method(self, name):
|
||||
"""
|
||||
Returns a registered method.
|
||||
@ -417,26 +443,63 @@ class RPCServer(component.Component):
|
||||
def get_session_id(self):
|
||||
"""
|
||||
Returns the session id of the current RPC.
|
||||
|
||||
|
||||
:returns: the session id, this will be -1 if no connections have been made
|
||||
:rtype: int
|
||||
|
||||
"""
|
||||
return self.factory.session_id
|
||||
|
||||
|
||||
def get_session_user(self):
|
||||
"""
|
||||
Returns the username calling the current RPC.
|
||||
|
||||
:returns: the username of the user calling the current RPC
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
if not self.listen:
|
||||
return "localclient"
|
||||
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 get_session_auth_level(self):
|
||||
"""
|
||||
Returns the auth level of the user calling the current RPC.
|
||||
|
||||
:returns: the auth level
|
||||
:rtype: int
|
||||
"""
|
||||
if not self.listen or not self.is_session_valid(self.get_session_id()):
|
||||
return AUTH_LEVEL_ADMIN
|
||||
return self.factory.authorized_sessions[self.get_session_id()][0]
|
||||
|
||||
def get_rpc_auth_level(self, rpc):
|
||||
"""
|
||||
Returns the auth level requirement for an exported rpc.
|
||||
|
||||
:returns: the auth level
|
||||
:rtype: int
|
||||
"""
|
||||
self.factory.methods[rpc]._rpcserver_auth_level
|
||||
|
||||
def is_session_valid(self, session_id):
|
||||
"""
|
||||
Checks if the session is still valid, eg, if the client is still connected.
|
||||
|
||||
|
||||
:param session_id: the session id
|
||||
:type session_id: int
|
||||
|
||||
|
||||
:returns: True if the session is valid
|
||||
:rtype: bool
|
||||
|
||||
|
||||
"""
|
||||
return session_id in self.factory.authorized_sessions
|
||||
|
||||
|
||||
def emit_event(self, event):
|
||||
"""
|
||||
Emits the event to interested clients.
|
||||
@ -446,7 +509,7 @@ class RPCServer(component.Component):
|
||||
"""
|
||||
log.debug("intevents: %s", self.factory.interested_events)
|
||||
# Find sessions interested in this event
|
||||
for session_id, interest in self.factory.interested_events.iteritems():
|
||||
for session_id, interest in self.factory.interested_events.items():
|
||||
if event.name in interest:
|
||||
log.debug("Emit Event: %s %s", event.name, event.args)
|
||||
# This session is interested so send a RPC_EVENT
|
||||
@ -454,6 +517,29 @@ class RPCServer(component.Component):
|
||||
(RPC_EVENT, event.name, event.args)
|
||||
)
|
||||
|
||||
def emit_event_for_session_id(self, session_id, event):
|
||||
"""
|
||||
Emits the event to specified session_id.
|
||||
|
||||
:param session_id: the event to emit
|
||||
:type session_id: int
|
||||
:param event: the event to emit
|
||||
:type event: :class:`deluge.event.DelugeEvent`
|
||||
"""
|
||||
if not self.is_session_valid(session_id):
|
||||
log.debug("Session ID %s is not valid. Not sending event \"%s\".", session_id, event.name)
|
||||
return
|
||||
if session_id not in self.factory.interested_events:
|
||||
log.debug("Session ID %s is not interested in any events. Not sending event \"%s\".", session_id, event.name)
|
||||
return
|
||||
if event.name not in self.factory.interested_events[session_id]:
|
||||
log.debug("Session ID %s is not interested in event \"%s\". Not sending it.", session_id, event.name)
|
||||
return
|
||||
log.debug("Sending event \"%s\" with args \"%s\" to session id \"%s\".",
|
||||
event.name, event.args, session_id)
|
||||
self.factory.session_protocols[session_id].sendData((RPC_EVENT, event.name, event.args))
|
||||
|
||||
|
||||
def check_ssl_keys():
|
||||
"""
|
||||
Check for SSL cert/key and create them if necessary
|
||||
@ -497,8 +583,12 @@ def generate_ssl_keys():
|
||||
|
||||
# Write out files
|
||||
ssl_dir = deluge.configmanager.get_config_dir("ssl")
|
||||
open(os.path.join(ssl_dir, "daemon.pkey"), "w").write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
|
||||
open(os.path.join(ssl_dir, "daemon.cert"), "w").write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
||||
open(os.path.join(ssl_dir, "daemon.pkey"), "w").write(
|
||||
crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
|
||||
)
|
||||
open(os.path.join(ssl_dir, "daemon.cert"), "w").write(
|
||||
crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
|
||||
)
|
||||
# Make the files only readable by this user
|
||||
for f in ("daemon.pkey", "daemon.cert"):
|
||||
os.chmod(os.path.join(ssl_dir, f), stat.S_IREAD | stat.S_IWRITE)
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
@ -38,27 +38,28 @@
|
||||
|
||||
import cPickle
|
||||
import os
|
||||
import time
|
||||
import shutil
|
||||
import operator
|
||||
import logging
|
||||
import time
|
||||
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet.task import LoopingCall
|
||||
from twisted.internet.defer import Deferred, DeferredList
|
||||
from twisted.internet import reactor
|
||||
|
||||
from deluge._libtorrent import lt
|
||||
|
||||
|
||||
from deluge.event import *
|
||||
from deluge.error import *
|
||||
import deluge.common
|
||||
import deluge.component as component
|
||||
from deluge.configmanager import ConfigManager, get_config_dir
|
||||
from deluge.core.authmanager import AUTH_LEVEL_ADMIN
|
||||
from deluge.core.torrent import Torrent
|
||||
from deluge.core.torrent import TorrentOptions
|
||||
import deluge.core.oldstateupgrader
|
||||
from deluge.ui.common import utf8_encoded
|
||||
from deluge.common import utf8_encoded, decode_string
|
||||
|
||||
from deluge.log import LOG as log
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class TorrentState:
|
||||
def __init__(self,
|
||||
@ -74,6 +75,7 @@ class TorrentState:
|
||||
max_upload_speed=-1.0,
|
||||
max_download_speed=-1.0,
|
||||
prioritize_first_last=False,
|
||||
sequential_download=False,
|
||||
file_priorities=None,
|
||||
queue=None,
|
||||
auto_managed=True,
|
||||
@ -84,7 +86,10 @@ class TorrentState:
|
||||
move_completed=False,
|
||||
move_completed_path=None,
|
||||
magnet=None,
|
||||
time_added=-1
|
||||
time_added=-1,
|
||||
last_seen_complete=0,
|
||||
owner="",
|
||||
shared=False
|
||||
):
|
||||
self.torrent_id = torrent_id
|
||||
self.filename = filename
|
||||
@ -94,6 +99,8 @@ class TorrentState:
|
||||
self.is_finished = is_finished
|
||||
self.magnet = magnet
|
||||
self.time_added = time_added
|
||||
self.last_seen_complete = last_seen_complete
|
||||
self.owner = owner
|
||||
|
||||
# Options
|
||||
self.compact = compact
|
||||
@ -104,6 +111,7 @@ class TorrentState:
|
||||
self.max_upload_speed = max_upload_speed
|
||||
self.max_download_speed = max_download_speed
|
||||
self.prioritize_first_last = prioritize_first_last
|
||||
self.sequential_download = sequential_download
|
||||
self.file_priorities = file_priorities
|
||||
self.auto_managed = auto_managed
|
||||
self.stop_ratio = stop_ratio
|
||||
@ -111,6 +119,7 @@ class TorrentState:
|
||||
self.remove_at_ratio = remove_at_ratio
|
||||
self.move_completed = move_completed
|
||||
self.move_completed_path = move_completed_path
|
||||
self.shared = shared
|
||||
|
||||
class TorrentManagerState:
|
||||
def __init__(self):
|
||||
@ -124,7 +133,8 @@ class TorrentManager(component.Component):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
component.Component.__init__(self, "TorrentManager", interval=5, depend=["CorePluginManager"])
|
||||
component.Component.__init__(self, "TorrentManager", interval=5,
|
||||
depend=["CorePluginManager", "AlertManager"])
|
||||
log.debug("TorrentManager init..")
|
||||
# Set the libtorrent session
|
||||
self.session = component.get("Core").session
|
||||
@ -139,18 +149,19 @@ class TorrentManager(component.Component):
|
||||
|
||||
# Create the torrents dict { torrent_id: Torrent }
|
||||
self.torrents = {}
|
||||
self.queued_torrents = set()
|
||||
|
||||
# This is a list of torrent_id when we shutdown the torrentmanager.
|
||||
# We use this list to determine if all active torrents have been paused
|
||||
# and that their resume data has been written.
|
||||
self.shutdown_torrent_pause_list = []
|
||||
# This is a map of torrent_ids to Deferreds used to track needed resume data.
|
||||
# The Deferreds will be completed when resume data has been saved.
|
||||
self.waiting_on_resume_data = {}
|
||||
|
||||
# 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
|
||||
# Keeps track of resume data
|
||||
self.resume_data = {}
|
||||
|
||||
self.torrents_status_requests = []
|
||||
self.status_dict = {}
|
||||
self.last_state_update_alert_ts = 0
|
||||
|
||||
# Register set functions
|
||||
self.config.register_set_function("max_connections_per_torrent",
|
||||
self.on_set_max_connections_per_torrent)
|
||||
@ -192,6 +203,10 @@ class TorrentManager(component.Component):
|
||||
self.on_alert_metadata_received)
|
||||
self.alerts.register_handler("file_error_alert",
|
||||
self.on_alert_file_error)
|
||||
self.alerts.register_handler("file_completed_alert",
|
||||
self.on_alert_file_completed)
|
||||
self.alerts.register_handler("state_update_alert",
|
||||
self.on_alert_state_update)
|
||||
|
||||
def start(self):
|
||||
# Get the pluginmanager reference
|
||||
@ -203,11 +218,14 @@ class TorrentManager(component.Component):
|
||||
# Try to load the state from file
|
||||
self.load_state()
|
||||
|
||||
# Save the state every 5 minutes
|
||||
# Save the state periodically
|
||||
self.save_state_timer = LoopingCall(self.save_state)
|
||||
self.save_state_timer.start(200, False)
|
||||
self.save_resume_data_timer = LoopingCall(self.save_resume_data)
|
||||
self.save_resume_data_timer.start(190)
|
||||
self.save_resume_data_timer.start(190, False)
|
||||
# Force update for all resume data a bit less frequently
|
||||
self.save_all_resume_data_timer = LoopingCall(self.save_resume_data, self.torrents.keys())
|
||||
self.save_all_resume_data_timer.start(900, False)
|
||||
|
||||
def stop(self):
|
||||
# Stop timers
|
||||
@ -217,55 +235,31 @@ class TorrentManager(component.Component):
|
||||
if self.save_resume_data_timer.running:
|
||||
self.save_resume_data_timer.stop()
|
||||
|
||||
if self.save_all_resume_data_timer.running:
|
||||
self.save_all_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
|
||||
# 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 = []
|
||||
self.session.pause()
|
||||
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:
|
||||
if self.shutdown_torrent_pause_list:
|
||||
wait = True
|
||||
else:
|
||||
wait = False
|
||||
for torrent in self.torrents.values():
|
||||
if torrent.waiting_on_resume_data:
|
||||
wait = True
|
||||
break
|
||||
|
||||
time.sleep(0.01)
|
||||
# Wait for all alerts
|
||||
self.alerts.handle_alerts(True)
|
||||
return self.save_resume_data(self.torrents.keys())
|
||||
|
||||
def update(self):
|
||||
for torrent_id, torrent in self.torrents.items():
|
||||
if self.config["stop_seed_at_ratio"] or torrent.options["stop_at_ratio"] and torrent.state not in ("Checking", "Allocating", "Paused", "Queued"):
|
||||
# If the global setting is set, but the per-torrent isn't.. Just skip to the next torrent
|
||||
# This is so that a user can turn-off the stop at ratio option on a per-torrent basis
|
||||
if self.config["stop_seed_at_ratio"] and not torrent.options["stop_at_ratio"]:
|
||||
if 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
|
||||
stop_ratio = self.config["stop_seed_ratio"]
|
||||
if torrent.options["stop_at_ratio"]:
|
||||
stop_ratio = torrent.options["stop_ratio"]
|
||||
if torrent.get_ratio() >= stop_ratio and torrent.is_finished:
|
||||
if self.config["remove_seed_at_ratio"] or torrent.options["remove_at_ratio"]:
|
||||
if torrent.get_ratio() >= torrent.options["stop_ratio"] and torrent.is_finished:
|
||||
if torrent.options["remove_at_ratio"]:
|
||||
self.remove(torrent_id)
|
||||
break
|
||||
if not torrent.handle.is_paused():
|
||||
@ -277,14 +271,24 @@ class TorrentManager(component.Component):
|
||||
|
||||
def get_torrent_list(self):
|
||||
"""Returns a list of torrent_ids"""
|
||||
return self.torrents.keys()
|
||||
torrent_ids = self.torrents.keys()
|
||||
if component.get("RPCServer").get_session_auth_level() == AUTH_LEVEL_ADMIN:
|
||||
return torrent_ids
|
||||
|
||||
current_user = component.get("RPCServer").get_session_user()
|
||||
for torrent_id in torrent_ids[:]:
|
||||
torrent_status = self[torrent_id].get_status(["owner", "shared"])
|
||||
if torrent_status["owner"] != current_user and torrent_status["shared"] == False:
|
||||
torrent_ids.pop(torrent_ids.index(torrent_id))
|
||||
return torrent_ids
|
||||
|
||||
def get_torrent_info_from_file(self, filepath):
|
||||
"""Returns a torrent_info for the file specified or None"""
|
||||
torrent_info = None
|
||||
# Get the torrent data from the torrent file
|
||||
try:
|
||||
log.debug("Attempting to create torrent_info from %s", filepath)
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug("Attempting to create torrent_info from %s", filepath)
|
||||
_file = open(filepath, "rb")
|
||||
torrent_info = lt.torrent_info(lt.bdecode(_file.read()))
|
||||
_file.close()
|
||||
@ -317,14 +321,17 @@ class TorrentManager(component.Component):
|
||||
log.warning("Unable to delete the fastresume file: %s", e)
|
||||
|
||||
def add(self, torrent_info=None, state=None, options=None, save_state=True,
|
||||
filedump=None, filename=None, magnet=None, resume_data=None):
|
||||
filedump=None, filename=None, magnet=None, resume_data=None, owner=None):
|
||||
"""Add a torrent to the manager and returns it's torrent_id"""
|
||||
if owner is None:
|
||||
owner = component.get("RPCServer").get_session_user()
|
||||
if not owner:
|
||||
owner = "localclient"
|
||||
|
||||
if torrent_info is None and state is None and filedump is None and magnet is None:
|
||||
log.debug("You must specify a valid torrent_info, torrent state or magnet.")
|
||||
return
|
||||
|
||||
log.debug("torrentmanager.add")
|
||||
add_torrent_params = {}
|
||||
|
||||
if filedump is not None:
|
||||
@ -346,6 +353,7 @@ class TorrentManager(component.Component):
|
||||
options["max_upload_speed"] = state.max_upload_speed
|
||||
options["max_download_speed"] = state.max_download_speed
|
||||
options["prioritize_first_last_pieces"] = state.prioritize_first_last
|
||||
options["sequential_download"] = state.sequential_download
|
||||
options["file_priorities"] = state.file_priorities
|
||||
options["compact_allocation"] = state.compact
|
||||
options["download_location"] = state.save_path
|
||||
@ -356,6 +364,7 @@ class TorrentManager(component.Component):
|
||||
options["move_completed"] = state.move_completed
|
||||
options["move_completed_path"] = state.move_completed_path
|
||||
options["add_paused"] = state.paused
|
||||
options["shared"] = state.shared
|
||||
|
||||
ti = self.get_torrent_info_from_file(
|
||||
os.path.join(get_config_dir(),
|
||||
@ -374,9 +383,38 @@ class TorrentManager(component.Component):
|
||||
resume_data = self.legacy_get_resume_data_from_file(state.torrent_id)
|
||||
self.legacy_delete_resume_data(state.torrent_id)
|
||||
|
||||
add_torrent_params["resume_data"] = resume_data
|
||||
if resume_data:
|
||||
add_torrent_params["resume_data"] = resume_data
|
||||
else:
|
||||
# We have a torrent_info object so we're not loading from state.
|
||||
# We have a torrent_info object or magnet uri so we're not loading from state.
|
||||
if torrent_info:
|
||||
add_torrent_id = str(torrent_info.info_hash())
|
||||
if add_torrent_id in self.get_torrent_list():
|
||||
# Torrent already exists just append any extra trackers.
|
||||
log.debug("Torrent (%s) exists, checking for trackers to add...", add_torrent_id)
|
||||
add_torrent_trackers = []
|
||||
for value in torrent_info.trackers():
|
||||
tracker = {}
|
||||
tracker["url"] = value.url
|
||||
tracker["tier"] = value.tier
|
||||
add_torrent_trackers.append(tracker)
|
||||
|
||||
torrent_trackers = {}
|
||||
tracker_list = []
|
||||
for tracker in self[add_torrent_id].get_status(["trackers"])["trackers"]:
|
||||
torrent_trackers[(tracker["url"])] = tracker
|
||||
tracker_list.append(tracker)
|
||||
|
||||
added_tracker = False
|
||||
for tracker in add_torrent_trackers:
|
||||
if tracker['url'] not in torrent_trackers:
|
||||
tracker_list.append(tracker)
|
||||
added_tracker = True
|
||||
|
||||
if added_tracker:
|
||||
self[add_torrent_id].set_trackers(tracker_list)
|
||||
return
|
||||
|
||||
# Check if options is None and load defaults
|
||||
if options == None:
|
||||
options = TorrentOptions()
|
||||
@ -388,15 +426,22 @@ class TorrentManager(component.Component):
|
||||
# Check for renamed files and if so, rename them in the torrent_info
|
||||
# before adding to the session.
|
||||
if options["mapped_files"]:
|
||||
for index, name in options["mapped_files"].items():
|
||||
log.debug("renaming file index %s to %s", index, name)
|
||||
torrent_info.rename_file(index, utf8_encoded(name))
|
||||
for index, fname in options["mapped_files"].items():
|
||||
try:
|
||||
fname = unicode(fname, "utf-8")
|
||||
except TypeError:
|
||||
pass
|
||||
fname = deluge.core.torrent.sanitize_filepath(fname)
|
||||
log.debug("renaming file index %s to %s", index, fname)
|
||||
try:
|
||||
torrent_info.rename_file(index, fname)
|
||||
except TypeError:
|
||||
torrent_info.rename_file(index, fname.encode("utf-8"))
|
||||
|
||||
add_torrent_params["ti"] = torrent_info
|
||||
add_torrent_params["resume_data"] = ""
|
||||
|
||||
#log.info("Adding torrent: %s", filename)
|
||||
log.debug("options: %s", options)
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug("options: %s", options)
|
||||
|
||||
# Set the right storage_mode
|
||||
if options["compact_allocation"]:
|
||||
@ -418,7 +463,7 @@ class TorrentManager(component.Component):
|
||||
handle = None
|
||||
try:
|
||||
if magnet:
|
||||
handle = lt.add_magnet_uri(self.session, magnet, add_torrent_params)
|
||||
handle = lt.add_magnet_uri(self.session, utf8_encoded(magnet), add_torrent_params)
|
||||
else:
|
||||
handle = self.session.add_torrent(add_torrent_params)
|
||||
except RuntimeError, e:
|
||||
@ -430,11 +475,19 @@ class TorrentManager(component.Component):
|
||||
component.resume("AlertManager")
|
||||
return
|
||||
|
||||
log.debug("handle id: %s", str(handle.info_hash()))
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug("handle id: %s", str(handle.info_hash()))
|
||||
# Set auto_managed to False because the torrent is paused
|
||||
handle.auto_managed(False)
|
||||
# Create a Torrent object
|
||||
torrent = Torrent(handle, options, state, filename, magnet)
|
||||
owner = state.owner if state else (
|
||||
owner if owner else component.get("RPCServer").get_session_user()
|
||||
)
|
||||
account_exists = component.get("AuthManager").has_account(owner)
|
||||
if not account_exists:
|
||||
owner = 'localclient'
|
||||
torrent = Torrent(handle, options, state, filename, magnet, owner)
|
||||
|
||||
# Add the torrent object to the dictionary
|
||||
self.torrents[torrent.torrent_id] = torrent
|
||||
if self.config["queue_new_to_top"]:
|
||||
@ -446,6 +499,9 @@ class TorrentManager(component.Component):
|
||||
if not options["add_paused"]:
|
||||
torrent.resume()
|
||||
|
||||
# Add to queued torrents set
|
||||
self.queued_torrents.add(torrent.torrent_id)
|
||||
|
||||
# Write the .torrent file to the state directory
|
||||
if filedump:
|
||||
try:
|
||||
@ -473,9 +529,19 @@ class TorrentManager(component.Component):
|
||||
# Save the session state
|
||||
self.save_state()
|
||||
|
||||
# Emit the torrent_added signal
|
||||
component.get("EventManager").emit(TorrentAddedEvent(torrent.torrent_id))
|
||||
# Emit torrent_added signal
|
||||
from_state = state is not None
|
||||
component.get("EventManager").emit(
|
||||
TorrentAddedEvent(torrent.torrent_id, from_state)
|
||||
)
|
||||
|
||||
if log.isEnabledFor(logging.INFO):
|
||||
name_and_owner = torrent.get_status(["name", "owner"])
|
||||
log.info("Torrent %s from user \"%s\" %s" % (
|
||||
name_and_owner["name"],
|
||||
name_and_owner["owner"],
|
||||
from_state and "loaded" or "added")
|
||||
)
|
||||
return torrent.torrent_id
|
||||
|
||||
def load_torrent(self, torrent_id):
|
||||
@ -511,8 +577,9 @@ class TorrentManager(component.Component):
|
||||
:raises InvalidTorrentError: if the torrent_id is not in the session
|
||||
|
||||
"""
|
||||
|
||||
if torrent_id not in self.torrents:
|
||||
try:
|
||||
torrent_name = self.torrents[torrent_id].get_status(["name"])["name"]
|
||||
except KeyError:
|
||||
raise InvalidTorrentError("torrent_id not in session")
|
||||
|
||||
# Emit the signal to the clients
|
||||
@ -526,9 +593,7 @@ class TorrentManager(component.Component):
|
||||
return False
|
||||
|
||||
# Remove fastresume data if it is exists
|
||||
resume_data = self.load_resume_data_file()
|
||||
resume_data.pop(torrent_id, None)
|
||||
self.save_resume_data_file(resume_data)
|
||||
self.resume_data.pop(torrent_id, None)
|
||||
|
||||
# Remove the .torrent file in the state
|
||||
self.torrents[torrent_id].delete_torrentfile()
|
||||
@ -551,10 +616,17 @@ class TorrentManager(component.Component):
|
||||
# Stop the looping call
|
||||
self.torrents[torrent_id].prev_status_cleanup_loop.stop()
|
||||
|
||||
# Remove from set if it wasn't finished
|
||||
if not self.torrents[torrent_id].is_finished:
|
||||
try:
|
||||
self.queued_torrents.remove(torrent_id)
|
||||
except KeyError:
|
||||
log.debug("%s isn't in queued torrents set?", torrent_id)
|
||||
|
||||
# Remove the torrent from deluge's session
|
||||
try:
|
||||
del self.torrents[torrent_id]
|
||||
except KeyError, ValueError:
|
||||
except (KeyError, ValueError):
|
||||
return False
|
||||
|
||||
# Save the session state
|
||||
@ -562,7 +634,8 @@ 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):
|
||||
@ -575,33 +648,41 @@ class TorrentManager(component.Component):
|
||||
os.path.join(get_config_dir(), "state", "torrents.state"), "rb")
|
||||
state = cPickle.load(state_file)
|
||||
state_file.close()
|
||||
except (EOFError, IOError, Exception), e:
|
||||
except (EOFError, IOError, Exception, cPickle.UnpicklingError), e:
|
||||
log.warning("Unable to load state file: %s", e)
|
||||
|
||||
# Try to use an old state
|
||||
try:
|
||||
state_tmp = TorrentState()
|
||||
if dir(state.torrents[0]) != dir(state_tmp):
|
||||
for attr in (set(dir(state_tmp)) - set(dir(state.torrents[0]))):
|
||||
for s in state.torrents:
|
||||
setattr(s, attr, getattr(state_tmp, attr, None))
|
||||
if len(state.torrents) > 0:
|
||||
state_tmp = TorrentState()
|
||||
if dir(state.torrents[0]) != dir(state_tmp):
|
||||
for attr in (set(dir(state_tmp)) - set(dir(state.torrents[0]))):
|
||||
for s in state.torrents:
|
||||
setattr(s, attr, getattr(state_tmp, attr, None))
|
||||
except Exception, e:
|
||||
log.warning("Unable to update state file to a compatible version: %s", e)
|
||||
log.exception("Unable to update state file to a compatible version: %s", e)
|
||||
|
||||
# Reorder the state.torrents list to add torrents in the correct queue
|
||||
# order.
|
||||
state.torrents.sort(key=operator.attrgetter("queue"))
|
||||
|
||||
state.torrents.sort(key=operator.attrgetter("queue"), reverse=self.config["queue_new_to_top"])
|
||||
resume_data = self.load_resume_data_file()
|
||||
|
||||
# Tell alertmanager to wait for the handlers while adding torrents.
|
||||
# This speeds up startup loading the torrents by quite a lot for some reason (~40%)
|
||||
self.alerts.wait_on_handler = True
|
||||
|
||||
for torrent_state in state.torrents:
|
||||
try:
|
||||
self.add(state=torrent_state, save_state=False,
|
||||
resume_data=resume_data.get(torrent_state.torrent_id))
|
||||
except AttributeError, e:
|
||||
log.error("Torrent state file is either corrupt or incompatible! %s", e)
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
break
|
||||
|
||||
self.alerts.wait_on_handler = False
|
||||
|
||||
component.get("EventManager").emit(SessionStartedEvent())
|
||||
|
||||
def save_state(self):
|
||||
@ -613,10 +694,16 @@ class TorrentManager(component.Component):
|
||||
if torrent.state == "Paused":
|
||||
paused = True
|
||||
|
||||
torrent_status = torrent.get_status([
|
||||
"total_uploaded",
|
||||
"last_seen_complete"
|
||||
], update=True
|
||||
)
|
||||
|
||||
torrent_state = TorrentState(
|
||||
torrent.torrent_id,
|
||||
torrent.filename,
|
||||
torrent.get_status(["total_uploaded"])["total_uploaded"],
|
||||
torrent_status["total_uploaded"],
|
||||
torrent.trackers,
|
||||
torrent.options["compact_allocation"],
|
||||
paused,
|
||||
@ -626,6 +713,7 @@ class TorrentManager(component.Component):
|
||||
torrent.options["max_upload_speed"],
|
||||
torrent.options["max_download_speed"],
|
||||
torrent.options["prioritize_first_last_pieces"],
|
||||
torrent.options["sequential_download"],
|
||||
torrent.options["file_priorities"],
|
||||
torrent.get_queue_position(),
|
||||
torrent.options["auto_managed"],
|
||||
@ -636,7 +724,10 @@ class TorrentManager(component.Component):
|
||||
torrent.options["move_completed"],
|
||||
torrent.options["move_completed_path"],
|
||||
torrent.magnet,
|
||||
torrent.time_added
|
||||
torrent.time_added,
|
||||
torrent_status["last_seen_complete"],
|
||||
torrent.owner,
|
||||
torrent.options["shared"]
|
||||
)
|
||||
state.torrents.append(torrent_state)
|
||||
|
||||
@ -649,8 +740,8 @@ class TorrentManager(component.Component):
|
||||
state_file.flush()
|
||||
os.fsync(state_file.fileno())
|
||||
state_file.close()
|
||||
except IOError:
|
||||
log.warning("Unable to save state file.")
|
||||
except IOError, e:
|
||||
log.warning("Unable to save state file: %s", e)
|
||||
return True
|
||||
|
||||
# We have to move the 'torrents.state.new' file to 'torrents.state'
|
||||
@ -667,17 +758,34 @@ class TorrentManager(component.Component):
|
||||
|
||||
def save_resume_data(self, torrent_ids=None):
|
||||
"""
|
||||
Saves resume data for list of torrent_ids or for all torrents if
|
||||
torrent_ids is None
|
||||
Saves resume data for list of torrent_ids or for all torrents
|
||||
needing resume data updated if torrent_ids is None
|
||||
|
||||
:returns: A Deferred whose callback will be invoked when save is complete
|
||||
:rtype: twisted.internet.defer.Deferred
|
||||
"""
|
||||
|
||||
if torrent_ids is None:
|
||||
torrent_ids = self.torrents.keys()
|
||||
torrent_ids = (t[0] for t in self.torrents.iteritems() if t[1].handle.need_save_resume_data())
|
||||
|
||||
deferreds = []
|
||||
|
||||
def on_torrent_resume_save(result, torrent_id):
|
||||
self.waiting_on_resume_data.pop(torrent_id, None)
|
||||
|
||||
for torrent_id in torrent_ids:
|
||||
d = self.waiting_on_resume_data.get(torrent_id)
|
||||
if not d:
|
||||
d = Deferred().addBoth(on_torrent_resume_save, torrent_id)
|
||||
self.waiting_on_resume_data[torrent_id] = d
|
||||
deferreds.append(d)
|
||||
self.torrents[torrent_id].save_resume_data()
|
||||
|
||||
self.num_resume_data = len(torrent_ids)
|
||||
def on_all_resume_data_finished(result):
|
||||
if result:
|
||||
self.save_resume_data_file()
|
||||
|
||||
return DeferredList(deferreds).addBoth(on_all_resume_data_finished)
|
||||
|
||||
def load_resume_data_file(self):
|
||||
resume_data = {}
|
||||
@ -697,40 +805,26 @@ class TorrentManager(component.Component):
|
||||
|
||||
return resume_data
|
||||
|
||||
def save_resume_data_file(self, resume_data=None):
|
||||
def save_resume_data_file(self):
|
||||
"""
|
||||
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
|
||||
|
||||
Saves the resume data file with the contents of self.resume_data.
|
||||
"""
|
||||
# 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 = {}
|
||||
|
||||
try:
|
||||
log.debug("Saving fastresume file: %s", path)
|
||||
fastresume_file = open(path, "wb")
|
||||
fastresume_file.write(lt.bencode(resume_data))
|
||||
fastresume_file.write(lt.bencode(self.resume_data))
|
||||
fastresume_file.flush()
|
||||
os.fsync(fastresume_file.fileno())
|
||||
fastresume_file.close()
|
||||
except IOError:
|
||||
log.warning("Error trying to save fastresume file")
|
||||
|
||||
def get_queue_position(self, torrent_id):
|
||||
"""Get queue position of torrent"""
|
||||
return self.torrents[torrent_id].get_queue_position()
|
||||
|
||||
def queue_top(self, torrent_id):
|
||||
"""Queue torrent to top"""
|
||||
if self.torrents[torrent_id].get_queue_position() == 0:
|
||||
@ -749,7 +843,7 @@ class TorrentManager(component.Component):
|
||||
|
||||
def queue_down(self, torrent_id):
|
||||
"""Queue torrent down one position"""
|
||||
if self.torrents[torrent_id].get_queue_position() == (len(self.torrents) - 1):
|
||||
if self.torrents[torrent_id].get_queue_position() == (len(self.queued_torrents) - 1):
|
||||
return False
|
||||
|
||||
self.torrents[torrent_id].handle.queue_position_down()
|
||||
@ -757,7 +851,7 @@ class TorrentManager(component.Component):
|
||||
|
||||
def queue_bottom(self, torrent_id):
|
||||
"""Queue torrent to bottom"""
|
||||
if self.torrents[torrent_id].get_queue_position() == (len(self.torrents) - 1):
|
||||
if self.torrents[torrent_id].get_queue_position() == (len(self.queued_torrents) - 1):
|
||||
return False
|
||||
|
||||
self.torrents[torrent_id].handle.queue_position_bottom()
|
||||
@ -790,9 +884,9 @@ class TorrentManager(component.Component):
|
||||
log.debug("on_alert_torrent_finished")
|
||||
try:
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
except:
|
||||
return
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
log.debug("%s is finished..", torrent_id)
|
||||
|
||||
# Get the total_download and if it's 0, do not move.. It's likely
|
||||
@ -808,11 +902,18 @@ class TorrentManager(component.Component):
|
||||
if torrent.options["download_location"] != move_path:
|
||||
torrent.move_storage(move_path)
|
||||
|
||||
torrent.is_finished = True
|
||||
component.get("EventManager").emit(TorrentFinishedEvent(torrent_id))
|
||||
|
||||
torrent.is_finished = True
|
||||
torrent.update_state()
|
||||
|
||||
# Torrent is no longer part of the queue
|
||||
try:
|
||||
self.queued_torrents.remove(torrent_id)
|
||||
except KeyError:
|
||||
# Sometimes libtorrent fires a TorrentFinishedEvent twice
|
||||
log.debug("%s isn't in queued torrents set?", torrent_id)
|
||||
|
||||
# Only save resume data if it was actually downloaded something. Helps
|
||||
# on startup with big queues with lots of seeding torrents. Libtorrent
|
||||
# emits alert_torrent_finished for them, but there seems like nothing
|
||||
@ -822,47 +923,51 @@ class TorrentManager(component.Component):
|
||||
self.save_resume_data((torrent_id, ))
|
||||
|
||||
def on_alert_torrent_paused(self, alert):
|
||||
log.debug("on_alert_torrent_paused")
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug("on_alert_torrent_paused")
|
||||
try:
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
except:
|
||||
return
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
# Set the torrent state
|
||||
old_state = torrent.state
|
||||
torrent.update_state()
|
||||
if torrent.state != old_state:
|
||||
component.get("EventManager").emit(TorrentStateChangedEvent(torrent_id, torrent.state))
|
||||
|
||||
# Don't save resume data for each torrent after self.stop() was called.
|
||||
# We save resume data in bulk in self.stop() in this case.
|
||||
if self.save_resume_data_timer.running:
|
||||
# Write the fastresume file
|
||||
self.save_resume_data((torrent_id, ))
|
||||
|
||||
if torrent_id in self.shutdown_torrent_pause_list:
|
||||
self.shutdown_torrent_pause_list.remove(torrent_id)
|
||||
# Write the fastresume file if we are not waiting on a bulk write
|
||||
if torrent_id not in self.waiting_on_resume_data:
|
||||
self.save_resume_data((torrent_id,))
|
||||
|
||||
def on_alert_torrent_checked(self, alert):
|
||||
log.debug("on_alert_torrent_checked")
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug("on_alert_torrent_checked")
|
||||
try:
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
except:
|
||||
return
|
||||
|
||||
# Check to see if we're forcing a recheck and set it back to paused
|
||||
# if necessary
|
||||
if torrent.forcing_recheck:
|
||||
torrent.forcing_recheck = False
|
||||
if torrent.forcing_recheck_paused:
|
||||
torrent.handle.pause()
|
||||
|
||||
# Set the torrent state
|
||||
torrent.update_state()
|
||||
|
||||
def on_alert_tracker_reply(self, alert):
|
||||
log.debug("on_alert_tracker_reply: %s", alert.message().decode("utf8"))
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug("on_alert_tracker_reply: %s", decode_string(alert.message()))
|
||||
try:
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
except:
|
||||
return
|
||||
|
||||
# Set the tracker status for the torrent
|
||||
if alert.message() != "Got peers from DHT":
|
||||
torrent.set_tracker_status(_("Announce OK"))
|
||||
torrent.set_tracker_status(_("Announce OK"))
|
||||
|
||||
# Check to see if we got any peer information from the tracker
|
||||
if alert.handle.status().num_complete == -1 or \
|
||||
@ -871,7 +976,8 @@ class TorrentManager(component.Component):
|
||||
torrent.scrape_tracker()
|
||||
|
||||
def on_alert_tracker_announce(self, alert):
|
||||
log.debug("on_alert_tracker_announce")
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug("on_alert_tracker_announce")
|
||||
try:
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
except:
|
||||
@ -886,7 +992,7 @@ class TorrentManager(component.Component):
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
except:
|
||||
return
|
||||
tracker_status = '%s: %s' % (_("Warning"), str(alert.message()))
|
||||
tracker_status = '%s: %s' % (_("Warning"), decode_string(alert.message()))
|
||||
# Set the tracker status for the torrent
|
||||
torrent.set_tracker_status(tracker_status)
|
||||
|
||||
@ -896,7 +1002,7 @@ class TorrentManager(component.Component):
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
except:
|
||||
return
|
||||
tracker_status = "%s: %s" % (_("Error"), alert.msg)
|
||||
tracker_status = "%s: %s" % (_("Error"), decode_string(alert.msg))
|
||||
torrent.set_tracker_status(tracker_status)
|
||||
|
||||
def on_alert_storage_moved(self, alert):
|
||||
@ -905,17 +1011,16 @@ class TorrentManager(component.Component):
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
except:
|
||||
return
|
||||
torrent.set_save_path(alert.handle.save_path())
|
||||
torrent.set_save_path(os.path.normpath(alert.handle.save_path()))
|
||||
torrent.set_move_completed(False)
|
||||
|
||||
def on_alert_torrent_resumed(self, alert):
|
||||
log.debug("on_alert_torrent_resumed")
|
||||
try:
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
except:
|
||||
return
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
torrent.is_finished = torrent.handle.is_seed()
|
||||
old_state = torrent.state
|
||||
torrent.update_state()
|
||||
if torrent.state != old_state:
|
||||
@ -924,7 +1029,8 @@ class TorrentManager(component.Component):
|
||||
component.get("EventManager").emit(TorrentResumedEvent(torrent_id))
|
||||
|
||||
def on_alert_state_changed(self, alert):
|
||||
log.debug("on_alert_state_changed")
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug("on_alert_state_changed")
|
||||
try:
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
torrent = self.torrents[torrent_id]
|
||||
@ -933,66 +1039,50 @@ class TorrentManager(component.Component):
|
||||
|
||||
old_state = torrent.state
|
||||
torrent.update_state()
|
||||
|
||||
# Torrent may need to download data after checking.
|
||||
if torrent.state in ('Checking', 'Checking Resume Data', 'Downloading'):
|
||||
torrent.is_finished = False
|
||||
self.queued_torrents.add(torrent_id)
|
||||
|
||||
# Only emit a state changed event if the state has actually changed
|
||||
if torrent.state != old_state:
|
||||
component.get("EventManager").emit(TorrentStateChangedEvent(torrent_id, torrent.state))
|
||||
|
||||
def on_alert_save_resume_data(self, alert):
|
||||
log.debug("on_alert_save_resume_data")
|
||||
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug("on_alert_save_resume_data")
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
|
||||
try:
|
||||
torrent = self.torrents[torrent_id]
|
||||
except:
|
||||
return
|
||||
if torrent_id in self.torrents:
|
||||
# Libtorrent in add_torrent() expects resume_data to be bencoded
|
||||
self.resume_data[torrent_id] = lt.bencode(alert.resume_data)
|
||||
|
||||
# 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()
|
||||
if torrent_id in self.waiting_on_resume_data:
|
||||
self.waiting_on_resume_data[torrent_id].callback(None)
|
||||
|
||||
def on_alert_save_resume_data_failed(self, alert):
|
||||
log.debug("on_alert_save_resume_data_failed: %s", alert.message())
|
||||
try:
|
||||
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()
|
||||
log.debug("on_alert_save_resume_data_failed: %s", decode_string(alert.message()))
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
|
||||
if torrent_id in self.waiting_on_resume_data:
|
||||
self.waiting_on_resume_data[torrent_id].errback(Exception(decode_string(alert.message())))
|
||||
|
||||
def on_alert_file_renamed(self, alert):
|
||||
log.debug("on_alert_file_renamed")
|
||||
log.debug("index: %s name: %s", alert.index, alert.name.decode("utf8"))
|
||||
log.debug("index: %s name: %s", alert.index, decode_string(alert.name))
|
||||
try:
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
except:
|
||||
return
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
|
||||
# We need to see if this file index is in a waiting_on_folder list
|
||||
folder_rename = False
|
||||
for i, wait_on_folder in enumerate(torrent.waiting_on_folder_rename):
|
||||
if alert.index in wait_on_folder[2]:
|
||||
folder_rename = True
|
||||
if len(wait_on_folder[2]) == 1:
|
||||
# This is the last alert we were waiting for, time to send signal
|
||||
component.get("EventManager").emit(TorrentFolderRenamedEvent(torrent_id, wait_on_folder[0], wait_on_folder[1]))
|
||||
del torrent.waiting_on_folder_rename[i]
|
||||
self.save_resume_data((torrent_id,))
|
||||
break
|
||||
# This isn't the last file to be renamed in this folder, so just
|
||||
# remove the index and continue
|
||||
torrent.waiting_on_folder_rename[i][2].remove(alert.index)
|
||||
|
||||
if not folder_rename:
|
||||
# We need to see if this file index is in a waiting_on_folder dict
|
||||
for wait_on_folder in torrent.waiting_on_folder_rename:
|
||||
if alert.index in wait_on_folder:
|
||||
wait_on_folder[alert.index].callback(None)
|
||||
break
|
||||
else:
|
||||
# This is just a regular file rename so send the signal
|
||||
component.get("EventManager").emit(TorrentFileRenamedEvent(torrent_id, alert.index, alert.name))
|
||||
self.save_resume_data((torrent_id,))
|
||||
@ -1003,12 +1093,93 @@ class TorrentManager(component.Component):
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
except:
|
||||
return
|
||||
torrent.write_torrentfile()
|
||||
torrent.on_metadata_received()
|
||||
|
||||
def on_alert_file_error(self, alert):
|
||||
log.debug("on_alert_file_error: %s", alert.message())
|
||||
log.debug("on_alert_file_error: %s", decode_string(alert.message()))
|
||||
try:
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
except:
|
||||
return
|
||||
torrent.update_state()
|
||||
|
||||
def on_alert_file_completed(self, alert):
|
||||
log.debug("file_completed_alert: %s", decode_string(alert.message()))
|
||||
try:
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
except:
|
||||
return
|
||||
component.get("EventManager").emit(
|
||||
TorrentFileCompletedEvent(torrent_id, alert.index))
|
||||
|
||||
def separate_keys(self, keys, torrent_ids):
|
||||
"""Separates the input keys into keys for the Torrent class
|
||||
and keys for plugins.
|
||||
"""
|
||||
if self.torrents:
|
||||
for torrent_id in torrent_ids:
|
||||
if torrent_id in self.torrents:
|
||||
status_keys = self.torrents[torrent_id].status_funcs.keys()
|
||||
leftover_keys = list(set(keys) - set(status_keys))
|
||||
torrent_keys = list(set(keys) - set(leftover_keys))
|
||||
return torrent_keys, leftover_keys
|
||||
return [], []
|
||||
|
||||
def on_alert_state_update(self, alert):
|
||||
log.debug("on_status_notification: %s", alert.message())
|
||||
self.last_state_update_alert_ts = time.time()
|
||||
|
||||
for s in alert.status:
|
||||
torrent_id = str(s.info_hash)
|
||||
if torrent_id in self.torrents:
|
||||
self.torrents[torrent_id].update_status(s)
|
||||
|
||||
self.handle_torrents_status_callback(self.torrents_status_requests.pop())
|
||||
|
||||
def handle_torrents_status_callback(self, status_request):
|
||||
"""
|
||||
Builds the status dictionary with the values from the Torrent.
|
||||
"""
|
||||
d, torrent_ids, keys, diff = status_request
|
||||
status_dict = {}.fromkeys(torrent_ids)
|
||||
torrent_keys, plugin_keys = self.separate_keys(keys, torrent_ids)
|
||||
|
||||
# Get the torrent status for each torrent_id
|
||||
for torrent_id in torrent_ids:
|
||||
if not torrent_id in self.torrents:
|
||||
# The torrent_id does not exist in the dict.
|
||||
# Could be the clients cache (sessionproxy) isn't up to speed.
|
||||
del status_dict[torrent_id]
|
||||
else:
|
||||
status_dict[torrent_id] = self.torrents[torrent_id].get_status(torrent_keys, diff)
|
||||
self.status_dict = status_dict
|
||||
d.callback((status_dict, plugin_keys))
|
||||
|
||||
def torrents_status_update(self, torrent_ids, keys, diff=False):
|
||||
"""
|
||||
returns status dict for the supplied torrent_ids async
|
||||
If the torrent states were updated recently (less than 1.5 seconds ago,
|
||||
post_torrent_updates is not called. Instead the cached state is used.
|
||||
|
||||
:param torrent_ids: the torrent IDs to get the status on
|
||||
:type torrent_ids: list of str
|
||||
: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 status dictionary for the equested torrents.
|
||||
:rtype: dict
|
||||
|
||||
"""
|
||||
d = Deferred()
|
||||
now = time.time()
|
||||
# If last update was recent, use cached data instead of request updates from libtorrent
|
||||
if (now - self.last_state_update_alert_ts) < 1.5:
|
||||
reactor.callLater(0, self.handle_torrents_status_callback, (d, torrent_ids, keys, diff))
|
||||
else:
|
||||
# Ask libtorrent for status update
|
||||
self.torrents_status_requests.insert(0, (d, torrent_ids, keys, diff))
|
||||
self.session.post_torrent_updates()
|
||||
return d
|
||||
|
@ -1,2 +0,0 @@
|
||||
From: http://famfamfam.com/lab/icons/flags/
|
||||
"These flag icons are available for free use for any purpose with no requirement for attribution."
|
@ -1,12 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=Deluge BitTorrent Client
|
||||
GenericName=Bittorrent Client
|
||||
Comment=Transfer files using the Bittorrent protocol
|
||||
Exec=deluge-gtk
|
||||
Icon=deluge
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Network;
|
||||
StartupNotify=true
|
||||
MimeType=application/x-bittorrent;
|
@ -2,6 +2,7 @@
|
||||
# error.py
|
||||
#
|
||||
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
@ -35,7 +36,21 @@
|
||||
|
||||
|
||||
class DelugeError(Exception):
|
||||
pass
|
||||
def _get_message(self):
|
||||
return self._message
|
||||
def _set_message(self, message):
|
||||
self._message = message
|
||||
message = property(_get_message, _set_message)
|
||||
del _get_message, _set_message
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
inst = super(DelugeError, cls).__new__(cls, *args, **kwargs)
|
||||
inst._args = args
|
||||
inst._kwargs = kwargs
|
||||
return inst
|
||||
|
||||
class NoCoreError(DelugeError):
|
||||
pass
|
||||
@ -48,3 +63,69 @@ class InvalidTorrentError(DelugeError):
|
||||
|
||||
class InvalidPathError(DelugeError):
|
||||
pass
|
||||
|
||||
class WrappedException(DelugeError):
|
||||
def _get_traceback(self):
|
||||
return self._traceback
|
||||
def _set_traceback(self, traceback):
|
||||
self._traceback = traceback
|
||||
traceback = property(_get_traceback, _set_traceback)
|
||||
del _get_traceback, _set_traceback
|
||||
|
||||
def _get_type(self):
|
||||
return self._type
|
||||
def _set_type(self, type):
|
||||
self._type = type
|
||||
type = property(_get_type, _set_type)
|
||||
del _get_type, _set_type
|
||||
|
||||
def __init__(self, message, exception_type, traceback):
|
||||
self.message = message
|
||||
self.type = exception_type
|
||||
self.traceback = traceback
|
||||
|
||||
class _ClientSideRecreateError(DelugeError):
|
||||
pass
|
||||
|
||||
class IncompatibleClient(_ClientSideRecreateError):
|
||||
def __init__(self, daemon_version):
|
||||
self.daemon_version = daemon_version
|
||||
self.message = _(
|
||||
"Your deluge client is not compatible with the daemon. "
|
||||
"Please upgrade your client to %(daemon_version)s"
|
||||
) % dict(daemon_version=self.daemon_version)
|
||||
|
||||
class NotAuthorizedError(_ClientSideRecreateError):
|
||||
|
||||
def __init__(self, current_level, required_level):
|
||||
self.message = _(
|
||||
"Auth level too low: %(current_level)s < %(required_level)s" %
|
||||
dict(current_level=current_level, required_level=required_level)
|
||||
)
|
||||
self.current_level = current_level
|
||||
self.required_level = required_level
|
||||
|
||||
|
||||
class _UsernameBasedPasstroughError(_ClientSideRecreateError):
|
||||
|
||||
def _get_username(self):
|
||||
return self._username
|
||||
def _set_username(self, username):
|
||||
self._username = username
|
||||
username = property(_get_username, _set_username)
|
||||
del _get_username, _set_username
|
||||
|
||||
def __init__(self, message, username):
|
||||
super(_UsernameBasedPasstroughError, self).__init__(message)
|
||||
self.message = message
|
||||
self.username = username
|
||||
|
||||
|
||||
class BadLoginError(_UsernameBasedPasstroughError):
|
||||
pass
|
||||
|
||||
class AuthenticationRequired(_UsernameBasedPasstroughError):
|
||||
pass
|
||||
|
||||
class AuthManagerError(_UsernameBasedPasstroughError):
|
||||
pass
|
||||
|
@ -57,7 +57,9 @@ class DelugeEvent(object):
|
||||
The base class for all events.
|
||||
|
||||
:prop name: this is the name of the class which is in-turn the event name
|
||||
:type name: string
|
||||
:prop args: a list of the attribute values
|
||||
:type args: list
|
||||
|
||||
"""
|
||||
__metaclass__ = DelugeEventMetaClass
|
||||
@ -77,11 +79,14 @@ class TorrentAddedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a new torrent is successfully added to the session.
|
||||
"""
|
||||
def __init__(self, torrent_id):
|
||||
def __init__(self, torrent_id, from_state):
|
||||
"""
|
||||
:param torrent_id: str, the torrent_id of the torrent that was added
|
||||
:param torrent_id: the torrent_id of the torrent that was added
|
||||
:type torrent_id: string
|
||||
:param from_state: was the torrent loaded from state? Or is it a new torrent.
|
||||
:type from_state: bool
|
||||
"""
|
||||
self._args = [torrent_id]
|
||||
self._args = [torrent_id, from_state]
|
||||
|
||||
class TorrentRemovedEvent(DelugeEvent):
|
||||
"""
|
||||
@ -89,7 +94,8 @@ class TorrentRemovedEvent(DelugeEvent):
|
||||
"""
|
||||
def __init__(self, torrent_id):
|
||||
"""
|
||||
:param torrent_id: str, the torrent_id
|
||||
:param torrent_id: the torrent_id
|
||||
:type torrent_id: string
|
||||
"""
|
||||
self._args = [torrent_id]
|
||||
|
||||
@ -99,7 +105,8 @@ class PreTorrentRemovedEvent(DelugeEvent):
|
||||
"""
|
||||
def __init__(self, torrent_id):
|
||||
"""
|
||||
:param torrent_id: str, the torrent_id
|
||||
:param torrent_id: the torrent_id
|
||||
:type torrent_id: string
|
||||
"""
|
||||
self._args = [torrent_id]
|
||||
|
||||
@ -109,8 +116,10 @@ class TorrentStateChangedEvent(DelugeEvent):
|
||||
"""
|
||||
def __init__(self, torrent_id, state):
|
||||
"""
|
||||
:param torrent_id: str, the torrent_id
|
||||
:param state: str, the new state
|
||||
:param torrent_id: the torrent_id
|
||||
:type torrent_id: string
|
||||
:param state: the new state
|
||||
:type state: string
|
||||
"""
|
||||
self._args = [torrent_id, state]
|
||||
|
||||
@ -126,9 +135,12 @@ class TorrentFolderRenamedEvent(DelugeEvent):
|
||||
"""
|
||||
def __init__(self, torrent_id, old, new):
|
||||
"""
|
||||
:param torrent_id: str, the torrent_id
|
||||
:param old: str, the old folder name
|
||||
:param new: str, the new folder name
|
||||
:param torrent_id: the torrent_id
|
||||
:type torrent_id: string
|
||||
:param old: the old folder name
|
||||
:type old: string
|
||||
:param new: the new folder name
|
||||
:type new: string
|
||||
"""
|
||||
self._args = [torrent_id, old, new]
|
||||
|
||||
@ -138,9 +150,12 @@ class TorrentFileRenamedEvent(DelugeEvent):
|
||||
"""
|
||||
def __init__(self, torrent_id, index, name):
|
||||
"""
|
||||
:param torrent_id: str, the torrent_id
|
||||
:param index: int, the index of the file
|
||||
:param name: str, the new filename
|
||||
:param torrent_id: the torrent_id
|
||||
:type torrent_id: string
|
||||
:param index: the index of the file
|
||||
:type index: int
|
||||
:param name: the new filename
|
||||
:type name: string
|
||||
"""
|
||||
self._args = [torrent_id, index, name]
|
||||
|
||||
@ -150,7 +165,8 @@ class TorrentFinishedEvent(DelugeEvent):
|
||||
"""
|
||||
def __init__(self, torrent_id):
|
||||
"""
|
||||
:param torrent_id: str, the torrent_id
|
||||
:param torrent_id: the torrent_id
|
||||
:type torrent_id: string
|
||||
"""
|
||||
self._args = [torrent_id]
|
||||
|
||||
@ -160,17 +176,42 @@ class TorrentResumedEvent(DelugeEvent):
|
||||
"""
|
||||
def __init__(self, torrent_id):
|
||||
"""
|
||||
:param torrent_id: str, the torrent_id
|
||||
:param torrent_id: the torrent_id
|
||||
:type torrent_id: string
|
||||
"""
|
||||
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 CreateTorrentProgressEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when creating a torrent file remotely.
|
||||
"""
|
||||
def __init__(self, piece_count, num_pieces):
|
||||
self._args = [piece_count, num_pieces]
|
||||
|
||||
class NewVersionAvailableEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a more recent version of Deluge is available.
|
||||
"""
|
||||
def __init__(self, new_release):
|
||||
"""
|
||||
:param new_release: str, the new version that is available
|
||||
:param new_release: the new version that is available
|
||||
:type new_release: string
|
||||
"""
|
||||
self._args = [new_release]
|
||||
|
||||
@ -199,7 +240,8 @@ class ConfigValueChangedEvent(DelugeEvent):
|
||||
"""
|
||||
def __init__(self, key, value):
|
||||
"""
|
||||
:param key: str, the key that changed
|
||||
:param key: the key that changed
|
||||
:type key: string
|
||||
:param value: the new value of the `:param:key`
|
||||
"""
|
||||
self._args = [key, value]
|
||||
@ -208,20 +250,13 @@ class PluginEnabledEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a plugin is enabled in the Core.
|
||||
"""
|
||||
def __init__(self, name):
|
||||
"""
|
||||
:param name: the plugin name
|
||||
:type name: string
|
||||
"""
|
||||
self._args = [name]
|
||||
def __init__(self, plugin_name):
|
||||
self._args = [plugin_name]
|
||||
|
||||
class PluginDisabledEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a plugin is disabled in the Core.
|
||||
"""
|
||||
def __init__(self, name):
|
||||
"""
|
||||
:param name: the plugin name
|
||||
:type name: string
|
||||
"""
|
||||
self._args = [name]
|
||||
def __init__(self, plugin_name):
|
||||
self._args = [plugin_name]
|
||||
|
||||
|
@ -36,21 +36,26 @@ 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 logging
|
||||
import os.path
|
||||
import zlib
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class HTTPDownloader(client.HTTPDownloader):
|
||||
"""
|
||||
Factory class for downloading files and keeping track of progress.
|
||||
"""
|
||||
def __init__(self, url, filename, part_callback=None, headers=None, force_filename=False, allow_compression=True):
|
||||
def __init__(self, url, filename, part_callback=None, headers=None,
|
||||
force_filename=False, allow_compression=True):
|
||||
"""
|
||||
:param url: the url to download from
|
||||
:type url: string
|
||||
:param filename: the filename to save the file as
|
||||
:type filename: string
|
||||
:param force_filename: forces use of the supplied filename, regardless of header content
|
||||
:type force_filename: bool
|
||||
:param part_callback: a function to be called when a part of data
|
||||
is received, it's signature should be: func(data, current_length, total_length)
|
||||
:type part_callback: function
|
||||
@ -84,15 +89,20 @@ class HTTPDownloader(client.HTTPDownloader):
|
||||
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
|
||||
new_file_name = str(headers["content-disposition"][0]).split(";")[1].split("=")[1]
|
||||
new_file_name = sanitise_filename(new_file_name)
|
||||
new_file_name = os.path.join(os.path.split(self.fileName)[0], new_file_name)
|
||||
|
||||
count = 1
|
||||
fileroot = os.path.splitext(new_file_name)[0]
|
||||
fileext = os.path.splitext(new_file_name)[1]
|
||||
while os.path.isfile(new_file_name):
|
||||
# Increment filename if already exists
|
||||
new_file_name = "%s-%s%s" % (fileroot, count, fileext)
|
||||
count += 1
|
||||
|
||||
self.fileName = new_file_name
|
||||
self.value = new_file_name
|
||||
|
||||
elif self.code in (http.MOVED_PERMANENTLY, http.FOUND, http.SEE_OTHER, http.TEMPORARY_REDIRECT):
|
||||
location = headers["location"][0]
|
||||
@ -129,8 +139,6 @@ def sanitise_filename(filename):
|
||||
:type filename: string
|
||||
:returns: the sanitised filename
|
||||
:rtype: string
|
||||
|
||||
:raises IOError: when the filename exists
|
||||
"""
|
||||
|
||||
# Remove any quotes
|
||||
@ -141,18 +149,16 @@ def sanitise_filename(filename):
|
||||
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):
|
||||
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.
|
||||
|
@ -1,290 +0,0 @@
|
||||
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/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/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/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/session.py
|
||||
deluge/ui/tracker_icons.py
|
||||
deluge/ui/__init__.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/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
|
4750
deluge/i18n/ar.po
4750
deluge/i18n/ar.po
File diff suppressed because it is too large
Load Diff
7003
deluge/i18n/ast.po
7003
deluge/i18n/ast.po
File diff suppressed because it is too large
Load Diff
6177
deluge/i18n/be.po
6177
deluge/i18n/be.po
File diff suppressed because it is too large
Load Diff
4895
deluge/i18n/bg.po
4895
deluge/i18n/bg.po
File diff suppressed because it is too large
Load Diff
6026
deluge/i18n/bn.po
6026
deluge/i18n/bn.po
File diff suppressed because it is too large
Load Diff
6097
deluge/i18n/bs.po
6097
deluge/i18n/bs.po
File diff suppressed because it is too large
Load Diff
3796
deluge/i18n/ca.po
3796
deluge/i18n/ca.po
File diff suppressed because it is too large
Load Diff
4777
deluge/i18n/cs.po
4777
deluge/i18n/cs.po
File diff suppressed because it is too large
Load Diff
6069
deluge/i18n/cy.po
6069
deluge/i18n/cy.po
File diff suppressed because it is too large
Load Diff
5057
deluge/i18n/da.po
5057
deluge/i18n/da.po
File diff suppressed because it is too large
Load Diff
3936
deluge/i18n/de.po
3936
deluge/i18n/de.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
4528
deluge/i18n/el.po
4528
deluge/i18n/el.po
File diff suppressed because it is too large
Load Diff
4641
deluge/i18n/en_AU.po
4641
deluge/i18n/en_AU.po
File diff suppressed because it is too large
Load Diff
4969
deluge/i18n/en_CA.po
4969
deluge/i18n/en_CA.po
File diff suppressed because it is too large
Load Diff
5135
deluge/i18n/en_GB.po
5135
deluge/i18n/en_GB.po
File diff suppressed because it is too large
Load Diff
6088
deluge/i18n/eo.po
6088
deluge/i18n/eo.po
File diff suppressed because it is too large
Load Diff
4757
deluge/i18n/es.po
4757
deluge/i18n/es.po
File diff suppressed because it is too large
Load Diff
4412
deluge/i18n/et.po
4412
deluge/i18n/et.po
File diff suppressed because it is too large
Load Diff
3626
deluge/i18n/eu.po
3626
deluge/i18n/eu.po
File diff suppressed because it is too large
Load Diff
6218
deluge/i18n/fa.po
6218
deluge/i18n/fa.po
File diff suppressed because it is too large
Load Diff
4663
deluge/i18n/fi.po
4663
deluge/i18n/fi.po
File diff suppressed because it is too large
Load Diff
4981
deluge/i18n/fr.po
4981
deluge/i18n/fr.po
File diff suppressed because it is too large
Load Diff
7069
deluge/i18n/fy.po
7069
deluge/i18n/fy.po
File diff suppressed because it is too large
Load Diff
4161
deluge/i18n/gl.po
4161
deluge/i18n/gl.po
File diff suppressed because it is too large
Load Diff
4531
deluge/i18n/he.po
4531
deluge/i18n/he.po
File diff suppressed because it is too large
Load Diff
4248
deluge/i18n/hi.po
4248
deluge/i18n/hi.po
File diff suppressed because it is too large
Load Diff
6508
deluge/i18n/hr.po
6508
deluge/i18n/hr.po
File diff suppressed because it is too large
Load Diff
5012
deluge/i18n/hu.po
5012
deluge/i18n/hu.po
File diff suppressed because it is too large
Load Diff
3889
deluge/i18n/id.po
3889
deluge/i18n/id.po
File diff suppressed because it is too large
Load Diff
4512
deluge/i18n/is.po
4512
deluge/i18n/is.po
File diff suppressed because it is too large
Load Diff
5469
deluge/i18n/it.po
5469
deluge/i18n/it.po
File diff suppressed because it is too large
Load Diff
6013
deluge/i18n/iu.po
6013
deluge/i18n/iu.po
File diff suppressed because it is too large
Load Diff
4634
deluge/i18n/ja.po
4634
deluge/i18n/ja.po
File diff suppressed because it is too large
Load Diff
6203
deluge/i18n/ka.po
6203
deluge/i18n/ka.po
File diff suppressed because it is too large
Load Diff
4857
deluge/i18n/kk.po
4857
deluge/i18n/kk.po
File diff suppressed because it is too large
Load Diff
6143
deluge/i18n/kn.po
6143
deluge/i18n/kn.po
File diff suppressed because it is too large
Load Diff
4936
deluge/i18n/ko.po
4936
deluge/i18n/ko.po
File diff suppressed because it is too large
Load Diff
6041
deluge/i18n/ku.po
6041
deluge/i18n/ku.po
File diff suppressed because it is too large
Load Diff
6020
deluge/i18n/la.po
6020
deluge/i18n/la.po
File diff suppressed because it is too large
Load Diff
5138
deluge/i18n/lt.po
5138
deluge/i18n/lt.po
File diff suppressed because it is too large
Load Diff
5190
deluge/i18n/lv.po
5190
deluge/i18n/lv.po
File diff suppressed because it is too large
Load Diff
6115
deluge/i18n/mk.po
6115
deluge/i18n/mk.po
File diff suppressed because it is too large
Load Diff
5453
deluge/i18n/ms.po
5453
deluge/i18n/ms.po
File diff suppressed because it is too large
Load Diff
4992
deluge/i18n/nb.po
4992
deluge/i18n/nb.po
File diff suppressed because it is too large
Load Diff
6024
deluge/i18n/nds.po
6024
deluge/i18n/nds.po
File diff suppressed because it is too large
Load Diff
5318
deluge/i18n/nl.po
5318
deluge/i18n/nl.po
File diff suppressed because it is too large
Load Diff
5225
deluge/i18n/pl.po
5225
deluge/i18n/pl.po
File diff suppressed because it is too large
Load Diff
6014
deluge/i18n/pms.po
6014
deluge/i18n/pms.po
File diff suppressed because it is too large
Load Diff
4628
deluge/i18n/pt.po
4628
deluge/i18n/pt.po
File diff suppressed because it is too large
Load Diff
4496
deluge/i18n/pt_BR.po
4496
deluge/i18n/pt_BR.po
File diff suppressed because it is too large
Load Diff
4671
deluge/i18n/ro.po
4671
deluge/i18n/ro.po
File diff suppressed because it is too large
Load Diff
4948
deluge/i18n/ru.po
4948
deluge/i18n/ru.po
File diff suppressed because it is too large
Load Diff
6013
deluge/i18n/si.po
6013
deluge/i18n/si.po
File diff suppressed because it is too large
Load Diff
4154
deluge/i18n/sk.po
4154
deluge/i18n/sk.po
File diff suppressed because it is too large
Load Diff
4630
deluge/i18n/sl.po
4630
deluge/i18n/sl.po
File diff suppressed because it is too large
Load Diff
4435
deluge/i18n/sr.po
4435
deluge/i18n/sr.po
File diff suppressed because it is too large
Load Diff
5552
deluge/i18n/sv.po
5552
deluge/i18n/sv.po
File diff suppressed because it is too large
Load Diff
6018
deluge/i18n/ta.po
6018
deluge/i18n/ta.po
File diff suppressed because it is too large
Load Diff
3453
deluge/i18n/th.po
3453
deluge/i18n/th.po
File diff suppressed because it is too large
Load Diff
6014
deluge/i18n/tl.po
6014
deluge/i18n/tl.po
File diff suppressed because it is too large
Load Diff
6013
deluge/i18n/tlh.po
6013
deluge/i18n/tlh.po
File diff suppressed because it is too large
Load Diff
4963
deluge/i18n/tr.po
4963
deluge/i18n/tr.po
File diff suppressed because it is too large
Load Diff
5340
deluge/i18n/uk.po
5340
deluge/i18n/uk.po
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user