Compare commits
1812 Commits
deluge-1.2
...
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 | |||
e7907a63ee | |||
cee416b1b5 | |||
53a9c217e7 | |||
d6c8b13041 | |||
ba03356151 | |||
d28cf93686 | |||
452656e09d | |||
9c460266ac | |||
bb0bd36c51 | |||
5ea7e21943 | |||
facb4669e3 | |||
e4ef17975c | |||
c4b20aa595 | |||
bc028998d7 | |||
7e7da94a6e | |||
53930e0898 | |||
b13adbafbf | |||
556c8b831a | |||
a6d10562f1 | |||
a8ac98bb37 | |||
d7fa383da3 | |||
6ded75caef | |||
88929d4821 | |||
8747611e9e | |||
acba442ddb | |||
5b0f93ba72 | |||
619092aee0 | |||
89fb5b02d7 | |||
75b69b1f11 | |||
f5eddafea9 | |||
d49e1eda79 | |||
e86d2ad4e2 | |||
5effdd4cd4 | |||
c2b4fad389 | |||
82a5b5262c | |||
1b0e08b3d9 | |||
49ec3a1535 | |||
23544bd6b2 | |||
5766e04987 | |||
0857af98d0 | |||
5e78daf726 | |||
1efb700ed8 | |||
e0153e8bdc | |||
b46562d932 | |||
5c8eccdd82 | |||
c45583e8e7 | |||
ce46dcdf7a | |||
63f5c8b116 | |||
111bea19d9 | |||
68f0e9ddc7 | |||
5e1f6a8738 | |||
28a313e74e | |||
5b6faa47b0 | |||
0f12200f6f | |||
7420c6f12f | |||
fb0adbcded | |||
59b7a175eb | |||
cbcd277d91 | |||
269e0b89b6 | |||
698a5ff475 | |||
fd122db7b7 | |||
67905b6f5b | |||
7df7f13e26 | |||
066d199c78 | |||
c8c5e3449a | |||
00dc5f0128 | |||
cd10555a8a | |||
158feaa8e0 | |||
3310cc636f | |||
9849a16d2c | |||
bf28b3ac31 | |||
759ae6356d | |||
c0fd70a856 | |||
028a35bfc8 | |||
fec735f948 | |||
9f185da446 | |||
8414b9cfa9 | |||
91f44c2ad3 | |||
31304d3397 | |||
310b4bad31 | |||
9e7dde8997 | |||
dfde04561f | |||
545e4ef717 | |||
b1cf238489 | |||
7fe5d37094 | |||
a7940d5bf9 | |||
e0e2b1b350 | |||
a58c391675 | |||
9f46958f20 | |||
dd82f95975 | |||
67e27b9b7a | |||
1cc315878a | |||
412b96ba55 | |||
d6b7917350 | |||
8b23af062a | |||
3b1d038d2d | |||
a47f9bc8dd | |||
466b245fdf | |||
bfdaa47aff | |||
c3290b4ac2 | |||
342001c642 | |||
b4404feed7 | |||
85c0725f83 | |||
29634505e4 | |||
faa1752d04 | |||
2edf19c187 | |||
42b3dc7dde | |||
80f151be94 | |||
c8ada0ba07 | |||
e05384909a | |||
3a12a50f3e | |||
5b0ce6b3d8 | |||
9cde1f3e45 | |||
1825ce09fb | |||
62158d7861 | |||
4de8e57f56 | |||
a3d9b93480 | |||
7e12222d33 | |||
bf2fc64ce0 | |||
3fdfedb7f7 | |||
01e847b997 | |||
50162694b5 | |||
ade5f596f4 | |||
8b7c1681ae | |||
2376e857d2 | |||
d024c293ed | |||
54617db03f | |||
70580e35db | |||
8d02fc3db6 | |||
112b0dc1f0 | |||
3fcdcc8eec | |||
7711239452 | |||
283cff7852 | |||
fb4bfe7656 | |||
070443f811 | |||
76483bf766 | |||
dd1716c240 | |||
c176ff900f | |||
9e8d588a05 | |||
0fbbf4ac6c | |||
61dd9a5589 | |||
9a632fc3d3 | |||
e0f2c2473e | |||
80e480854e | |||
86232cac8e | |||
bba825703b | |||
3f3f7bb5b4 | |||
047bdf9e3e | |||
0ae609c6df | |||
76fa8e707a | |||
fdc7d3d7fc | |||
35dfcf3a77 | |||
f21dd242f6 | |||
8bafc9f966 | |||
901d2d715c | |||
02b71451c6 | |||
6e737518d8 | |||
addda6cfcc | |||
afa283cd2d | |||
84b33c3418 | |||
a15500d472 | |||
7d4c791241 | |||
4be615b084 | |||
8e4d88f03c | |||
38c85cf7bb | |||
bc165133d0 | |||
117fe3bb43 | |||
5dab17df89 | |||
3560dac792 | |||
c970a80030 | |||
09de50ec18 | |||
78a1ef0cc5 | |||
d2d9269c87 | |||
0d091cdacc | |||
ac7a1f0065 | |||
772653d872 | |||
ce23ff34a7 | |||
c1200ed63f | |||
af17346ac6 | |||
e9a922f829 | |||
387ea4a911 | |||
7f1dadf3cd | |||
2417e8537b | |||
91692bc966 | |||
4296344502 | |||
7053163f88 | |||
14d9f6b7ba | |||
692ec5bb1b | |||
6039280fb5 | |||
48876fa45c | |||
ae54d3fa18 | |||
47509ee705 | |||
47a80526b3 | |||
183064f857 | |||
7c5dacba5f | |||
1c807ad7c8 | |||
6d83556ba8 | |||
de9ba4986d | |||
2956a7db54 | |||
76de427b96 | |||
e9df745dd0 | |||
bf224b0556 | |||
79e62e6069 | |||
7b84f54974 | |||
4ca14d68c1 | |||
ee9c7d1971 | |||
7dff81b60b | |||
66bd2e3030 | |||
da9af84dc1 | |||
0b44023f92 | |||
dd8400558c | |||
61b5659972 | |||
c987b74d61 | |||
c430ef9a84 | |||
7be5b4c8bc | |||
ab38ca2ad4 | |||
4d5d31a2b0 | |||
eaa03a6f2c | |||
01bb9c4df5 | |||
46a6576c68 | |||
0182641bbb | |||
e939f17654 | |||
17903c78d4 | |||
676c59c318 | |||
4ec10575f3 | |||
323638a751 | |||
ce0dc49572 | |||
3f0edee17a | |||
ea65974dc2 | |||
ddd0f40d4c | |||
12b2f47762 | |||
250471fe47 | |||
1a26287a5a | |||
5c42cfbf64 | |||
1cfc4f522e | |||
73db03a33b | |||
efe2c06347 | |||
53fd0a57ee | |||
017d1e058e | |||
e7e480cf3e | |||
c978c6d016 | |||
8fb56f0410 | |||
211d0bee5d | |||
e7c7b8f4db | |||
78f9f22a40 | |||
c8f2173a04 | |||
256ae0745c | |||
8dad06cfbd | |||
b8270be10f | |||
afbca066d7 | |||
ee8531aa24 | |||
7d27b847fb | |||
73ec9b0338 | |||
4005003003 | |||
3e4f2f94dd | |||
d39b5bd071 | |||
52ea19249c | |||
a93bbc35a1 | |||
79ab0f118f | |||
9d13b17a3c | |||
6aacc6e75c | |||
c03f519f9a | |||
c6caae848f | |||
5945b24476 | |||
606b623d73 | |||
16b832f7ab | |||
b0714f625f | |||
670ad51de1 | |||
986e632475 | |||
f08c0e053c | |||
fd9dc2d892 | |||
67bcaa267a | |||
5a9b671c85 | |||
af9eeb02b0 | |||
ff1ad9d764 | |||
7e5e28ea2b | |||
51555cab83 | |||
dd866f07de | |||
26defff7fc | |||
7947773a88 | |||
ed03721789 | |||
0f6cab42a8 | |||
4031b9f94b | |||
a23648c657 | |||
9672480d39 | |||
7a115622df | |||
f7071b4428 | |||
296d790421 | |||
228d623aef | |||
e83737805d | |||
2952a5a7a3 | |||
21431f18e1 | |||
4929ba3c44 | |||
81d28b686f | |||
e2840148af | |||
450d526eca | |||
ae426eb0cd | |||
3e6c956ac6 | |||
3a54a9aebc | |||
f6c058dd34 | |||
cd7681b909 | |||
d2dafe4180 | |||
92837080cd | |||
97c2d0346a | |||
da9cb956a8 | |||
0ed6eb8564 | |||
f7f928f0b9 | |||
24ce77cdf0 | |||
3a56af99c0 | |||
c1bf8c1da1 | |||
f825e8996a | |||
b919613a51 | |||
38802245b6 | |||
c6da126f55 | |||
84374fd83a | |||
8de2d30de0 | |||
a961947720 | |||
ae5071d6cb | |||
00c896ff1d | |||
0f126bcbd5 | |||
57fa3d8834 | |||
e0a8fd70f5 | |||
e2b78be264 | |||
6fd3cd56ff | |||
88004c0d54 | |||
739636cc0b | |||
238e183851 | |||
8a15a18361 | |||
2ed9f97bb0 | |||
a1be15ffb2 | |||
44d3e2fa2f | |||
2ce62bf19b | |||
3a86fd7068 | |||
a69ed83e25 | |||
e5c734fb05 | |||
cfeae2baf4 | |||
0ffca9a1f6 | |||
b8521e7e28 | |||
50ae65b58a | |||
4a7782af28 | |||
5bbe8bec5c | |||
a065f95075 | |||
8dd4d2f094 | |||
9ad59d5f48 | |||
7999bd1e8c | |||
c81b1620ca | |||
6d4cf138c8 | |||
a011d6d659 | |||
1790a1cf2d | |||
b5533a22bc | |||
6750e9f122 | |||
04abf6d17b | |||
00734d14b6 | |||
bb00387903 | |||
ea365bf671 | |||
1d2a6f7f0e | |||
7bd5ba3cdb | |||
68b5f92ec0 | |||
04242ba91c | |||
eef9d8ec13 | |||
0e9a691954 | |||
9b23ce9e06 | |||
f9d2e96d39 | |||
caed5accea | |||
343521ca25 | |||
1d29f67a75 | |||
da8367de4d | |||
0647c4e3d7 | |||
976824ad99 | |||
a4e12c1d94 | |||
db4d0fabe7 | |||
25b282fdb8 | |||
518a73079a | |||
0634592bd9 | |||
3e13fe1229 | |||
7125eab8b2 | |||
57a9e925a7 | |||
4afcae325a | |||
dc764b2ad5 | |||
454321614b | |||
d39f6843c9 | |||
f63435db90 | |||
8c3b6cb0db | |||
a89d0d42aa | |||
3368c4c67d | |||
447cb52bf1 | |||
2afd0a4e97 | |||
7727a98c45 | |||
744f11e19b | |||
0a86a30c2a | |||
ec410fc1a9 | |||
7af689e57f | |||
fa0d6b1aa0 | |||
8fc4caa2f1 | |||
25afa04ba3 | |||
c202f7727f | |||
d74050e7a6 | |||
c7635b0ff0 | |||
0d069d0fe8 | |||
e441f96204 | |||
99197b2063 | |||
92d19fd58d | |||
b4b95c9423 | |||
e79f6ada2f | |||
0e0cc30128 | |||
9b26f6ebee | |||
30280b0803 | |||
fe6bcd62be | |||
14cea4fbc7 | |||
899ac7c86e | |||
39341f623f | |||
42cbf4f5c6 | |||
a3d98029f9 | |||
031f75a2bb | |||
a59332b4ef | |||
1b2d5bc6ad | |||
f14b3a8459 | |||
76fcfa498d | |||
2073ae0221 | |||
cd24acd74f | |||
1ac0403f05 | |||
90e562ff98 | |||
e010a789c4 | |||
964d85d908 | |||
3068f006e2 | |||
f4cf3d9893 | |||
708ad2e665 | |||
0819697c5b | |||
0121d721cb | |||
a82b6e4fb5 | |||
4420f2fae5 | |||
f8f9438950 | |||
e211b6feca | |||
d9b9f22998 | |||
aaa7dae18e | |||
2de185adea | |||
0e2dd9f389 | |||
cbac2fbd5a | |||
be70305365 | |||
f586b91a59 | |||
623c5ab57b | |||
ed00536468 | |||
430b96f4f5 | |||
ac66f305e7 | |||
8175b2af58 | |||
4dbc93b1fa | |||
9b97c6a578 | |||
97375f1a7c | |||
6312ad4a7e | |||
b06f46ea7c | |||
a570e67a4d | |||
221c9984d6 | |||
ccc97f83a8 | |||
d557cda55c | |||
eee6d4c030 | |||
177ec7d5c2 | |||
f30a10f2b8 | |||
1c665b7d2f | |||
717897b343 | |||
7f33292aca | |||
3e67620e55 | |||
bc8cacdbd1 | |||
ec9564d0c1 | |||
ffb241a4de | |||
d57b2b43ec | |||
19e2ab2187 | |||
8e7d4f2cd4 | |||
ef2ca43c3d | |||
8bedc613a0 | |||
cd3221baf6 | |||
e0ebffb8fa | |||
5a8c443d50 | |||
c7d52f3ce5 | |||
fad6ba2193 | |||
22c9d7c0ee | |||
3a864bcdad | |||
14f894959f | |||
6aebcef2b5 | |||
9e53e33c24 | |||
62a336b7d8 | |||
0cc00ba5df | |||
a161bbfcb6 | |||
f03d1818fa | |||
13b2e7da26 | |||
377f8cf886 | |||
72c9a46fa3 | |||
53b9cdebcd | |||
dc9e2597da | |||
bc27c2cdf7 | |||
a9631daf7f | |||
d1f6ca90fc | |||
68749a7ad5 | |||
5b94861fc9 | |||
a8697114c1 | |||
0042fb1767 | |||
b69e25e308 | |||
3c24d72489 | |||
358b5f4b19 | |||
43df21517e | |||
43a5a9111b | |||
773f65d708 | |||
cd7b5082a0 | |||
f3f3b3669f | |||
7e0d09a7bc | |||
42e904b63c | |||
35186faf78 | |||
62dfd6a664 | |||
8212a66d5a | |||
eac2a10a12 | |||
9f034657b0 | |||
f7b3e11729 | |||
b47f6badd7 | |||
2ecb233b5b | |||
040f1a5c6d | |||
a126081d2c | |||
104852d47e | |||
a2cc2cdd8f | |||
7650ebc373 | |||
a4e8d1eb46 | |||
b9d2094a15 | |||
3f383df479 | |||
1ba3955025 | |||
0d3f364aac | |||
d119fa3629 | |||
5129682727 | |||
96becf60bd | |||
9c3efd17cb | |||
19455a7adf | |||
57c96477c1 | |||
f9c61bbc11 | |||
a80c4e18e7 | |||
e73c65e602 | |||
8c283875fb | |||
1353ce6903 | |||
3136e5490f | |||
cffce4a706 | |||
1c318504cf | |||
115a7c3795 | |||
3fcb0e7ae5 | |||
a567b23262 | |||
93468f342a | |||
635260e686 | |||
94625c48b2 | |||
20dc0a5e8a | |||
e6135aa2a9 | |||
1cb42252b8 | |||
7812f7b4e4 | |||
127b577440 | |||
a5e8a9dc69 | |||
9bfa5f10b6 | |||
67c0f8609b | |||
7061e09a8e | |||
e327d87ebc | |||
c4f0920c18 | |||
adb22bdfa5 | |||
01a43544d5 | |||
8420d6105b | |||
1b7a50f88b | |||
8a6ec7232d | |||
535940e2e6 | |||
0723a77214 | |||
1f58910a38 | |||
634ecdeb1d | |||
aa86aa6fe1 | |||
789356d44d | |||
2b3bd4f1f3 | |||
ad04b2a137 | |||
1a1ab4e780 | |||
330b8b3ced | |||
bdba9cd00a | |||
f98c3adf2f | |||
ec2c5ab937 | |||
8dff2375d0 | |||
4e2c0a70c4 | |||
41353c9ae4 | |||
e73052df1c | |||
9b8282010c | |||
dd67a935cb | |||
0a84bc73c5 | |||
baa177a1b1 | |||
fa5b7e7a66 | |||
902ef3fa28 | |||
8144d15689 | |||
dd860e67f7 | |||
19d9c71b13 | |||
46906f5447 | |||
1893b92f37 | |||
fb1d7cfece | |||
59b93f4d2e | |||
6880a142e3 | |||
3d76122666 | |||
0f81e2816f | |||
59c7fcf854 | |||
f6a4d19084 | |||
ec259d6aea | |||
7983187818 | |||
2a4cf7cb56 | |||
3b07cc40bd | |||
fd24e1c17c | |||
f876c17efd | |||
ee7f4e452d | |||
7c10dd4c0e | |||
a110ad1d20 | |||
7c2a2af1f0 | |||
6d2d3c0fd0 | |||
888997372c | |||
5991abcec5 | |||
675d1219cd | |||
a1e4c51c9c | |||
93e3d2acf6 | |||
219f745e68 | |||
d6e18f7729 | |||
d27d7c6733 | |||
831b81529c | |||
826f1a2be9 | |||
e17bd472a8 | |||
de82302c67 | |||
81949449ae | |||
acc850dab9 | |||
208eef713a | |||
dbe90d2882 | |||
3153a545ca | |||
65545df485 | |||
29d01993c9 | |||
9627b7cb92 | |||
c068384845 | |||
92eb10be29 | |||
c6d4208a29 | |||
c6eaec6998 | |||
b77e846744 | |||
566d3c5ebf | |||
6eee4fb31f | |||
36e74e05ca | |||
2289b5f173 | |||
f6b5eb0a0c | |||
d70edb245b | |||
21701cb096 | |||
da2a7ef138 | |||
3f1d769ffc | |||
9c491c13cc | |||
bdc173cf4b | |||
4ae439a99a | |||
04217e16d4 | |||
4a00edc066 | |||
aa274eca74 | |||
c0b5bcc7d3 | |||
551d38c7a0 | |||
c3f433500b | |||
23c949d4ec | |||
3d85791a03 | |||
a3636ccdb7 | |||
e0111271a3 | |||
44217fd977 | |||
6274a32ca1 | |||
11c0e9a304 | |||
844f98db04 | |||
3c69822761 | |||
3a90109724 | |||
ce0968e6dc | |||
2e6b21aa85 | |||
8a5ccbc2d4 |
25
.gitattributes
vendored
Normal file
25
.gitattributes
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
/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
|
||||
*.py diff=python
|
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
*~
|
||||
build
|
||||
dist
|
||||
*egg-info
|
||||
*.egg
|
||||
*.log
|
||||
*.pyc
|
||||
*.tar.*
|
||||
_trial_temp
|
||||
deluge/i18n/*/
|
||||
*.desktop
|
||||
.build_data*
|
||||
osx/app
|
||||
RELEASE-VERSION
|
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
|
||||
Γιάννης Κατσαμπίρης
|
||||
Артём Попов
|
||||
Миша
|
||||
Шаймарданов Максим
|
||||
蔡查理
|
569
ChangeLog
569
ChangeLog
@ -1,98 +1,521 @@
|
||||
=== Deluge 1.2.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 ====
|
||||
* Implement new RPC protocol DelugeRPC replacing XMLRPC
|
||||
* Move to a twisted framework
|
||||
* Add an 'Error' filter for Trackers to show trackers that currently have a tracker error
|
||||
* Use system GeoIP database if available, this is now an optional dependency
|
||||
* 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 ====
|
||||
* Remove SignalReceiver
|
||||
* Implemented a cross-platform IPC method thus removing the DBUS dependency
|
||||
* Implement a "True" Classic Mode where there is no longer a separate daemon process
|
||||
* Add preferences option "Add torrent in paused state"
|
||||
* Add tracker icons to the Tracker column
|
||||
* Implement #259 show tooltip with country name in the peers tab
|
||||
* Add an error category to the tracker sidebar list
|
||||
* Add Find More Plugins button to Plugins preference page
|
||||
* Fix #518 remove header in add torrent dialog to save vertical space
|
||||
* Add a Cache preferences page to adjust cache settings and examine cache status
|
||||
* Add ability to rename files prior to adding them
|
||||
* Fix shutdown handler with GNOME session manager
|
||||
* Allow 4 MiB piece sizes when creating a torrent
|
||||
* Fix 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.
|
||||
|
||||
==== ConsoleUI ====
|
||||
* Changed to use curses for a more interactive client
|
||||
==== 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 ====
|
||||
* Move over to using Twisted-Web for the webserver.
|
||||
* Move to only AJAX interface built upon Ext-JS.
|
||||
* 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
|
||||
* Implement #1012 httpdownloader supports gzip decoding
|
||||
* #496: Remove deprecated functions in favour of get_session_status()
|
||||
* #1112: Fix renaming files in add torrent dialog
|
||||
* #1247: Fix deluge-gtk from hanging on shutdown
|
||||
* #995: Rewrote tracker_icons
|
||||
* Add AutoAdd plugin
|
||||
* Add Notifications plugin
|
||||
|
||||
==== GtkUI ====
|
||||
* Use new SessionProxy class for caching torrent status client-side
|
||||
* Use torrent status diffs to reduce RPC traffic
|
||||
|
||||
==== Blocklist ====
|
||||
* Implement local blocklist support
|
||||
* #861: Pause transfers until blocklist is imported
|
||||
* Fix redirection not working with relative paths
|
||||
|
||||
==== Execute ====
|
||||
* Fix running commands with the TorrentAdded event
|
||||
* Fix the web interface
|
||||
|
||||
==== Label ====
|
||||
* Fix the web interface (#733)
|
||||
|
||||
==== Web ====
|
||||
* Migrate to ExtJS 3.1
|
||||
* Add gzip compression of HTTP data to the server
|
||||
* Improve the efficiency of the TorrentGrid with lots of torrents (#1026)
|
||||
* Add a base parameter to allow reverse proxying (#1076)
|
||||
* Fix showing all the peers in the details tab (#1054)
|
||||
* Fix uploading torrent files in Opera or IE (#1087)
|
||||
* Complete IE support
|
||||
|
||||
=== Deluge 1.2.0 - "Bursting like an infected kidney" (10 January 2010) ===
|
||||
==== Core ====
|
||||
* Implement new RPC protocol DelugeRPC replacing XMLRPC
|
||||
* Move to a twisted framework
|
||||
* Add an 'Error' filter for Trackers to show trackers that currently have a tracker error
|
||||
* Use system GeoIP database if available, this is now an optional dependency
|
||||
|
||||
==== GtkUI ====
|
||||
* Remove SignalReceiver
|
||||
* Implemented a cross-platform IPC method thus removing the DBUS dependency
|
||||
* Implement a "True" Classic Mode where there is no longer a separate daemon process
|
||||
* Add preferences option "Add torrent in paused state"
|
||||
* Add tracker icons to the Tracker column
|
||||
* Implement #259 show tooltip with country name in the peers tab
|
||||
* Add an error category to the tracker sidebar list
|
||||
* Add Find More Plugins button to Plugins preference page
|
||||
* Fix #518 remove header in add torrent dialog to save vertical space
|
||||
* Add a Cache preferences page to adjust cache settings and examine cache status
|
||||
* Add ability to rename files prior to adding them
|
||||
* Fix shutdown handler with GNOME session manager
|
||||
* Allow 4 MiB piece sizes when creating a torrent
|
||||
|
||||
==== ConsoleUI ====
|
||||
* Changed to use curses for a more interactive client
|
||||
|
||||
==== WebUI ====
|
||||
* Move over to using Twisted-Web for the webserver.
|
||||
* Move to only AJAX interface built upon Ext-JS.
|
||||
|
||||
==== Plugins ====
|
||||
* Add Scheduler plugin
|
||||
* Add Extractor plugin
|
||||
* Add Scheduler plugin
|
||||
* Add Extractor plugin
|
||||
|
||||
==== Misc ====
|
||||
* PyGTK dependency bumped to => 2.12 to use new tooltip system
|
||||
* Add new scripts for invoking UIs: deluge-gtk, deluge-web, deluge-console
|
||||
* Remove GeoIP database from the source tree
|
||||
* PyGTK dependency bumped to => 2.12 to use new tooltip system
|
||||
* Add new scripts for invoking UIs: deluge-gtk, deluge-web, deluge-console
|
||||
* Remove GeoIP database from the source tree
|
||||
|
||||
=== Deluge 1.1.0 - "Time gas!" (10 January 2009) ===
|
||||
==== Core ====
|
||||
* Implement #79 ability to change outgoing port range
|
||||
* Implement #296 ability to change peer TOS byte
|
||||
* Add per-torrent move on completed settings
|
||||
* Implement #414 use async save_resume_data method
|
||||
* Filter Manager with torrent filtering in get_torrents_status , for sidebar and plugins.
|
||||
* Implement #368 add torrents by infohash/magnet uri (trackerless torrents)
|
||||
* Remove remaining gtk functions in common
|
||||
* Tracker icons.
|
||||
* Add ETA for torrents with stop at seed ratio set
|
||||
* Fix #47 the state and config files are no longer invalidated when there is no diskspace
|
||||
* Fix #619 return "" instead of "Infinity" if seconds == 0 in ftime
|
||||
* Add -P, --pidfile option to deluged
|
||||
* Implement #79 ability to change outgoing port range
|
||||
* Implement #296 ability to change peer TOS byte
|
||||
* Add per-torrent move on completed settings
|
||||
* Implement #414 use async save_resume_data method
|
||||
* Filter Manager with torrent filtering in get_torrents_status , for sidebar and plugins.
|
||||
* Implement #368 add torrents by infohash/magnet uri (trackerless torrents)
|
||||
* Remove remaining gtk functions in common
|
||||
* Tracker icons.
|
||||
* Add ETA for torrents with stop at seed ratio set
|
||||
* Fix #47 the state and config files are no longer invalidated when there is no diskspace
|
||||
* Fix #619 return "" instead of "Infinity" if seconds == 0 in ftime
|
||||
* Add -P, --pidfile option to deluged
|
||||
|
||||
==== GtkUI ====
|
||||
* Add peer progress to the peers tab
|
||||
* Add ability to manually add peers
|
||||
* Sorting # column will place downloaders above seeds
|
||||
* Remove dependency on libtorrent for add torrent dialog
|
||||
* Allow adding multiple trackers at once in the edit tracker dialog
|
||||
* Implement #28 Create Torrent Dialog
|
||||
* Redesiged sidebar with filters for Active and Tracker (see Filter Manager)
|
||||
* Implement #428 the ability to rename files and directories
|
||||
* Implement #229 add date added column
|
||||
* Implement #596 show speeds in title
|
||||
* Fix #636 not setting the daemon's config directory when using --config= with the UI in classic mode.
|
||||
* Fix #624 do not allow changing file priorities when using compact allocation
|
||||
* Fix #602 re-did files/peers tab state saving/loading
|
||||
* Fix gtk warnings
|
||||
* Add protocol traffic statusbar item
|
||||
* Rework the Remove Torrent Dialog to only have 2 options, remove data and remove from session.
|
||||
* Add "Install Plugin" and "Rescan Plugins" buttons to the Plugins preferences
|
||||
* Make active port test use internal graphic instead of launching browser
|
||||
* Add peer progress to the peers tab
|
||||
* Add ability to manually add peers
|
||||
* Sorting # column will place downloaders above seeds
|
||||
* Remove dependency on libtorrent for add torrent dialog
|
||||
* Allow adding multiple trackers at once in the edit tracker dialog
|
||||
* Implement #28 Create Torrent Dialog
|
||||
* Redesiged sidebar with filters for Active and Tracker (see Filter Manager)
|
||||
* Implement #428 the ability to rename files and directories
|
||||
* Implement #229 add date added column
|
||||
* Implement #596 show speeds in title
|
||||
* Fix #636 not setting the daemon's config directory when using --config= with the UI in classic mode.
|
||||
* Fix #624 do not allow changing file priorities when using compact allocation
|
||||
* Fix #602 re-did files/peers tab state saving/loading
|
||||
* Fix gtk warnings
|
||||
* Add protocol traffic statusbar item
|
||||
* Rework the Remove Torrent Dialog to only have 2 options, remove data and remove from session.
|
||||
* Add "Install Plugin" and "Rescan Plugins" buttons to the Plugins preferences
|
||||
* Make active port test use internal graphic instead of launching browser
|
||||
|
||||
==== WebUI ====
|
||||
* Lots of smaller tweaks.
|
||||
* All details tabs have the same features as in gtk-ui 1.0.x
|
||||
* Persistent sessions #486
|
||||
* Plugin improvements for easy use of templates and images in eggs. #497
|
||||
* Classic template takes over some style elements from white template.
|
||||
* https (for users that know how to create certificates)
|
||||
* Easier apache mod_proxy use.
|
||||
* Redesigned sidebar
|
||||
* Lots of smaller tweaks.
|
||||
* All details tabs have the same features as in gtk-ui 1.0.x
|
||||
* Persistent sessions #486
|
||||
* Plugin improvements for easy use of templates and images in eggs. #497
|
||||
* Classic template takes over some style elements from white template.
|
||||
* https (for users that know how to create certificates)
|
||||
* Easier apache mod_proxy use.
|
||||
* Redesigned sidebar
|
||||
|
||||
==== AjaxUI ====
|
||||
* Hosted in a webui template.
|
||||
* Hosted in a webui template.
|
||||
|
||||
==== ConsoleUI ====
|
||||
* New ConsoleUI written by Idoa01
|
||||
* Callable from command-line for scripts.
|
||||
* New ConsoleUI written by Idoa01
|
||||
* Callable from command-line for scripts.
|
||||
|
||||
==== Plugins ====
|
||||
* Stats plugin for graphs.
|
||||
* Label plugin for grouping torrents and per torrent settings.
|
||||
* Stats plugin for graphs.
|
||||
* Label plugin for grouping torrents and per torrent settings.
|
||||
|
||||
==== Misc ====
|
||||
* Implement #478 display UI options in usage help
|
||||
* Fix #547 add description to name field per HIG entry 2.1.1.1
|
||||
* Fix #531 set default log level to ERROR and add 2 command-line options, "-L, --loglevel" and "-q, --quiet".
|
||||
* Implement #478 display UI options in usage help
|
||||
* Fix #547 add description to name field per HIG entry 2.1.1.1
|
||||
* Fix #531 set default log level to ERROR and add 2 command-line options, "-L, --loglevel" and "-q, --quiet".
|
||||
|
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
|
||||
|
23
MANIFEST.in
Normal file
23
MANIFEST.in
Normal file
@ -0,0 +1,23 @@
|
||||
include AUTHORS ChangeLog DEPENDS ez_setup.py LICENSE msgfmt.py RELEASE-VERSION version.py
|
||||
graft docs/man
|
||||
|
||||
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
|
||||
include deluge/ui/web/js/*.js
|
||||
exclude deluge/ui/web/js/*-debug.js
|
||||
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
|
32
create_potfiles_in.py
Executable file
32
create_potfiles_in.py
Executable file
@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
# Paths to exclude
|
||||
EXCLUSIONS = [
|
||||
"deluge/scripts",
|
||||
"deluge/i18n",
|
||||
]
|
||||
|
||||
POTFILE_IN = "deluge/i18n/POTFILES.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", ".in") \
|
||||
and dirpath not in EXCLUSIONS \
|
||||
and not compiled.match(dirpath):
|
||||
to_translate.append(os.path.join(dirpath, filename))
|
||||
|
||||
f = open(POTFILE_IN, "wb")
|
||||
for line in to_translate:
|
||||
f.write(line + "\n")
|
||||
|
||||
f.close()
|
||||
|
||||
print "Done"
|
@ -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
|
||||
|
5
debian/changelog
vendored
5
debian/changelog
vendored
@ -1,5 +0,0 @@
|
||||
deluge-torrent (0.9.08-1) unstable; urgency=low
|
||||
|
||||
* R8 release
|
||||
|
||||
-- Andrew Resch (andar) <andrewresch@gmail.com> Tue, 26 Aug 2008 16:31:14 -0800
|
5
debian/changelog.debian-lenny
vendored
5
debian/changelog.debian-lenny
vendored
@ -1,5 +0,0 @@
|
||||
deluge-torrent (0.6.0-svn3235-1) lenny; urgency=low
|
||||
|
||||
* Daily Build
|
||||
|
||||
-- Andrew Resch (andar) <andrewresch@gmail.com> Tue, 17 Jun 2008 16:31:14 -0800
|
5
debian/changelog.debian-sid
vendored
5
debian/changelog.debian-sid
vendored
@ -1,5 +0,0 @@
|
||||
deluge-torrent (0.6.0-svn3235-1) unstable; urgency=low
|
||||
|
||||
* Daily Build
|
||||
|
||||
-- Andrew Resch (andar) <andrewresch@gmail.com> Tue, 17 Jun 2008 16:31:14 -0800
|
5
debian/changelog.ubuntu-gutsy
vendored
5
debian/changelog.ubuntu-gutsy
vendored
@ -1,5 +0,0 @@
|
||||
deluge-torrent (0.6.0-svn3235-1) gutsy; urgency=low
|
||||
|
||||
* Daily Build
|
||||
|
||||
-- Andrew Resch (andar) <andrewresch@gmail.com> Tue, 17 Jun 2008 16:31:14 -0800
|
5
debian/changelog.ubuntu-hardy
vendored
5
debian/changelog.ubuntu-hardy
vendored
@ -1,5 +0,0 @@
|
||||
deluge-torrent (0.6.0-svn3235-1) hardy; urgency=low
|
||||
|
||||
* Daily Build
|
||||
|
||||
-- Andrew Resch (andar) <andrewresch@gmail.com> Tue, 17 Jun 2008 16:31:14 -0800
|
1
debian/compat
vendored
1
debian/compat
vendored
@ -1 +0,0 @@
|
||||
5
|
21
debian/control
vendored
21
debian/control
vendored
@ -1,21 +0,0 @@
|
||||
Source: deluge-torrent
|
||||
Section: net
|
||||
Priority: optional
|
||||
Maintainer: Andrew Resch (andar) <andrewresch@gmail.com>
|
||||
Build-Depends: debhelper (>= 5.0.37.2), python-all-dev (>= 2.3.5-11), python-all, python-support (>= 0.5.3), libboost-dev (>= 1.34.1), libboost-thread-dev (>= 1.34.1), libboost-date-time-dev (>= 1.34.1), libboost-filesystem-dev (>= 1.34.1), libboost-python-dev (>= 1.34.1), libboost-iostreams-dev (>= 1.34.1), zlib1g-dev, libssl-dev, dpatch, python-setuptools
|
||||
Standards-Version: 3.7.2
|
||||
|
||||
Package: deluge-torrent
|
||||
Architecture: any
|
||||
Depends: ${shlibs:Depends}, ${python:Depends}, python-gtk2, python-glade2, python-xdg, python-notify, notification-daemon | notification-daemon-xfce, python-dbus, librsvg2-common, python-pyopenssl, python-setuptools, python-pkg-resources
|
||||
Conflicts: deluge-torrent-common
|
||||
Replaces: deluge-torrent-common
|
||||
Description: A Bittorrent client written in Python/PyGTK
|
||||
Deluge is a Bittorrent client, created using Python and GTK+.
|
||||
.
|
||||
Deluge is intended to bring a native, full-featured client to Linux GTK
|
||||
desktop environments such as Gnome and XFCE.
|
||||
.
|
||||
It uses Rasterbar's version of libtorrent.
|
||||
.
|
||||
Homepage: http://www.deluge-torrent.org/
|
19
debian/control.debian-lenny
vendored
19
debian/control.debian-lenny
vendored
@ -1,19 +0,0 @@
|
||||
Source: deluge-torrent
|
||||
Section: net
|
||||
Priority: optional
|
||||
Maintainer: Andrew Resch (andar) <andrewresch@gmail.com>
|
||||
Build-Depends: debhelper (>= 5.0.37.2), python-all-dev (>= 2.3.5-11), python-all, python-support (>= 0.5.3), libboost-dev (>= 1.33.1), libboost-thread-dev (>= 1.33.1), libboost-date-time-dev (>= 1.33.1), libboost-filesystem-dev (>= 1.33.1), libboost-serialization-dev (>= 1.33.1), libboost-program-options-dev (>= 1.33.1), libboost-regex-dev (>= 1.33.1), libboost-python-dev (>= 1.33.1), zlib1g-dev, libssl-dev, dpatch, python-setuptools
|
||||
Standards-Version: 3.7.2
|
||||
|
||||
Package: deluge-torrent
|
||||
Architecture: any
|
||||
Depends: ${shlibs:Depends}, ${python:Depends}, python-gtk2, python-glade2, python-xdg, python-notify, notification-daemon | notification-daemon-xfce, python-dbus, librsvg2-common, python-pyopenssl, python-setuptools
|
||||
Description: A Bittorrent client written in Python/PyGTK
|
||||
Deluge is a Bittorrent client, created using Python and GTK+.
|
||||
.
|
||||
Deluge is intended to bring a native, full-featured client to Linux GTK
|
||||
desktop environments such as Gnome and XFCE.
|
||||
.
|
||||
It uses Rasterbar's version of libtorrent.
|
||||
.
|
||||
Homepage: http://www.deluge-torrent.org/
|
19
debian/control.debian-sid
vendored
19
debian/control.debian-sid
vendored
@ -1,19 +0,0 @@
|
||||
Source: deluge-torrent
|
||||
Section: net
|
||||
Priority: optional
|
||||
Maintainer: Andrew Resch (andar) <andrewresch@gmail.com>
|
||||
Build-Depends: debhelper (>= 5.0.37.2), python-all-dev (>= 2.3.5-11), python-all, python-support (>= 0.5.3), libboost1.36-dev (>= 1.36), libboost-thread1.36-dev (>= 1.36), libboost-date-time1.36-dev (>= 1.36), libboost-filesystem1.36-dev (>= 1.36), libboost-serialization1.36-dev (>= 1.36), libboost-program-options1.36-dev (>= 1.36), libboost-regex1.36-dev (>= 1.36), libboost-python1.36-dev (>= 1.36), zlib1g-dev, libssl-dev, dpatch, python-setuptools
|
||||
Standards-Version: 3.7.2
|
||||
|
||||
Package: deluge-torrent
|
||||
Architecture: any
|
||||
Depends: ${shlibs:Depends}, ${python:Depends}, python-gtk2, python-glade2, python-xdg, python-notify, notification-daemon | notification-daemon-xfce, python-dbus, librsvg2-common, python-pyopenssl, python-setuptools
|
||||
Description: A Bittorrent client written in Python/PyGTK
|
||||
Deluge is a Bittorrent client, created using Python and GTK+.
|
||||
.
|
||||
Deluge is intended to bring a native, full-featured client to Linux GTK
|
||||
desktop environments such as Gnome and XFCE.
|
||||
.
|
||||
It uses Rasterbar's version of libtorrent.
|
||||
.
|
||||
Homepage: http://www.deluge-torrent.org/
|
99
debian/copyright
vendored
99
debian/copyright
vendored
@ -1,99 +0,0 @@
|
||||
This package was debianized by Marcos Pinto (markybob) <markybob@gmail.com> on
|
||||
Fri, 31 Jul 2008 22:03:13 +0100.
|
||||
|
||||
It was downloaded from http://www.deluge-torrent.org/
|
||||
|
||||
Upstream Authors & Copyright:
|
||||
Andrew Resch
|
||||
Marcos Pinto
|
||||
Sadrul Habib Chowdhury
|
||||
Martijn Voncken
|
||||
|
||||
License:
|
||||
|
||||
This package is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This package is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this package; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
In addition, as a special exception, the copyright holders give
|
||||
permission to link the code of portions of this program with the OpenSSL
|
||||
library.
|
||||
You must obey the GNU General Public License in all respects for all of
|
||||
the code used other than OpenSSL. If you modify file(s) with this
|
||||
exception, you may extend this exception to your version of the file(s),
|
||||
but you are not obligated to do so. If you do not wish to do so, delete
|
||||
this exception statement from your version. If you delete this exception
|
||||
statement from all source files in the program, then also delete it here.
|
||||
|
||||
On Debian systems, the complete text of the GNU General
|
||||
Public License can be found in `/usr/share/common-licenses/GPL'.
|
||||
|
||||
libtorrent is (C) 2003-2008 Arvid Norberg arvid@cs.umu.se and its
|
||||
python bindings were initially written by Daniel Wallin in 2006.
|
||||
It is distributed unders terms of the BSD License below:
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in
|
||||
the documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the author nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
"libtorrent/include/libtorrent/asio*" are (C) 2003-2008 Christopher
|
||||
M. Kohlhoff <chris@kohlhoff.com> and distributed under terms of the Boost
|
||||
Software License, Version 1.0 :
|
||||
|
||||
Permission is hereby granted, free of charge, to any person or organization
|
||||
obtaining a copy of the software and accompanying documentation covered by
|
||||
this license (the "Software") to use, reproduce, display, distribute,
|
||||
execute, and transmit the Software, and to prepare derivative works of the
|
||||
Software, and to permit third-parties to whom the Software is furnished to
|
||||
do so, all subject to the following:
|
||||
The copyright notices in the Software and this entire statement, including
|
||||
the above license grant, this restriction and the following disclaimer,
|
||||
must be included in all copies of the Software, in whole or in part, and
|
||||
all derivative works of the Software, unless such copies or derivative
|
||||
works are solely in the form of machine-executable object code generated by
|
||||
a source language processor.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
"deluge/i18n/*" are (C) 2006 Rosetta Contributors and Canonical Ltd 2006 and distributed
|
||||
under the same license as the deluge software.
|
||||
|
||||
The Debian packaging is (C) 2006-2008, Marcos Pinto (markybob)
|
||||
<markybob@gmail.com> and is licensed under GPL3, see above.
|
2
debian/manpages
vendored
2
debian/manpages
vendored
@ -1,2 +0,0 @@
|
||||
docs/man/deluge.1
|
||||
docs/man/deluged.1
|
3
debian/menu
vendored
3
debian/menu
vendored
@ -1,3 +0,0 @@
|
||||
?package(deluge-torrent): needs="X11" section="Applications/Network/File Transfer" \
|
||||
title="Deluge BitTorrent Client" longtitle="Bittorrent client written in Python/PyGTK" \
|
||||
command="/usr/bin/deluge" icon="/usr/share/pixmaps/deluge.png"
|
1
debian/pyversions
vendored
1
debian/pyversions
vendored
@ -1 +0,0 @@
|
||||
2.5
|
1
debian/pyversions.ubuntu-gutsy
vendored
1
debian/pyversions.ubuntu-gutsy
vendored
@ -1 +0,0 @@
|
||||
2.5
|
1
debian/pyversions.ubuntu-hardy
vendored
1
debian/pyversions.ubuntu-hardy
vendored
@ -1 +0,0 @@
|
||||
2.5
|
75
debian/rules
vendored
75
debian/rules
vendored
@ -1,75 +0,0 @@
|
||||
#!/usr/bin/make -f
|
||||
|
||||
# Uncomment this to turn on verbose mode.
|
||||
#export DH_VERBOSE=1
|
||||
|
||||
# Dpatch targets
|
||||
include /usr/share/dpatch/dpatch.make
|
||||
|
||||
# Available python (using debian/pyversions) and destdir
|
||||
PYVERS = 2.5
|
||||
DESTDIR = $(CURDIR)/debian/deluge-torrent
|
||||
|
||||
# We need to known the target arch to enable/disable amd64 hack
|
||||
ARCH = $(shell dpkg-architecture -qDEB_BUILD_ARCH_CPU)
|
||||
ARCH64 = ia64 amd64 alpha kfreebsd-amd64 ppc64
|
||||
|
||||
CFLAGS = -Wall -g
|
||||
ifneq (,$(findstring noopt,$(DEB_BUILD_OPTIONS)))
|
||||
CFLAGS += -O0
|
||||
else
|
||||
CFLAGS += -O2
|
||||
endif
|
||||
# python-libtorrent need to define AMD64 to work fine on a 64 bits system
|
||||
ifneq (,$(findstring $(ARCH),$(ARCH64)))
|
||||
CFLAGS += -DAMD64
|
||||
endif
|
||||
|
||||
build: patch-stamp $(PYVERS:%=build-stamp%)
|
||||
build-stamp%: patch-stamp
|
||||
dh_testdir
|
||||
CFLAGS="$(CFLAGS)" python$* setup.py build
|
||||
touch $@
|
||||
|
||||
clean: unpatch
|
||||
dh_testdir
|
||||
dh_testroot
|
||||
rm -rf build/
|
||||
find . -name \*.pyc | xargs rm -f
|
||||
rm -rf build-stamp*
|
||||
dh_clean
|
||||
|
||||
install: build install-prereq $(PYVERS:%=install-%) install-finish
|
||||
install-prereq:
|
||||
dh_testdir
|
||||
dh_testroot
|
||||
dh_clean -k
|
||||
dh_installdirs
|
||||
install-%:
|
||||
python$* setup.py install --root=$(DESTDIR) --prefix=/usr --no-compile
|
||||
install-finish:
|
||||
# Desktop menu
|
||||
rm -rf $(DESTDIR)/usr/share/applications
|
||||
install -D -m644 $(CURDIR)/deluge/data/share/applications/deluge.desktop $(DESTDIR)/usr/share/applications/deluge.desktop
|
||||
|
||||
|
||||
binary-indep: build install
|
||||
binary-arch: build install
|
||||
dh_testdir
|
||||
dh_testroot
|
||||
dh_installchangelogs
|
||||
dh_installdocs
|
||||
dh_installmenu
|
||||
dh_strip
|
||||
dh_compress
|
||||
dh_fixperms
|
||||
dh_pysupport
|
||||
dh_desktop
|
||||
dh_installdeb
|
||||
dh_shlibdeps
|
||||
dh_gencontrol
|
||||
dh_md5sums
|
||||
dh_builddeb
|
||||
|
||||
binary: binary-indep binary-arch
|
||||
.PHONY: build clean binary-indep binary-arch binary install
|
@ -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.5.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)
|
||||
@ -57,4 +57,4 @@ try:
|
||||
check_version(lt)
|
||||
except ImportError:
|
||||
import libtorrent as lt
|
||||
check_version(lt)
|
||||
check_version(lt)
|
||||
|
480
deluge/common.py
480
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,11 +71,6 @@ if not hasattr(json, "dumps"):
|
||||
json.dump = dump
|
||||
json.load = load
|
||||
|
||||
import pkg_resources
|
||||
import xdg, xdg.BaseDirectory
|
||||
|
||||
from deluge.error import *
|
||||
|
||||
LT_TORRENT_STATE = {
|
||||
"Queued": 0,
|
||||
"Checking": 1,
|
||||
@ -86,7 +90,6 @@ LT_TORRENT_STATE = {
|
||||
7: "Checking Resume Data"
|
||||
}
|
||||
|
||||
|
||||
TORRENT_STATE = [
|
||||
"Allocating",
|
||||
"Checking",
|
||||
@ -101,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():
|
||||
@ -126,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():
|
||||
"""
|
||||
@ -144,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():
|
||||
@ -180,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
|
||||
@ -188,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):
|
||||
"""
|
||||
@ -200,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])
|
||||
|
||||
@ -210,7 +251,7 @@ def open_url_in_browser(url):
|
||||
|
||||
:param url: the url to open
|
||||
:type url: string
|
||||
|
||||
|
||||
"""
|
||||
import webbrowser
|
||||
webbrowser.open(url)
|
||||
@ -234,12 +275,36 @@ def fsize(fsize_b):
|
||||
"""
|
||||
fsize_kb = fsize_b / 1024.0
|
||||
if fsize_kb < 1024:
|
||||
return "%.1f KiB" % fsize_kb
|
||||
return "%.1f %s" % (fsize_kb, _("KiB"))
|
||||
fsize_mb = fsize_kb / 1024.0
|
||||
if fsize_mb < 1024:
|
||||
return "%.1f MiB" % fsize_mb
|
||||
return "%.1f %s" % (fsize_mb, _("MiB"))
|
||||
fsize_gb = fsize_mb / 1024.0
|
||||
return "%.1f GiB" % fsize_gb
|
||||
return "%.1f %s" % (fsize_gb, _("GiB"))
|
||||
|
||||
def fsize_short(fsize_b):
|
||||
"""
|
||||
Formats the bytes value into a string with K, M or G units
|
||||
|
||||
:param fsize_b: the filesize in bytes
|
||||
:type fsize_b: int
|
||||
:returns: formatted string in K, M or G units
|
||||
:rtype: string
|
||||
|
||||
**Usage**
|
||||
|
||||
>>> fsize(112245)
|
||||
'109.6 K'
|
||||
|
||||
"""
|
||||
fsize_kb = fsize_b / 1024.0
|
||||
if fsize_kb < 1024:
|
||||
return "%.1f %s" % (fsize_kb, _("K"))
|
||||
fsize_mb = fsize_kb / 1024.0
|
||||
if fsize_mb < 1024:
|
||||
return "%.1f %s" % (fsize_mb, _("M"))
|
||||
fsize_gb = fsize_mb / 1024.0
|
||||
return "%.1f %s" % (fsize_gb, _("G"))
|
||||
|
||||
def fpcnt(dec):
|
||||
"""
|
||||
@ -273,7 +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):
|
||||
"""
|
||||
@ -340,21 +412,21 @@ def ftime(seconds):
|
||||
|
||||
def fdate(seconds):
|
||||
"""
|
||||
Formats a date string in the locale's date representation based on the systems timezone
|
||||
Formats a date time string in the locale's date representation based on the systems timezone
|
||||
|
||||
:param seconds: time in seconds since the Epoch
|
||||
:type seconds: float
|
||||
:returns: a string in the locale's date representation or "" if seconds < 0
|
||||
:returns: a string in the locale's datetime representation or "" if seconds < 0
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
if seconds < 0:
|
||||
return ""
|
||||
return time.strftime("%x", time.localtime(seconds))
|
||||
return time.strftime("%x %X", time.localtime(seconds))
|
||||
|
||||
def is_url(url):
|
||||
"""
|
||||
A simple regex test to check if the URL is valid
|
||||
A simple test to check if the URL is valid
|
||||
|
||||
:param url: the url to test
|
||||
:type url: string
|
||||
@ -367,8 +439,7 @@ def is_url(url):
|
||||
True
|
||||
|
||||
"""
|
||||
import re
|
||||
return bool(re.search('^(https?|ftp|udp)://', url))
|
||||
return url.partition('://')[0] in ("http", "https", "ftp", "udp")
|
||||
|
||||
def is_magnet(uri):
|
||||
"""
|
||||
@ -385,34 +456,12 @@ 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
|
||||
|
||||
def fetch_url(url):
|
||||
"""
|
||||
Downloads a torrent file from a given URL and checks the file's validity
|
||||
|
||||
:param url: the url of the .torrent file to fetch
|
||||
:type url: string
|
||||
:returns: the filepath to the downloaded file
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
import urllib
|
||||
from deluge.log import LOG as log
|
||||
try:
|
||||
filename, headers = urllib.urlretrieve(url)
|
||||
except IOError:
|
||||
log.debug("Network error while trying to fetch torrent from %s", url)
|
||||
else:
|
||||
if filename.endswith(".torrent") or headers["content-type"] ==\
|
||||
"application/x-bittorrent":
|
||||
return filename
|
||||
else:
|
||||
log.debug("URL doesn't appear to be a valid torrent file: %s", url)
|
||||
return None
|
||||
|
||||
def create_magnet_uri(infohash, name=None, trackers=[]):
|
||||
"""
|
||||
Creates a magnet uri
|
||||
@ -469,20 +518,19 @@ def free_space(path):
|
||||
:type path: string
|
||||
:returns: the free space at path in bytes
|
||||
:rtype: int
|
||||
|
||||
|
||||
:raises InvalidPathError: if the path is not valid
|
||||
|
||||
"""
|
||||
if not os.path.exists(path):
|
||||
raise InvalidPathError("%s is not a valid path" % path)
|
||||
|
||||
|
||||
if windows_check():
|
||||
import win32file
|
||||
sectors, bytes, free, total = map(long, win32file.GetDiskFreeSpace(path))
|
||||
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):
|
||||
@ -503,58 +551,290 @@ 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
|
||||
|
||||
|
||||
def path_join(*parts):
|
||||
"""
|
||||
An implementation of os.path.join that always uses / for the separator
|
||||
to ensure that the correct paths are produced when working with internal
|
||||
paths on Windows.
|
||||
"""
|
||||
path = ''
|
||||
for part in parts:
|
||||
if not part:
|
||||
continue
|
||||
elif part[0] == '/':
|
||||
path = part
|
||||
elif not path:
|
||||
path = part
|
||||
else:
|
||||
path += '/' + part
|
||||
return path
|
||||
|
||||
XML_ESCAPES = (
|
||||
('&', '&'),
|
||||
('<', '<'),
|
||||
('>', '>'),
|
||||
('"', '"'),
|
||||
("'", ''')
|
||||
)
|
||||
|
||||
def xml_decode(string):
|
||||
"""
|
||||
Unescape a string that was previously encoded for use within xml.
|
||||
|
||||
:param string: The string to escape
|
||||
:type string: string
|
||||
:returns: The unescaped version of the string.
|
||||
:rtype: string
|
||||
"""
|
||||
for char, escape in XML_ESCAPES:
|
||||
string = string.replace(escape, char)
|
||||
return string
|
||||
|
||||
def xml_encode(string):
|
||||
"""
|
||||
Escape a string for use within an xml element or attribute.
|
||||
|
||||
:param string: The string to escape
|
||||
:type string: string
|
||||
:returns: An escaped version of the string.
|
||||
:rtype: string
|
||||
"""
|
||||
for char, escape in XML_ESCAPES:
|
||||
string = string.replace(char, escape)
|
||||
return string
|
||||
|
||||
def decode_string(s, encoding="utf8"):
|
||||
"""
|
||||
Decodes a string and 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.
|
||||
|
||||
|
||||
:param ver: the version
|
||||
:type ver: string
|
||||
|
||||
|
||||
"""
|
||||
def __init__(self, ver):
|
||||
ver = ver.lower()
|
||||
vs = ver.split("_") if "_" in ver else ver.split("-")
|
||||
self.version = vs[0]
|
||||
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):
|
||||
"""
|
||||
The comparison method.
|
||||
|
||||
|
||||
:param ver: the version to compare with
|
||||
:type ver: VersionSplit
|
||||
|
||||
"""
|
||||
|
||||
if self.version > ver.version or (self.suffix and self.suffix[:3] == "dev"):
|
||||
return 1
|
||||
if self.version < ver.version:
|
||||
return -1
|
||||
|
||||
if self.version == ver.version:
|
||||
if self.suffix == ver.suffix:
|
||||
return 0
|
||||
if self.suffix is None:
|
||||
return 1
|
||||
if ver.suffix is None:
|
||||
return -1
|
||||
if self.suffix < ver.suffix:
|
||||
return -1
|
||||
if self.suffix > ver.suffix:
|
||||
return 1
|
||||
"""
|
||||
# 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 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)
|
||||
|
||||
|
||||
# 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]
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# component.py
|
||||
#
|
||||
# Copyright (C) 2007, 2008 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2007-2010 Andrew Resch <andrewresch@gmail.com>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
@ -33,196 +33,413 @@
|
||||
#
|
||||
#
|
||||
|
||||
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
|
||||
|
||||
COMPONENT_STATE = [
|
||||
"Stopped",
|
||||
"Started",
|
||||
"Paused"
|
||||
]
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class ComponentAlreadyRegistered(Exception):
|
||||
pass
|
||||
|
||||
class Component(object):
|
||||
"""
|
||||
Component objects are singletons managed by the :class:`ComponentRegistry`.
|
||||
When a new Component object is instantiated, it will be automatically
|
||||
registered with the :class:`ComponentRegistry`.
|
||||
|
||||
The ComponentRegistry has the ability to start, stop, pause and shutdown the
|
||||
components registered with it.
|
||||
|
||||
**Events:**
|
||||
|
||||
**start()** - This method is called when the client has connected to a
|
||||
Deluge core.
|
||||
|
||||
**stop()** - This method is called when the client has disconnected from a
|
||||
Deluge core.
|
||||
|
||||
**update()** - This method is called every 1 second by default while the
|
||||
Componented is in a *Started* state. The interval can be
|
||||
specified during instantiation. The update() timer can be
|
||||
paused by instructing the :class:`ComponentRegistry` to pause
|
||||
this Component.
|
||||
|
||||
**shutdown()** - This method is called when the client is exiting. If the
|
||||
Component is in a "Started" state when this is called, a
|
||||
call to stop() will be issued prior to shutdown().
|
||||
|
||||
**States:**
|
||||
|
||||
A Component can be in one of these 5 states.
|
||||
|
||||
**Started** - The Component has been started by the :class:`ComponentRegistry`
|
||||
and will have it's update timer started.
|
||||
|
||||
**Starting** - The Component has had it's start method called, but it hasn't
|
||||
fully started yet.
|
||||
|
||||
**Stopped** - The Component has either been stopped or has yet to be started.
|
||||
|
||||
**Stopping** - The Component has had it's stop method called, but it hasn't
|
||||
fully stopped yet.
|
||||
|
||||
**Paused** - The Component has had it's update timer stopped, but will
|
||||
still be considered in a Started state.
|
||||
|
||||
"""
|
||||
def __init__(self, name, interval=1, depend=None):
|
||||
# Register with the ComponentRegistry
|
||||
register(name, self, depend)
|
||||
self._interval = interval
|
||||
self._timer = None
|
||||
self._state = COMPONENT_STATE.index("Stopped")
|
||||
self._name = name
|
||||
self._component_name = name
|
||||
self._component_interval = interval
|
||||
self._component_depend = depend
|
||||
self._component_state = "Stopped"
|
||||
self._component_timer = None
|
||||
self._component_starting_deferred = None
|
||||
self._component_stopping_deferred = None
|
||||
_ComponentRegistry.register(self)
|
||||
|
||||
def get_state(self):
|
||||
return self._state
|
||||
def __del__(self):
|
||||
if _ComponentRegistry:
|
||||
_ComponentRegistry.deregister(self)
|
||||
|
||||
def get_component_name(self):
|
||||
return self._name
|
||||
|
||||
def start(self):
|
||||
pass
|
||||
|
||||
def _start(self):
|
||||
self._state = COMPONENT_STATE.index("Started")
|
||||
def _component_start_timer(self):
|
||||
if hasattr(self, "update"):
|
||||
self._timer = LoopingCall(self.update)
|
||||
self._timer.start(self._interval)
|
||||
self._component_timer = LoopingCall(self.update)
|
||||
self._component_timer.start(self._component_interval)
|
||||
|
||||
def stop(self):
|
||||
pass
|
||||
def _component_start(self):
|
||||
def on_start(result):
|
||||
self._component_state = "Started"
|
||||
self._component_starting_deferred = None
|
||||
self._component_start_timer()
|
||||
return True
|
||||
|
||||
def _stop(self):
|
||||
self._state = COMPONENT_STATE.index("Stopped")
|
||||
try:
|
||||
self._timer.stop()
|
||||
except:
|
||||
pass
|
||||
def on_start_fail(result):
|
||||
self._component_state = "Stopped"
|
||||
self._component_starting_deferred = None
|
||||
log.error(result)
|
||||
return result
|
||||
|
||||
def _pause(self):
|
||||
self._state = COMPONENT_STATE.index("Paused")
|
||||
try:
|
||||
self._timer.stop()
|
||||
except:
|
||||
pass
|
||||
if self._component_state == "Stopped":
|
||||
if hasattr(self, "start"):
|
||||
self._component_state = "Starting"
|
||||
d = maybeDeferred(self.start)
|
||||
d.addCallback(on_start)
|
||||
d.addErrback(on_start_fail)
|
||||
self._component_starting_deferred = d
|
||||
else:
|
||||
d = maybeDeferred(on_start, None)
|
||||
elif self._component_state == "Starting":
|
||||
return self._component_starting_deferred
|
||||
elif self._component_state == "Started":
|
||||
d = succeed(True)
|
||||
else:
|
||||
d = fail("Cannot start a component not in a Stopped state!")
|
||||
|
||||
def _resume(self):
|
||||
self._start()
|
||||
return d
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
def _component_stop(self):
|
||||
def on_stop(result):
|
||||
self._component_state = "Stopped"
|
||||
if self._component_timer and self._component_timer.running:
|
||||
self._component_timer.stop()
|
||||
return True
|
||||
|
||||
class ComponentRegistry:
|
||||
def __init__(self):
|
||||
self.components = {}
|
||||
self.depend = {}
|
||||
def on_stop_fail(result):
|
||||
self._component_state = "Started"
|
||||
self._component_stopping_deferred = None
|
||||
log.error(result)
|
||||
return result
|
||||
|
||||
def register(self, name, obj, depend):
|
||||
"""Registers a component.. depend must be list or None"""
|
||||
log.debug("Registered %s with ComponentRegistry..", name)
|
||||
self.components[name] = obj
|
||||
if depend != None:
|
||||
self.depend[name] = depend
|
||||
if self._component_state != "Stopped" and self._component_state != "Stopping":
|
||||
if hasattr(self, "stop"):
|
||||
self._component_state = "Stopping"
|
||||
d = maybeDeferred(self.stop)
|
||||
d.addCallback(on_stop)
|
||||
d.addErrback(on_stop_fail)
|
||||
self._component_stopping_deferred = d
|
||||
else:
|
||||
d = maybeDeferred(on_stop, None)
|
||||
|
||||
def get(self, name):
|
||||
"""Returns a reference to the component 'name'"""
|
||||
return self.components[name]
|
||||
if self._component_state == "Stopping":
|
||||
return self._component_stopping_deferred
|
||||
|
||||
return succeed(None)
|
||||
|
||||
def _component_pause(self):
|
||||
def on_pause(result):
|
||||
self._component_state = "Paused"
|
||||
|
||||
if self._component_state == "Started":
|
||||
if self._component_timer and self._component_timer.running:
|
||||
d = maybeDeferred(self._component_timer.stop)
|
||||
d.addCallback(on_pause)
|
||||
else:
|
||||
d = succeed(None)
|
||||
elif self._component_state == "Paused":
|
||||
d = succeed(None)
|
||||
else:
|
||||
d = fail("Cannot pause a component in a non-Started state!")
|
||||
|
||||
return d
|
||||
|
||||
def _component_resume(self):
|
||||
def on_resume(result):
|
||||
self._component_state = "Started"
|
||||
|
||||
if self._component_state == "Paused":
|
||||
d = maybeDeferred(self._component_start_timer)
|
||||
d.addCallback(on_resume)
|
||||
else:
|
||||
d = fail("Component cannot be resumed from a non-Paused state!")
|
||||
|
||||
return d
|
||||
|
||||
def _component_shutdown(self):
|
||||
def on_stop(result):
|
||||
if hasattr(self, "shutdown"):
|
||||
return maybeDeferred(self.shutdown)
|
||||
return succeed(None)
|
||||
|
||||
d = self._component_stop()
|
||||
d.addCallback(on_stop)
|
||||
return d
|
||||
|
||||
def start(self):
|
||||
"""Starts all components"""
|
||||
for component in self.components.keys():
|
||||
self.start_component(component)
|
||||
|
||||
def start_component(self, name):
|
||||
"""Starts a component"""
|
||||
# Check to see if this component has any dependencies
|
||||
if self.depend.has_key(name):
|
||||
for depend in self.depend[name]:
|
||||
self.start_component(depend)
|
||||
|
||||
# Only start if the component is stopped.
|
||||
if self.components[name].get_state() == \
|
||||
COMPONENT_STATE.index("Stopped"):
|
||||
log.debug("Starting component %s..", name)
|
||||
self.components[name].start()
|
||||
self.components[name]._start()
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
"""Stops all components"""
|
||||
for component in self.components.keys():
|
||||
self.stop_component(component)
|
||||
|
||||
def stop_component(self, component):
|
||||
if self.components[component].get_state() != \
|
||||
COMPONENT_STATE.index("Stopped"):
|
||||
log.debug("Stopping component %s..", component)
|
||||
self.components[component].stop()
|
||||
self.components[component]._stop()
|
||||
|
||||
def pause(self):
|
||||
"""Pauses all components. Stops calling update()"""
|
||||
for component in self.components.keys():
|
||||
self.pause_component(component)
|
||||
|
||||
def pause_component(self, component):
|
||||
if self.components[component].get_state() not in \
|
||||
[COMPONENT_STATE.index("Paused"), COMPONENT_STATE.index("Stopped")]:
|
||||
log.debug("Pausing component %s..", component)
|
||||
self.components[component]._pause()
|
||||
|
||||
def resume(self):
|
||||
"""Resumes all components. Starts calling update()"""
|
||||
for component in self.components.keys():
|
||||
self.resume_component(component)
|
||||
|
||||
def resume_component(self, component):
|
||||
if self.components[component].get_state() == COMPONENT_STATE.index("Paused"):
|
||||
log.debug("Resuming component %s..", component)
|
||||
self.components[component]._resume()
|
||||
pass
|
||||
|
||||
def update(self):
|
||||
"""Updates all components"""
|
||||
for component in self.components.keys():
|
||||
# Only update the component if it's started
|
||||
if self.components[component].get_state() == \
|
||||
COMPONENT_STATE.index("Started"):
|
||||
self.components[component].update()
|
||||
|
||||
return True
|
||||
pass
|
||||
|
||||
def shutdown(self):
|
||||
"""Shuts down all components. This should be called when the program
|
||||
exits so that components can do any necessary clean-up."""
|
||||
# Stop all components first
|
||||
self.stop()
|
||||
for component in self.components.keys():
|
||||
log.debug("Shutting down component %s..", component)
|
||||
try:
|
||||
self.components[component].shutdown()
|
||||
except Exception, e:
|
||||
log.debug("Unable to call shutdown()")
|
||||
log.exception(e)
|
||||
pass
|
||||
|
||||
class ComponentRegistry(object):
|
||||
"""
|
||||
The ComponentRegistry holds a list of currently registered
|
||||
:class:`Component` objects. It is used to manage the Components by
|
||||
starting, stopping, pausing and shutting them down.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.components = {}
|
||||
# Stores all of the components that are dependent on a particular component
|
||||
self.dependents = defaultdict(list)
|
||||
|
||||
def register(self, obj):
|
||||
"""
|
||||
Registers a component object with the registry. This is done
|
||||
automatically when a Component object is instantiated.
|
||||
|
||||
:param obj: the Component object
|
||||
:type obj: object
|
||||
|
||||
:raises ComponentAlreadyRegistered: if a component with the same name is already registered.
|
||||
|
||||
"""
|
||||
name = obj._component_name
|
||||
if name in self.components:
|
||||
raise ComponentAlreadyRegistered(
|
||||
"Component already registered with name %s" % name)
|
||||
|
||||
self.components[obj._component_name] = obj
|
||||
if obj._component_depend:
|
||||
for depend in obj._component_depend:
|
||||
self.dependents[depend].append(name)
|
||||
|
||||
def deregister(self, obj):
|
||||
"""
|
||||
Deregisters a component from the registry. A stop will be
|
||||
issued to the component prior to deregistering it.
|
||||
|
||||
:param obj: the Component object
|
||||
:type obj: object
|
||||
|
||||
"""
|
||||
|
||||
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, obj._component_name)
|
||||
else:
|
||||
return succeed(None)
|
||||
|
||||
def start(self, names=[]):
|
||||
"""
|
||||
Starts Components that are currently in a Stopped state and their
|
||||
dependencies. If *names* is specified, will only start those
|
||||
Components and their dependencies and if not it will start all
|
||||
registered components.
|
||||
|
||||
:param names: a list of Components to start
|
||||
:type names: list
|
||||
|
||||
:returns: a Deferred object that will fire once all Components have been sucessfully started
|
||||
:rtype: twisted.internet.defer.Deferred
|
||||
|
||||
"""
|
||||
# Start all the components if names is empty
|
||||
if not names:
|
||||
names = self.components.keys()
|
||||
elif isinstance(names, str):
|
||||
names = [names]
|
||||
|
||||
def on_depends_started(result, name):
|
||||
return self.components[name]._component_start()
|
||||
|
||||
deferreds = []
|
||||
|
||||
for name in names:
|
||||
if self.components[name]._component_depend:
|
||||
# This component has depends, so we need to start them first.
|
||||
d = self.start(self.components[name]._component_depend)
|
||||
d.addCallback(on_depends_started, name)
|
||||
deferreds.append(d)
|
||||
else:
|
||||
deferreds.append(self.components[name]._component_start())
|
||||
|
||||
return DeferredList(deferreds)
|
||||
|
||||
def stop(self, names=[]):
|
||||
"""
|
||||
Stops Components that are currently not in a Stopped state. If
|
||||
*names* is specified, then it will only stop those Components,
|
||||
and if not it will stop all the registered Components.
|
||||
|
||||
:param names: a list of Components to start
|
||||
:type names: list
|
||||
|
||||
:returns: a Deferred object that will fire once all Components have been sucessfully stopped
|
||||
:rtype: twisted.internet.defer.Deferred
|
||||
|
||||
"""
|
||||
if not names:
|
||||
names = self.components.keys()
|
||||
elif isinstance(names, str):
|
||||
names = [names]
|
||||
|
||||
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:
|
||||
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)
|
||||
|
||||
def pause(self, names=[]):
|
||||
"""
|
||||
Pauses Components that are currently in a Started state. If
|
||||
*names* is specified, then it will only pause those Components,
|
||||
and if not it will pause all the registered Components.
|
||||
|
||||
:param names: a list of Components to pause
|
||||
:type names: list
|
||||
|
||||
:returns: a Deferred object that will fire once all Components have been sucessfully paused
|
||||
:rtype: twisted.internet.defer.Deferred
|
||||
|
||||
"""
|
||||
if not names:
|
||||
names = self.components.keys()
|
||||
elif isinstance(names, str):
|
||||
names = [names]
|
||||
|
||||
deferreds = []
|
||||
|
||||
for name in names:
|
||||
if self.components[name]._component_state == "Started":
|
||||
deferreds.append(self.components[name]._component_pause())
|
||||
|
||||
return DeferredList(deferreds)
|
||||
|
||||
def resume(self, names=[]):
|
||||
"""
|
||||
Resumes Components that are currently in a Paused state. If
|
||||
*names* is specified, then it will only resume those Components,
|
||||
and if not it will resume all the registered Components.
|
||||
|
||||
:param names: a list of Components to resume
|
||||
:type names: list
|
||||
|
||||
:returns: a Deferred object that will fire once all Components have been successfully resumed
|
||||
:rtype: twisted.internet.defer.Deferred
|
||||
|
||||
"""
|
||||
if not names:
|
||||
names = self.components.keys()
|
||||
elif isinstance(names, str):
|
||||
names = [names]
|
||||
|
||||
deferreds = []
|
||||
|
||||
for name in names:
|
||||
if self.components[name]._component_state == "Paused":
|
||||
deferreds.append(self.components[name]._component_resume())
|
||||
|
||||
return DeferredList(deferreds)
|
||||
|
||||
def shutdown(self):
|
||||
"""
|
||||
Shutdowns all Components regardless of state. This will call
|
||||
:meth:`stop` on call the components prior to shutting down. This should
|
||||
be called when the program is exiting to ensure all Components have a
|
||||
chance to properly shutdown.
|
||||
|
||||
:returns: a Deferred object that will fire once all Components have been successfully shut down
|
||||
:rtype: twisted.internet.defer.Deferred
|
||||
|
||||
"""
|
||||
def on_stopped(result):
|
||||
return DeferredList(map(lambda c: c._component_shutdown(), self.components.values()))
|
||||
|
||||
return self.stop(self.components.keys()).addCallback(on_stopped)
|
||||
|
||||
def update(self):
|
||||
"""
|
||||
Updates all Components that are in a Started state.
|
||||
|
||||
"""
|
||||
for component in self.components.items():
|
||||
component.update()
|
||||
|
||||
_ComponentRegistry = ComponentRegistry()
|
||||
|
||||
def register(name, obj, depend=None):
|
||||
"""Registers a component with the registry"""
|
||||
_ComponentRegistry.register(name, obj, depend)
|
||||
deregister = _ComponentRegistry.deregister
|
||||
start = _ComponentRegistry.start
|
||||
stop = _ComponentRegistry.stop
|
||||
pause = _ComponentRegistry.pause
|
||||
resume = _ComponentRegistry.resume
|
||||
update = _ComponentRegistry.update
|
||||
shutdown = _ComponentRegistry.shutdown
|
||||
|
||||
def start(component=None):
|
||||
"""Starts all components"""
|
||||
if component == None:
|
||||
_ComponentRegistry.start()
|
||||
else:
|
||||
_ComponentRegistry.start_component(component)
|
||||
def get(name):
|
||||
"""
|
||||
Return a reference to a component.
|
||||
|
||||
def stop(component=None):
|
||||
"""Stops all or specified components"""
|
||||
if component == None:
|
||||
_ComponentRegistry.stop()
|
||||
else:
|
||||
_ComponentRegistry.stop_component(component)
|
||||
:param name: the Component name to get
|
||||
:type name: string
|
||||
|
||||
def pause(component=None):
|
||||
"""Pauses all or specificed components"""
|
||||
if component == None:
|
||||
_ComponentRegistry.pause()
|
||||
else:
|
||||
_ComponentRegistry.pause_component(component)
|
||||
:returns: the Component object
|
||||
:rtype: object
|
||||
|
||||
def resume(component=None):
|
||||
"""Resumes all or specificed components"""
|
||||
if component == None:
|
||||
_ComponentRegistry.resume()
|
||||
else:
|
||||
_ComponentRegistry.resume_component(component)
|
||||
:raises KeyError: if the Component does not exist
|
||||
|
||||
def update():
|
||||
"""Updates all components"""
|
||||
_ComponentRegistry.update()
|
||||
|
||||
def shutdown():
|
||||
"""Shutdowns all components"""
|
||||
_ComponentRegistry.shutdown()
|
||||
|
||||
def get(component):
|
||||
"""Return a reference to the component"""
|
||||
return _ComponentRegistry.get(component)
|
||||
"""
|
||||
return _ComponentRegistry.components[name]
|
||||
|
@ -45,9 +45,9 @@ The format of the config file is two json encoded dicts:
|
||||
<version dict>
|
||||
<content dict>
|
||||
|
||||
The version dict contains two keys: file and format. The format version is
|
||||
controlled by the Config class. It should only be changed when anything below
|
||||
it is changed directly by the Config class. An example of this would be if we
|
||||
The version dict contains two keys: file and format. The format version is
|
||||
controlled by the Config class. It should only be changed when anything below
|
||||
it is changed directly by the Config class. An example of this would be if we
|
||||
changed the serializer for the content to something different.
|
||||
|
||||
The config file version is changed by the 'owner' of the config file. This is
|
||||
@ -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:
|
||||
@ -156,6 +159,9 @@ class Config(object):
|
||||
|
||||
self.load()
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self.__config
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""
|
||||
See
|
||||
@ -184,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))
|
||||
@ -197,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
|
||||
@ -247,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):
|
||||
"""
|
||||
@ -345,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]
|
||||
@ -368,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)
|
||||
|
||||
@ -393,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
|
||||
|
||||
@ -421,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,9 +67,18 @@ 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.handle_alerts()
|
||||
self.delayed_calls = [dc for dc in self.delayed_calls if dc.active()]
|
||||
self.handle_alerts(wait=self.wait_on_handler)
|
||||
|
||||
def stop(self):
|
||||
for dc in self.delayed_calls:
|
||||
if dc.active():
|
||||
dc.cancel()
|
||||
self.delayed_calls = []
|
||||
|
||||
def register_handler(self, alert_type, handler):
|
||||
"""
|
||||
@ -107,18 +118,17 @@ 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]:
|
||||
if not wait:
|
||||
reactor.callLater(0, handler, alert)
|
||||
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,53 +119,126 @@ 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
|
||||
line = line.strip()
|
||||
if line.startswith("#") or not line:
|
||||
# This line is a comment or empty
|
||||
continue
|
||||
try:
|
||||
lsplit = line.split(":")
|
||||
@ -131,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,131 +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":
|
||||
filepath = os.path.join(self.config["autoadd_location"], filename)
|
||||
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.
|
||||
#
|
||||
@ -17,9 +18,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,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
|
||||
@ -288,28 +330,6 @@ class Core(component.Component):
|
||||
log.debug("Removing torrent %s from the core.", torrent_id)
|
||||
return self.torrentmanager.remove(torrent_id, remove_data)
|
||||
|
||||
@export
|
||||
def get_stats(self):
|
||||
"""
|
||||
Deprecated: please use get_session_status()
|
||||
|
||||
"""
|
||||
warnings.warn("Use get_session_status() instead of get_stats()", DeprecationWarning)
|
||||
stats = self.get_session_status(["payload_download_rate", "payload_upload_rate",
|
||||
"dht_nodes", "has_incoming_connections", "download_rate", "upload_rate"])
|
||||
|
||||
stats.update({
|
||||
#dynamic stats:
|
||||
"num_connections":self.session.num_connections(),
|
||||
"free_space":deluge.common.free_space(self.config["download_location"]),
|
||||
#max config values:
|
||||
"max_download":self.config["max_download_speed"],
|
||||
"max_upload":self.config["max_upload_speed"],
|
||||
"max_num_connections":self.config["max_connections_global"],
|
||||
})
|
||||
|
||||
return stats
|
||||
|
||||
@export
|
||||
def get_session_status(self, keys):
|
||||
"""
|
||||
@ -406,30 +426,43 @@ class Core(component.Component):
|
||||
for torrent_id in torrent_ids:
|
||||
self.torrentmanager[torrent_id].resume()
|
||||
|
||||
@export
|
||||
def get_torrent_status(self, torrent_id, keys):
|
||||
# Build the status dictionary
|
||||
status = self.torrentmanager[torrent_id].get_status(keys)
|
||||
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 {}
|
||||
|
||||
# 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))
|
||||
# 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_torrents_status(self, filter_dict, keys):
|
||||
def get_torrent_status(self, torrent_id, keys, diff=False):
|
||||
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):
|
||||
"""
|
||||
returns all torrents , optionally filtered by filter_dict.
|
||||
"""
|
||||
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)
|
||||
|
||||
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):
|
||||
@ -476,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]
|
||||
|
||||
@ -490,24 +523,6 @@ class Core(component.Component):
|
||||
"""Returns the current number of connections"""
|
||||
return self.session.num_connections()
|
||||
|
||||
@export
|
||||
def get_dht_nodes(self):
|
||||
"""Returns the number of dht nodes"""
|
||||
warnings.warn("Use get_session_status().", DeprecationWarning)
|
||||
return self.session.status().dht_nodes
|
||||
|
||||
@export
|
||||
def get_download_rate(self):
|
||||
"""Returns the payload download rate"""
|
||||
warnings.warn("Use get_session_status().", DeprecationWarning)
|
||||
return self.session.status().payload_download_rate
|
||||
|
||||
@export
|
||||
def get_upload_rate(self):
|
||||
"""Returns the payload upload rate"""
|
||||
warnings.warn("Use get_session_status().", DeprecationWarning)
|
||||
return self.session.status().payload_upload_rate
|
||||
|
||||
@export
|
||||
def get_available_plugins(self):
|
||||
"""Returns a list of plugins available in the core"""
|
||||
@ -575,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"""
|
||||
@ -605,11 +625,31 @@ 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 get_health(self):
|
||||
"""Returns True if we have established incoming connections"""
|
||||
warnings.warn("Use get_session_status().", DeprecationWarning)
|
||||
return self.session.status().has_incoming_connections
|
||||
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):
|
||||
@ -622,7 +662,7 @@ class Core(component.Component):
|
||||
webseeds, private, created_by, trackers, add_to_session):
|
||||
|
||||
log.debug("creating torrent..")
|
||||
threading.Thread(target=_create_torrent_thread,
|
||||
threading.Thread(target=self._create_torrent_thread,
|
||||
args=(
|
||||
path,
|
||||
tracker,
|
||||
@ -655,14 +695,21 @@ class Core(component.Component):
|
||||
self.add_torrent_file(os.path.split(target)[1], open(target, "rb").read(), options)
|
||||
|
||||
@export
|
||||
def upload_plugin(self, filename, plugin_data):
|
||||
def upload_plugin(self, filename, filedump):
|
||||
"""This method is used to upload new plugins to the daemon. It is used
|
||||
when connecting to the daemon remotely and installing a new plugin on
|
||||
the client side. 'plugin_data' is a xmlrpc.Binary object of the file data,
|
||||
ie, plugin_file.read()"""
|
||||
|
||||
try:
|
||||
filedump = base64.decodestring(filedump)
|
||||
except Exception, e:
|
||||
log.error("There was an error decoding the filedump string!")
|
||||
log.exception(e)
|
||||
return
|
||||
|
||||
f = open(os.path.join(deluge.configmanager.get_config_dir(), "plugins", filename), "wb")
|
||||
f.write(plugin_data.data)
|
||||
f.write(filedump)
|
||||
f.close()
|
||||
component.get("CorePluginManager").scan_for_plugins()
|
||||
|
||||
@ -718,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):
|
||||
@ -729,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):
|
||||
@ -780,21 +841,27 @@ class Core(component.Component):
|
||||
"""
|
||||
from twisted.web.client import getPage
|
||||
|
||||
d = getPage("http://deluge-torrent.org/test_port.php?port=%s" % self.get_listen_port())
|
||||
d = getPage("http://deluge-torrent.org/test_port.php?port=%s" %
|
||||
self.get_listen_port(), timeout=30)
|
||||
|
||||
def on_get_page(result):
|
||||
return bool(int(result))
|
||||
|
||||
def logError(failure):
|
||||
log.warning("Error testing listen port: %s", failure)
|
||||
|
||||
d.addCallback(on_get_page)
|
||||
d.addErrback(logError)
|
||||
|
||||
return d
|
||||
|
||||
@export
|
||||
def get_free_space(self, path):
|
||||
def get_free_space(self, path=None):
|
||||
"""
|
||||
Returns the number of free bytes at path
|
||||
|
||||
:param path: the path to check free space at
|
||||
:param path: the path to check free space at, if None, use the default
|
||||
download location
|
||||
:type path: string
|
||||
|
||||
:returns: the number of free bytes at path
|
||||
@ -803,7 +870,12 @@ class Core(component.Component):
|
||||
:raises InvalidPathError: if the path is invalid
|
||||
|
||||
"""
|
||||
return deluge.common.free_space(path)
|
||||
if not path:
|
||||
path = self.config["download_location"]
|
||||
try:
|
||||
return deluge.common.free_space(path)
|
||||
except InvalidPathError:
|
||||
return 0
|
||||
|
||||
@export
|
||||
def get_libtorrent_version(self):
|
||||
@ -815,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,24 +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)
|
||||
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():
|
||||
@ -118,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)
|
||||
|
||||
@ -131,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:
|
||||
@ -175,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):
|
||||
@ -202,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")
|
||||
@ -84,16 +106,15 @@ def tracker_error_filter(torrent_ids, values):
|
||||
# If this is a tracker_host, then we need to filter on it
|
||||
if values[0] != "Error":
|
||||
for torrent_id in torrent_ids:
|
||||
if values[0] in tm[torrent_id].get_status(["tracker_host"])["tracker_host"]:
|
||||
if values[0] == tm[torrent_id].get_status(["tracker_host"])["tracker_host"]:
|
||||
filtered_torrent_ids.append(torrent_id)
|
||||
return filtered_torrent_ids
|
||||
|
||||
# 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,22 +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
|
||||
|
||||
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:
|
||||
@ -237,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):
|
||||
@ -97,7 +99,7 @@ class OldStateUpgrader:
|
||||
torrent_info = lt.torrent_info(lt.bdecode(_file.read()))
|
||||
_file.close()
|
||||
except (IOError, RuntimeError), e:
|
||||
log.warning("Unable to open %s: %s", filepath, e)
|
||||
log.warning("Unable to open %s: %s", torrent_path, e)
|
||||
|
||||
# Copy the torrent file to the new location
|
||||
import shutil
|
||||
|
@ -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):
|
||||
@ -96,8 +96,7 @@ class PluginManager(deluge.pluginmanagerbase.PluginManagerBase,
|
||||
try:
|
||||
status[field] = self.status_fields[field](torrent_id)
|
||||
except KeyError:
|
||||
log.warning("Status field %s is not registered with the\
|
||||
PluginManager.", field)
|
||||
pass
|
||||
return status
|
||||
|
||||
def register_status_field(self, field, function):
|
||||
|
@ -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,
|
||||
@ -58,9 +58,11 @@ DEFAULT_PREFS = {
|
||||
"listen_ports": [6881, 6891],
|
||||
"listen_interface": "",
|
||||
"copy_torrent_file": False,
|
||||
"del_copy_torrent_file": False,
|
||||
"torrentfiles_location": deluge.common.get_default_download_dir(),
|
||||
"plugins_location": os.path.join(deluge.configmanager.get_config_dir(), "plugins"),
|
||||
"prioritize_first_last_pieces": False,
|
||||
"sequential_download": False,
|
||||
"random_port": True,
|
||||
"dht": True,
|
||||
"upnp": True,
|
||||
@ -84,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,
|
||||
@ -139,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):
|
||||
@ -147,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):
|
||||
@ -246,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
|
||||
@ -268,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:
|
||||
@ -283,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")
|
||||
@ -336,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)
|
||||
@ -386,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):
|
||||
@ -462,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):
|
||||
@ -488,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!")
|
||||
|
||||
@ -511,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
|
||||
@ -86,11 +96,31 @@ def export(auth_level=AUTH_LEVEL_DEFAULT):
|
||||
else:
|
||||
return wrap
|
||||
|
||||
class DelugeError(Exception):
|
||||
pass
|
||||
|
||||
class NotAuthorizedError(DelugeError):
|
||||
pass
|
||||
def format_request(call):
|
||||
"""
|
||||
Format the RPCRequest message for debug printing
|
||||
|
||||
:param call: the request
|
||||
:type call: a RPCRequest
|
||||
|
||||
:returns: a formatted string for printing
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
try:
|
||||
s = call[1] + "("
|
||||
if call[2]:
|
||||
s += ", ".join([str(x) for x in call[2]])
|
||||
if call[3]:
|
||||
if call[2]:
|
||||
s += ", "
|
||||
s += ", ".join([key + "=" + str(value) for key, value in call[3].items()])
|
||||
s += ")"
|
||||
except UnicodeEncodeError:
|
||||
return "UnicodeEncodeError, call: %s" % call
|
||||
else:
|
||||
return s
|
||||
|
||||
class ServerContextFactory(object):
|
||||
def getContext(self):
|
||||
@ -106,70 +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 a thread
|
||||
with :meth:`dispatch`.
|
||||
|
||||
:param data: the data from the client. It should be a zlib compressed
|
||||
rencoded string.
|
||||
:type data: str
|
||||
If the RPC Request message is valid, then the method is called in
|
||||
:meth:`dispatch`.
|
||||
|
||||
: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
|
||||
|
||||
# Format the RPCRequest message for debug printing
|
||||
try:
|
||||
s = call[1] + "("
|
||||
if call[2]:
|
||||
s += ", ".join([str(x) for x in call[2]])
|
||||
if call[3]:
|
||||
if call[2]:
|
||||
s += ", "
|
||||
s += ", ".join([key + "=" + str(value) for key, value in call[3].items()])
|
||||
s += ")"
|
||||
except UnicodeEncodeError:
|
||||
pass
|
||||
#log.debug("RPCRequest had some non-ascii text..")
|
||||
else:
|
||||
pass
|
||||
#log.debug("RPCRequest: %s", s)
|
||||
|
||||
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):
|
||||
"""
|
||||
@ -177,16 +171,18 @@ 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
|
||||
|
||||
@ -208,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
|
||||
@ -229,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.
|
||||
@ -270,18 +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:
|
||||
@ -324,6 +351,8 @@ class RPCServer(component.Component):
|
||||
|
||||
self.factory = Factory()
|
||||
self.factory.protocol = DelugeRPCProtocol
|
||||
self.factory.session_id = -1
|
||||
|
||||
# Holds the registered methods
|
||||
self.factory.methods = {}
|
||||
# Holds the session_ids and auth levels
|
||||
@ -333,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
|
||||
|
||||
@ -376,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.
|
||||
@ -399,6 +440,66 @@ class RPCServer(component.Component):
|
||||
"""
|
||||
return self.factory.methods.keys()
|
||||
|
||||
def get_session_id(self):
|
||||
"""
|
||||
Returns the session id of the current RPC.
|
||||
|
||||
:returns: the session id, this will be -1 if no connections have been made
|
||||
:rtype: int
|
||||
|
||||
"""
|
||||
return self.factory.session_id
|
||||
|
||||
def get_session_user(self):
|
||||
"""
|
||||
Returns the username calling the current RPC.
|
||||
|
||||
:returns: the username of the user calling the current RPC
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
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.
|
||||
@ -408,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
|
||||
@ -416,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
|
||||
@ -459,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
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
Icon=deluge
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Network;
|
||||
StartupNotify=true
|
||||
MimeType=application/x-bittorrent;
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# common.py
|
||||
# decorators.py
|
||||
#
|
||||
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2010 John Garland <johnnybg+deluge@gmail.com>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
@ -33,19 +33,19 @@
|
||||
#
|
||||
#
|
||||
|
||||
from functools import wraps
|
||||
|
||||
import pkg_resources
|
||||
import os.path
|
||||
def proxy(proxy_func):
|
||||
"""
|
||||
Factory class which returns a decorator that passes
|
||||
the decorated function to a proxy function
|
||||
|
||||
def get_resource(filename):
|
||||
return pkg_resources.resource_filename("blocklist", os.path.join("data", filename))
|
||||
|
||||
def raiseError(error):
|
||||
def safer(func):
|
||||
def new(self, *args, **kwargs):
|
||||
try:
|
||||
return func(self, *args, **kwargs)
|
||||
except:
|
||||
raise error
|
||||
return new
|
||||
return safer
|
||||
:param proxy_func: the proxy function
|
||||
:type proxy_func: function
|
||||
"""
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
return proxy_func(func, *args, **kwargs)
|
||||
return wrapper
|
||||
return decorator
|
@ -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
|
||||
|
112
deluge/event.py
112
deluge/event.py
@ -17,9 +17,9 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with deluge. If not, write to:
|
||||
# The Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor
|
||||
# Boston, MA 02110-1301, USA.
|
||||
# The Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# In addition, as a special exception, the copyright holders give
|
||||
# permission to link the code of portions of this program with the OpenSSL
|
||||
@ -41,14 +41,29 @@ and subsequently emitted to the clients.
|
||||
|
||||
"""
|
||||
|
||||
known_events = {}
|
||||
|
||||
class DelugeEventMetaClass(type):
|
||||
"""
|
||||
This metaclass simply keeps a list of all events classes created.
|
||||
"""
|
||||
def __init__(cls, name, bases, dct):
|
||||
super(DelugeEventMetaClass, cls).__init__(name, bases, dct)
|
||||
if name != "DelugeEvent":
|
||||
known_events[name] = cls
|
||||
|
||||
class DelugeEvent(object):
|
||||
"""
|
||||
The base class for all events.
|
||||
|
||||
: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
|
||||
|
||||
def _get_name(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
@ -64,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):
|
||||
"""
|
||||
@ -76,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]
|
||||
|
||||
@ -86,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]
|
||||
|
||||
@ -96,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]
|
||||
|
||||
@ -113,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]
|
||||
|
||||
@ -125,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]
|
||||
|
||||
@ -137,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]
|
||||
|
||||
@ -147,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]
|
||||
|
||||
@ -186,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]
|
||||
@ -195,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,27 +36,40 @@ from twisted.web import client, http
|
||||
from twisted.web.error import PageRedirect
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.internet import reactor
|
||||
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):
|
||||
def __init__(self, url, filename, part_callback=None, headers=None,
|
||||
force_filename=False, allow_compression=True):
|
||||
"""
|
||||
:param url: the url to download from
|
||||
:type url: string
|
||||
:param filename: the filename to save the file as
|
||||
:type filename: string
|
||||
:param force_filename: forces use of the supplied filename, regardless of header content
|
||||
:type force_filename: bool
|
||||
:param part_callback: a function to be called when a part of data
|
||||
is received, it's signature should be: func(data, current_length, total_length)
|
||||
:type part_callback: function
|
||||
:param headers: any optional headers to send
|
||||
:type headers: dictionary
|
||||
"""
|
||||
self.__part_callback = part_callback
|
||||
self.part_callback = part_callback
|
||||
self.current_length = 0
|
||||
self.decoder = None
|
||||
self.value = filename
|
||||
client.HTTPDownloader.__init__(self, url, filename, headers=headers)
|
||||
self.force_filename = force_filename
|
||||
self.allow_compression = allow_compression
|
||||
agent = "Deluge/%s (http://deluge-torrent.org)" % get_version()
|
||||
client.HTTPDownloader.__init__(self, url, filename, headers=headers, agent=agent)
|
||||
|
||||
def gotStatus(self, version, status, message):
|
||||
self.code = int(status)
|
||||
@ -68,7 +81,30 @@ class HTTPDownloader(client.HTTPDownloader):
|
||||
self.total_length = int(headers["content-length"][0])
|
||||
else:
|
||||
self.total_length = 0
|
||||
elif self.code in (http.TEMPORARY_REDIRECT, http.MOVED_PERMANENTLY):
|
||||
|
||||
if self.allow_compression and "content-encoding" in headers and \
|
||||
headers["content-encoding"][0] in ("gzip", "x-gzip", "deflate"):
|
||||
# Adding 32 to the wbits enables gzip & zlib decoding (with automatic header detection)
|
||||
# Adding 16 just enables gzip decoding (no zlib)
|
||||
self.decoder = zlib.decompressobj(zlib.MAX_WBITS + 32)
|
||||
|
||||
if "content-disposition" in headers and not self.force_filename:
|
||||
new_file_name = str(headers["content-disposition"][0]).split(";")[1].split("=")[1]
|
||||
new_file_name = sanitise_filename(new_file_name)
|
||||
new_file_name = os.path.join(os.path.split(self.fileName)[0], new_file_name)
|
||||
|
||||
count = 1
|
||||
fileroot = os.path.splitext(new_file_name)[0]
|
||||
fileext = os.path.splitext(new_file_name)[1]
|
||||
while os.path.isfile(new_file_name):
|
||||
# Increment filename if already exists
|
||||
new_file_name = "%s-%s%s" % (fileroot, count, fileext)
|
||||
count += 1
|
||||
|
||||
self.fileName = new_file_name
|
||||
self.value = new_file_name
|
||||
|
||||
elif self.code in (http.MOVED_PERMANENTLY, http.FOUND, http.SEE_OTHER, http.TEMPORARY_REDIRECT):
|
||||
location = headers["location"][0]
|
||||
error = PageRedirect(self.code, location=location)
|
||||
self.noPage(Failure(error))
|
||||
@ -78,12 +114,51 @@ class HTTPDownloader(client.HTTPDownloader):
|
||||
def pagePart(self, data):
|
||||
if self.code == http.OK:
|
||||
self.current_length += len(data)
|
||||
if self.__part_callback:
|
||||
self.__part_callback(data, self.current_length, self.total_length)
|
||||
if self.decoder:
|
||||
data = self.decoder.decompress(data)
|
||||
if self.part_callback:
|
||||
self.part_callback(data, self.current_length, self.total_length)
|
||||
|
||||
return client.HTTPDownloader.pagePart(self, data)
|
||||
|
||||
def download_file(url, filename, callback=None, headers=None):
|
||||
def pageEnd(self):
|
||||
if self.decoder:
|
||||
data = self.decoder.flush()
|
||||
self.current_length -= len(data)
|
||||
self.decoder = None
|
||||
self.pagePart(data)
|
||||
|
||||
return client.HTTPDownloader.pageEnd(self)
|
||||
|
||||
def sanitise_filename(filename):
|
||||
"""
|
||||
Sanitises a filename to use as a download destination file.
|
||||
Logs any filenames that could be considered malicious.
|
||||
|
||||
:param filename: the filename to sanitise
|
||||
:type filename: string
|
||||
:returns: the sanitised filename
|
||||
:rtype: string
|
||||
"""
|
||||
|
||||
# Remove any quotes
|
||||
filename = filename.strip("'\"")
|
||||
|
||||
if os.path.basename(filename) != filename:
|
||||
# Dodgy server, log it
|
||||
log.warning("Potentially malicious server: trying to write to file '%s'" % filename)
|
||||
# Only use the basename
|
||||
filename = os.path.basename(filename)
|
||||
|
||||
filename = filename.strip()
|
||||
if filename.startswith(".") or ";" in filename or "|" in filename:
|
||||
# Dodgy server, log it
|
||||
log.warning("Potentially malicious server: trying to write to file '%s'" % filename)
|
||||
|
||||
return filename
|
||||
|
||||
def download_file(url, filename, callback=None, headers=None,
|
||||
force_filename=False, allow_compression=True):
|
||||
"""
|
||||
Downloads a file from a specific URL and returns a Deferred. You can also
|
||||
specify a callback function to be called as parts are received.
|
||||
@ -97,6 +172,11 @@ def download_file(url, filename, callback=None, headers=None):
|
||||
:type callback: function
|
||||
:param headers: any optional headers to send
|
||||
:type headers: dictionary
|
||||
:param force_filename: force us to use the filename specified rather than
|
||||
one the server may suggest
|
||||
:type force_filename: boolean
|
||||
:param allow_compression: allows gzip & deflate decoding
|
||||
:type allow_compression: boolean
|
||||
|
||||
:returns: the filename of the downloaded file
|
||||
:rtype: Deferred
|
||||
@ -111,8 +191,13 @@ def download_file(url, filename, callback=None, headers=None):
|
||||
for key, value in headers.items():
|
||||
headers[str(key)] = str(value)
|
||||
|
||||
if allow_compression:
|
||||
if not headers:
|
||||
headers = {}
|
||||
headers["accept-encoding"] = "deflate, gzip, x-gzip"
|
||||
|
||||
scheme, host, port, path = client._parse(url)
|
||||
factory = HTTPDownloader(url, filename, callback, headers)
|
||||
factory = HTTPDownloader(url, filename, callback, headers, force_filename, allow_compression)
|
||||
if scheme == "https":
|
||||
from twisted.internet import ssl
|
||||
reactor.connectSSL(host, port, factory, ssl.ClientContextFactory())
|
||||
|
@ -1,139 +0,0 @@
|
||||
deluge/plugins/label/label/data/label_pref.glade
|
||||
deluge/plugins/label/label/data/label_options.glade
|
||||
deluge/plugins/blocklist/blocklist/data/blocklist_pref.glade
|
||||
deluge/plugins/stats/stats/data/config.glade
|
||||
deluge/plugins/stats/stats/data/tabs.glade
|
||||
deluge/ui/gtkui/glade/add_torrent_dialog.glade
|
||||
deluge/ui/gtkui/glade/filtertree_menu.glade
|
||||
deluge/ui/gtkui/glade/torrent_menu.glade
|
||||
deluge/ui/gtkui/glade/remove_torrent_dialog.glade
|
||||
deluge/ui/gtkui/glade/preferences_dialog.glade
|
||||
deluge/ui/gtkui/glade/edit_trackers.glade
|
||||
deluge/ui/gtkui/glade/queuedtorrents.glade
|
||||
deluge/ui/gtkui/glade/move_storage_dialog.glade
|
||||
deluge/ui/gtkui/glade/connection_manager.glade
|
||||
deluge/ui/gtkui/glade/create_torrent_dialog.glade
|
||||
deluge/ui/gtkui/glade/dgtkpopups.glade
|
||||
deluge/ui/gtkui/glade/tray_menu.glade
|
||||
deluge/ui/gtkui/glade/main_window.glade
|
||||
deluge/bencode.py
|
||||
docs/source/conf.py
|
||||
deluge/core/autoadd.py
|
||||
deluge/core/preferencesmanager.py
|
||||
deluge/core/filtermanager.py
|
||||
deluge/core/torrentmanager.py
|
||||
deluge/core/daemon.py
|
||||
deluge/core/torrent.py
|
||||
deluge/core/pluginmanager.py
|
||||
deluge/core/oldstateupgrader.py
|
||||
deluge/core/__init__.py
|
||||
deluge/core/core.py
|
||||
deluge/core/alertmanager.py
|
||||
deluge/core/rpcserver.py
|
||||
deluge/config.py
|
||||
deluge/countries.py
|
||||
deluge/metafile.py
|
||||
deluge/pluginmanagerbase.py
|
||||
deluge/plugins/label/label/webui.py
|
||||
deluge/plugins/label/label/test.py
|
||||
deluge/plugins/label/label/gtkui/label_config.py
|
||||
deluge/plugins/label/label/gtkui/sidebar_menu.py
|
||||
deluge/plugins/label/label/gtkui/submenu.py
|
||||
deluge/plugins/label/label/gtkui/__init__.py
|
||||
deluge/plugins/label/label/__init__.py
|
||||
deluge/plugins/label/label/core.py
|
||||
deluge/plugins/label/setup.py
|
||||
deluge/plugins/pluginbase.py
|
||||
deluge/plugins/init.py
|
||||
deluge/plugins/blocklist/setup.py
|
||||
deluge/plugins/blocklist/blocklist/webui.py
|
||||
deluge/plugins/blocklist/blocklist/gtkui.py
|
||||
deluge/plugins/blocklist/blocklist/text.py
|
||||
deluge/plugins/blocklist/blocklist/peerguardian.py
|
||||
deluge/plugins/blocklist/blocklist/__init__.py
|
||||
deluge/plugins/blocklist/blocklist/core.py
|
||||
deluge/plugins/__init__.py
|
||||
deluge/plugins/webuipluginbase.py
|
||||
deluge/plugins/stats/setup.py
|
||||
deluge/plugins/stats/stats/webui.py
|
||||
deluge/plugins/stats/stats/gtkui.py
|
||||
deluge/plugins/stats/stats/test.py
|
||||
deluge/plugins/stats/stats/graph.py
|
||||
deluge/plugins/stats/stats/test_total.py
|
||||
deluge/plugins/stats/stats/__init__.py
|
||||
deluge/plugins/stats/stats/core.py
|
||||
deluge/configmanager.py
|
||||
deluge/ui/tracker_icons.py
|
||||
deluge/ui/client.py
|
||||
deluge/ui/ui.py
|
||||
deluge/ui/coreconfig.py
|
||||
deluge/ui/console/colors.py
|
||||
deluge/ui/console/commands/resume.py
|
||||
deluge/ui/console/commands/config.py
|
||||
deluge/ui/console/commands/halt.py
|
||||
deluge/ui/console/commands/debug.py
|
||||
deluge/ui/console/commands/__init__.py
|
||||
deluge/ui/console/commands/quit.py
|
||||
deluge/ui/console/commands/connect.py
|
||||
deluge/ui/console/commands/pause.py
|
||||
deluge/ui/console/commands/add.py
|
||||
deluge/ui/console/commands/rm.py
|
||||
deluge/ui/console/commands/info.py
|
||||
deluge/ui/console/commands/help.py
|
||||
deluge/ui/console/main.py
|
||||
deluge/ui/console/__init__.py
|
||||
deluge/ui/gtkui/listview.py
|
||||
deluge/ui/gtkui/options_tab.py
|
||||
deluge/ui/gtkui/statusbar.py
|
||||
deluge/ui/gtkui/status_tab.py
|
||||
deluge/ui/gtkui/addtorrentdialog.py
|
||||
deluge/ui/gtkui/sidebar.py
|
||||
deluge/ui/gtkui/gtkui.py
|
||||
deluge/ui/gtkui/aboutdialog.py
|
||||
deluge/ui/gtkui/systemtray.py
|
||||
deluge/ui/gtkui/files_tab.py
|
||||
deluge/ui/gtkui/menubar.py
|
||||
deluge/ui/gtkui/peers_tab.py
|
||||
deluge/ui/gtkui/notification.py
|
||||
deluge/ui/gtkui/toolbar.py
|
||||
deluge/ui/gtkui/ipcinterface.py
|
||||
deluge/ui/gtkui/filtertreeview.py
|
||||
deluge/ui/gtkui/queuedtorrents.py
|
||||
deluge/ui/gtkui/pluginmanager.py
|
||||
deluge/ui/gtkui/mainwindow.py
|
||||
deluge/ui/gtkui/removetorrentdialog.py
|
||||
deluge/ui/gtkui/common.py
|
||||
deluge/ui/gtkui/torrentdetails.py
|
||||
deluge/ui/gtkui/__init__.py
|
||||
deluge/ui/gtkui/edittrackersdialog.py
|
||||
deluge/ui/gtkui/preferences.py
|
||||
deluge/ui/gtkui/torrentview.py
|
||||
deluge/ui/gtkui/new_release_dialog.py
|
||||
deluge/ui/gtkui/connectionmanager.py
|
||||
deluge/ui/gtkui/createtorrentdialog.py
|
||||
deluge/ui/gtkui/details_tab.py
|
||||
deluge/ui/common.py
|
||||
deluge/ui/__init__.py
|
||||
deluge/ui/web/auth.py
|
||||
deluge/ui/web/common.py
|
||||
deluge/ui/web/gen_gettext.py
|
||||
deluge/ui/web/__init__.py
|
||||
deluge/ui/web/json_api.py
|
||||
deluge/ui/web/pluginmanager.py
|
||||
deluge/ui/web/server.py
|
||||
deluge/ui/web/web.py
|
||||
deluge/common.py
|
||||
deluge/component.py
|
||||
deluge/main.py
|
||||
deluge/error.py
|
||||
deluge/__init__.py
|
||||
deluge/tests/test_signalreceiver.py
|
||||
deluge/tests/test_filters.py
|
||||
deluge/tests/test_plugin_metadata.py
|
||||
deluge/tests/test_tracker_icons.py
|
||||
deluge/tests/test_stats.py
|
||||
deluge/tests/test_client.py
|
||||
deluge/log.py
|
||||
deluge/scripts/deluge_remote.py
|
||||
deluge/scripts/wiki_docgen.py
|
||||
deluge/scripts/create_plugin.py
|
6693
deluge/i18n/ar.po
6693
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
6771
deluge/i18n/bg.po
6771
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
6465
deluge/i18n/ca.po
6465
deluge/i18n/ca.po
File diff suppressed because it is too large
Load Diff
6704
deluge/i18n/cs.po
6704
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
7123
deluge/i18n/da.po
7123
deluge/i18n/da.po
File diff suppressed because it is too large
Load Diff
7319
deluge/i18n/de.po
7319
deluge/i18n/de.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
6710
deluge/i18n/el.po
6710
deluge/i18n/el.po
File diff suppressed because it is too large
Load Diff
7044
deluge/i18n/en_AU.po
7044
deluge/i18n/en_AU.po
File diff suppressed because it is too large
Load Diff
7465
deluge/i18n/en_CA.po
7465
deluge/i18n/en_CA.po
File diff suppressed because it is too large
Load Diff
7461
deluge/i18n/en_GB.po
7461
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
7111
deluge/i18n/es.po
7111
deluge/i18n/es.po
File diff suppressed because it is too large
Load Diff
6505
deluge/i18n/et.po
6505
deluge/i18n/et.po
File diff suppressed because it is too large
Load Diff
6045
deluge/i18n/eu.po
6045
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
7128
deluge/i18n/fi.po
7128
deluge/i18n/fi.po
File diff suppressed because it is too large
Load Diff
7463
deluge/i18n/fr.po
7463
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
6163
deluge/i18n/gl.po
6163
deluge/i18n/gl.po
File diff suppressed because it is too large
Load Diff
6998
deluge/i18n/he.po
6998
deluge/i18n/he.po
File diff suppressed because it is too large
Load Diff
6489
deluge/i18n/hi.po
6489
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
7142
deluge/i18n/hu.po
7142
deluge/i18n/hu.po
File diff suppressed because it is too large
Load Diff
6206
deluge/i18n/id.po
6206
deluge/i18n/id.po
File diff suppressed because it is too large
Load Diff
6539
deluge/i18n/is.po
6539
deluge/i18n/is.po
File diff suppressed because it is too large
Load Diff
7544
deluge/i18n/it.po
7544
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
7312
deluge/i18n/ja.po
7312
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
6297
deluge/i18n/kk.po
6297
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
8010
deluge/i18n/ko.po
8010
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
7195
deluge/i18n/lt.po
7195
deluge/i18n/lt.po
File diff suppressed because it is too large
Load Diff
6580
deluge/i18n/lv.po
6580
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
6960
deluge/i18n/ms.po
6960
deluge/i18n/ms.po
File diff suppressed because it is too large
Load Diff
7113
deluge/i18n/nb.po
7113
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
7186
deluge/i18n/nl.po
7186
deluge/i18n/nl.po
File diff suppressed because it is too large
Load Diff
7008
deluge/i18n/pl.po
7008
deluge/i18n/pl.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