Compare commits
217 Commits
deluge-2.0
...
deluge-1.2
Author | SHA1 | Date | |
---|---|---|---|
4748c96d26 | |||
79c78fa7ef | |||
89c9228cec | |||
0df79b03c5 | |||
c54a50af34 | |||
e81d2ea0bd | |||
5e19f91613 | |||
5a35d60178 | |||
34b2ada0db | |||
547cfa6339 | |||
3870c25c9d | |||
4cde35a067 | |||
d5a11d0e95 | |||
1f8919b47f | |||
59f05a890f | |||
afbeaa6b7d | |||
f5409e0d0c | |||
ac857eaef7 | |||
886f2d8682 | |||
fa79c37aa2 | |||
6a8e023ffd | |||
6b396f1f77 | |||
b2990e89cb | |||
7ebe07db15 | |||
14eb3b8b33 | |||
cffc25aac8 | |||
ed2568c8b0 | |||
700fe2ec17 | |||
571b8a84af | |||
3d821fb80b | |||
00c4b53b67 | |||
9a9ae71564 | |||
c9c57e0e32 | |||
c70a9875c5 | |||
c932de8470 | |||
0663da4424 | |||
8c265bb308 | |||
fe6c89a2e1 | |||
3c5d5aa16e | |||
389e9cdbb0 | |||
193f1b85ca | |||
4daa2a098b | |||
52817498cf | |||
cc3f3495b6 | |||
4bd00aa331 | |||
e27c38ca67 | |||
eeac8efcf2 | |||
f59901c1be | |||
ee76075fa4 | |||
3f69b05ded | |||
ecca68772e | |||
9878fced37 | |||
c312cf4b91 | |||
de262bf7db | |||
8edd91a3d5 | |||
f6fdae727d | |||
139d6538a2 | |||
b88319b4be | |||
dd129f8544 | |||
af18bdb72a | |||
85b3546c47 | |||
07cd4ba83d | |||
361f2b2390 | |||
97a69b8c0c | |||
29f2ed84dd | |||
9e9fbd6242 | |||
2840d43ccb | |||
14454791c4 | |||
fcbaa1f4f2 | |||
c716f55dda | |||
a1e950d6e5 | |||
e646a5ef87 | |||
3c703d2ef7 | |||
429f7064fa | |||
47b5274f34 | |||
a6c6d5b2b4 | |||
3d3edccdac | |||
36b2a82561 | |||
f5096c6e7c | |||
76bc32d72a | |||
581517c8e9 | |||
b41a025217 | |||
7a91b96183 | |||
a317fdec48 | |||
20b6791670 | |||
411ee123f1 | |||
2bc13a3fb7 | |||
dda893e2b7 | |||
00de8311fd | |||
6d7572c444 | |||
3e1a6a1fb8 | |||
dbbc1124f4 | |||
13d949381a | |||
bfea47c8bd | |||
f858e271ad | |||
708f052153 | |||
8d6bc283d0 | |||
313c73c40c | |||
012dd1919c | |||
d7e83d922c | |||
860ceccd52 | |||
907f618607 | |||
abcb76e959 | |||
825edb1fd8 | |||
4cdf919076 | |||
90a143a4ef | |||
04f8f80b9b | |||
266693a6f9 | |||
2b9e1e5ff4 | |||
68fe9512df | |||
b10ea808fd | |||
234863b664 | |||
940bc4a3dc | |||
aa5f69b203 | |||
5f4b39aeb8 | |||
0e07cdf8b1 | |||
64eb296c9d | |||
fa40f38c1b | |||
1b46d99620 | |||
441c20491c | |||
8c7bbbf398 | |||
267905295c | |||
fb4495fc33 | |||
447da3b1b4 | |||
9af41fd457 | |||
7e19a11521 | |||
4da33c72cc | |||
d411f90a67 | |||
3179ff2060 | |||
b6dc72218d | |||
e884d2cbfa | |||
fc3ae8229f | |||
48e3df5ff3 | |||
464e11abe6 | |||
0fbaecbc00 | |||
4c86ed7535 | |||
6f333cefc9 | |||
750f8fb794 | |||
be529c1518 | |||
e32bd8f4d8 | |||
87723a3354 | |||
0cb52cca23 | |||
09ae4fbb72 | |||
6e063e7c85 | |||
41db357084 | |||
3611584b0b | |||
6e695d8410 | |||
60fdcb3bd8 | |||
d110b23a9f | |||
2457fd0aea | |||
92afb8ab80 | |||
eba82457bd | |||
099415776d | |||
145f402aa2 | |||
d44efe8a66 | |||
1a9f715353 | |||
2308961a04 | |||
0cdd779396 | |||
db54dada9d | |||
7c5582a21e | |||
7055a96930 | |||
e7a45f6dd1 | |||
ea4dcfbb5e | |||
5c506121a4 | |||
cc7aaa1ae0 | |||
cca2a4ab60 | |||
197027510e | |||
b12b5a8403 | |||
0b5a0ac9a0 | |||
65e50d1992 | |||
ecd75ba424 | |||
694051e876 | |||
350c523e7d | |||
dbece6a6f6 | |||
5c26a4b1cf | |||
475a880dd9 | |||
b0702e6a6d | |||
e506147289 | |||
3c4144e01a | |||
d4f2c2eda8 | |||
4a996a1b41 | |||
e39d1ab679 | |||
c49ab0c6b1 | |||
9c412e8aff | |||
83f42f5bf8 | |||
039a41def3 | |||
b94060c60d | |||
33301b3433 | |||
b9ae412d06 | |||
f55bc6c9d8 | |||
511bfd5d84 | |||
89f88f3beb | |||
b1b09fbe89 | |||
07b50730a2 | |||
5607e0f6fa | |||
9ab638377b | |||
4b79ae8280 | |||
f1a1fdb630 | |||
d1f52a7051 | |||
000297dc95 | |||
81eeb6edf7 | |||
7d67da4371 | |||
ef5739a6a8 | |||
4f968b9887 | |||
22447993fa | |||
58be1f08fc | |||
41d26bbbce | |||
f1383f9655 | |||
644f0ecb38 | |||
42a57b1f41 | |||
8f50687034 | |||
9f661caee8 | |||
044306cb3d | |||
ebd0290e44 | |||
4f1713734f | |||
f86e319d49 | |||
95bb8fb4c3 |
25
.gitattributes
vendored
@ -1,25 +0,0 @@
|
||||
/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
@ -1,14 +0,0 @@
|
||||
*~
|
||||
build
|
||||
dist
|
||||
*egg-info
|
||||
*.egg
|
||||
*.log
|
||||
*.pyc
|
||||
*.tar.*
|
||||
_trial_temp
|
||||
deluge/i18n/*/
|
||||
*.desktop
|
||||
.build_data*
|
||||
osx/app
|
||||
RELEASE-VERSION
|
797
AUTHORS
@ -1,797 +0,0 @@
|
||||
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
|
||||
Γιάννης Κατσαμπίρης
|
||||
Артём Попов
|
||||
Миша
|
||||
Шаймарданов Максим
|
||||
蔡查理
|
717
ChangeLog
@ -1,427 +1,190 @@
|
||||
=== 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.
|
||||
|
||||
=== Deluge 1.2.0 - "Bursting like an infected kidney" (09 January 2010) ===
|
||||
==== Core ====
|
||||
* Make the distinction between adding to the session new unmanaged torrents
|
||||
and torrents loaded from state. This will break backwards compatability.
|
||||
* Pass a copy of an event instead of passing the event arguments to the
|
||||
event handlers. This will break backwards compatability.
|
||||
* Allow changing ownership of torrents.
|
||||
* File modifications on the auth file are now detected and when they happen,
|
||||
the file is reloaded. Upon finding an old auth file with an old format, an
|
||||
upgrade to the new format is made, file saved, and reloaded.
|
||||
* Authentication no longer requires a username/password. If one or both of
|
||||
these is missing, an authentication error will be sent to the client
|
||||
which sould then ask the username/password to the user.
|
||||
* Implemented sequential downloads.
|
||||
* #378: Provide information about a torrent's pieces states
|
||||
* Fix file renaming
|
||||
* Fix tracker host filtering (Closes #1106)
|
||||
* Fix exceptions when gettext/locale cannot be initialized properly
|
||||
* Change share ratio calculation to use the total done instead of the all time
|
||||
downloaded value. This change will make the share ratio calculation not
|
||||
use data downloaded in failed hash checks.
|
||||
* Fix torrent info name not being utf-8 decoded when root file/folder name
|
||||
is blank
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix uncaught exception when closing deluge in classic mode
|
||||
* Allow changing ownership of torrents
|
||||
* Host entries in the Connection Manager UI are now editable. They're
|
||||
now also migrated from the old format were automatic localhost logins were
|
||||
possible, which no longer is, this fixes #1814.
|
||||
* Implemented sequential downloads UI handling.
|
||||
* #378: Allow showing a pieces bar instead of a regular progress bar in a
|
||||
torrent's status tab.
|
||||
* #2093: Make torrent opening compatible with all unicode paths.
|
||||
|
||||
==== Blocklist Plugin ====
|
||||
* #1382: Implemented whitelist support to both core and GTK UI.
|
||||
* Implemented ip filter cleaning before each update. Restarting the deluge
|
||||
daemon is no longer needed.
|
||||
* If "check_after_days" is 0(zero), the timer is not started anymore. It
|
||||
would keep updating one call after the other. If the value changed, the
|
||||
timer is now stopped and restarted using the new value.
|
||||
|
||||
=== Deluge 1.3.7 (In Development) ===
|
||||
==== GtkUI ====
|
||||
* Fix issue with Plugins that add Tab to torrentdetails
|
||||
* Fix the scalable icon install directory
|
||||
|
||||
==== Extractor ====
|
||||
* #2290: Fix dotted filenames being rejected
|
||||
|
||||
=== Deluge 1.3.6 (25 Feburary 2013) ===
|
||||
==== Core ====
|
||||
* Catch & log KeyError when removing a torrent from the queued torrents set
|
||||
* Fix moving/renaming torrents issues when using libtorrent 0.16
|
||||
* Make sure queue order is preserved when restarting
|
||||
* #2160: Disable use of python bindings for libtorrent extensions and replace with session flag
|
||||
* #2163: Fix unable add torrent file with empty (0:) encoding tag
|
||||
* #2201: Fix error in authmanager if auth file has extra newlines
|
||||
* #2109: Fix the Proxy settings not being cleared by setting None
|
||||
* #2110: Fix accepting magnet uris with xt param anywhere within them
|
||||
* #2204: Fix daemon shutdown hang with large numbers of torrents
|
||||
|
||||
==== Client ====
|
||||
* Fix keyerrors after removing torrents from UIs
|
||||
|
||||
==== GtkUI ====
|
||||
* Add move completed option to add torrent dialog
|
||||
* Prevent jitter in torrent view
|
||||
* Fix torrent creation with non-ascii characters
|
||||
* Fix #2100 : Add option not to bring main window to front when adding torrents through ipcinterface
|
||||
* Add Quit Dialog when toggling classic mode in preferences and only show connection manager when not in classic mode.
|
||||
* #2169: Fix 'Download Location' in the Add Torrent Dialog not set correctly when folder typed into Other->Location field
|
||||
* #2171: Fix the Add Peer dialog not responding if empty or invalid values entered
|
||||
* #2104: Fix no title set for the appindicator
|
||||
* #2086: Fix submenus and icons for appindicator
|
||||
* #2146: Fix missing translations in View|Tabs submenu
|
||||
* Fix torrent names on libtorrent 0.16 on windows
|
||||
* #2147: Fix missing translations for plugin preferences page
|
||||
* #1474: Fix the on_show_prefs hook not being executed immediatly after enabling a plugin
|
||||
* #1946: Fix ReactorNotRestartable error when set as startup application
|
||||
* #2130: Fix same name can be given to different files in Add Torrent dialog
|
||||
* #2129: Fix empty filename able to be set in AddTorrent dialog
|
||||
* #2228: Fix Apply-To-All in AddTorrent Dialog copying file renames to other torrents
|
||||
* #2260: Fix the Add Torrent dialog also bringing the main window to active workspace
|
||||
* Fix showing exception error to user in Classic Mode with no libtorrent installed
|
||||
* Fix #1104, #735 use path.utf-8 if available
|
||||
* Fix #1114 test active port not working in classic mode
|
||||
|
||||
==== 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
|
||||
* Fix #1115 not showing usage for the 'debug' command
|
||||
* Fix #1116 not being able to use command aliases when not connected to a daemon
|
||||
* Fix #1117 can't use the '3' key
|
||||
|
||||
==== WebUI ====
|
||||
* Add move completed option to add torrent dialog
|
||||
* #2112: Fix world readable tmp directory in json_api
|
||||
* #2069: Fix login window layout problem when using larger than default font size
|
||||
* #1890: Fix columns in files and peers view could use some spacing
|
||||
* #2103: Fix sorting by name is case-sensitive [sedulous]
|
||||
* #2120: Fix manually entered values not being saved in spinners
|
||||
* #2212: Fix unable to scroll in proxy preferences page
|
||||
* Fix autoconnecting to the default host
|
||||
* #2046: Fix plugins not enabling properly until after refreshing page
|
||||
* #2125: Fix plugin methods not being available when enabled until restart
|
||||
* #2085: Fix not showing torrents in sidebar for categories other than 'All' in classic mode
|
||||
* #2232: Fix flag icon path in Peers Tab missing deluge.config.base
|
||||
* Fix submenus closing upon mouse click
|
||||
* Add failed login log message, including IP address, to enable use with fail2ban
|
||||
* #2261: Fix Proxy settings not being saved in preferences
|
||||
|
||||
==== Windows OS ====
|
||||
* Hide the cmd windows when running deluged.exe or deluge-web.exe
|
||||
* Add deluged-debug.exe and deluge-web-debug.exe that still show the cmd window
|
||||
* Add gtk locale files to fix untranslated text
|
||||
* Fix the Open Folder option not working with non-ascii paths
|
||||
* Fix the daemon starting with config dir containing spaces
|
||||
* Fix Windows tray submenu items requiring right-click instead of left-click
|
||||
* Fix issue with adding some torrents with illegal characters via url in gtk client
|
||||
* #2240: Fix freespace issue with large capacity drives
|
||||
|
||||
==== OS X ====
|
||||
* Fix Open File/Folder option
|
||||
* Add OS X Menu for GTK Quartz
|
||||
|
||||
==== Execute ====
|
||||
* Fix execute plugin not working with unicode torrent names
|
||||
|
||||
==== Extractor ====
|
||||
* Add Windows support, using 7-zip
|
||||
* Added support for more extensions
|
||||
* Disabled extracting 'Move Completed' torrents due to race condition
|
||||
|
||||
=== Deluge 1.3.5 (09 April 2012) ===
|
||||
==== GtkUI ====
|
||||
* Modified fix for #1957, keyerror with non-acsii columns
|
||||
* Fix translation of items in Sidebar and Torrent Menu
|
||||
* #2052: Fix translation of Progress bar text
|
||||
* #2071: Fix KeyError in gtkui when file priority set to value '3'
|
||||
* #2064: Fix files treeview height in Create Dialog
|
||||
* Fix missing semi-colon in deluge.desktop
|
||||
* Disable setting file priorities for seeding torrents
|
||||
* Bring MainWindow to front when opening another instance
|
||||
|
||||
==== WebUI ====
|
||||
* #2050: Fix 'Up Speed' column not sorting
|
||||
* Hide unused Infohash button in WebUI
|
||||
|
||||
==== Label ====
|
||||
* Disable unusable items for 'All' in sidebar menu
|
||||
* Fix items for translation
|
||||
|
||||
==== Console ====
|
||||
* Fix prefixed space for tab completing commands
|
||||
* Fix missing trailing space for command options with tab complete
|
||||
==== Windows ====
|
||||
* Fix displaying folders in the add torrent dialog
|
||||
* Fix displaying the new release dialog
|
||||
|
||||
==== 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)
|
||||
* Fix blocklist status icon not opening the blocklist preference
|
||||
page in certain locales
|
||||
* Fix blocklist not recognising comments that begin with whitespace
|
||||
* Minor speedup in parsing blocklists
|
||||
* Blocklist now attempts to download the URL multiple times before giving
|
||||
up
|
||||
* Fix blocklist not being able to open zipped blocklists with python 2.5
|
||||
|
||||
==== Web ====
|
||||
* 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
|
||||
* Put the default password in the manpage.
|
||||
|
||||
=== Deluge 1.2.0 - "Bursting like an infected kidney" (10 January 2010) ===
|
||||
=== Deluge 1.2.0_rc5 (17 December 2009) ===
|
||||
==== Web ====
|
||||
* Swap order of buttons in Remove window (Closes #1083)
|
||||
* Change the compressed js script to deluge-all.js to avoid naming
|
||||
conflicts on case-sensitive filesystems.
|
||||
* Apply patch from adios fixing the cookie date
|
||||
* Add tooltips to the statusbar items
|
||||
* Add disk usage to the statusbar
|
||||
* Add a ToggleField widget and use this on the Downloads preferences
|
||||
page allowing the movecom/copytorrent/autoadd boxes to be enabled.
|
||||
* Fix enabling plugins.
|
||||
* Implement installing plugins.
|
||||
* Update some icons
|
||||
* Fixed #1075 (changing priority on a whole folder doesn't work)
|
||||
|
||||
==== GtkUI ====
|
||||
* Attempt to register as the default magnet uri handler in GNOME on startup
|
||||
* Properly show 100.00% and reduce number of progress bar updates during a
|
||||
torrent creation
|
||||
* Fix crash in Windows when creating a torrent
|
||||
* Add button to Other preferences to associate magnet links with Deluge
|
||||
* Fix uploading plugins when the daemon is not localhost
|
||||
* Fix #692 no longer require tray password when quitting from the tray icon
|
||||
while the window is visible.
|
||||
* Fix #782 do not ask for tray password when window is not minimized to tray
|
||||
* Fix #1036 autoconnecting to localhost daemon on start-up
|
||||
* Fix issue where hosts will show up erroneously as Offline
|
||||
* Add #891 remove torrents by pressing the Delete key
|
||||
* Fix issue where stoping a daemon that you aren't connected to causes the
|
||||
gtkui to shutdown the currently connected daemon.
|
||||
* Fix #594 tray password dialog freeze in Windows
|
||||
* Made the password dialog prettier
|
||||
* Fix #1086 deprecated gtk.Tooltips usage
|
||||
* Fix #768 save tracker list for create torrent dialog
|
||||
* Fix #1095 incorrect piece size used when using some non-English languages
|
||||
|
||||
==== Console ====
|
||||
* Fix using the console in Windows, but only in command-line mode
|
||||
* Fix #823 setting config values to -1.0
|
||||
|
||||
==== Label ====
|
||||
* Fix #1085 only use ints for specific options to prevent unhandled exception
|
||||
|
||||
==== Execute ====
|
||||
* Use the move_completed path if it is different to the save path
|
||||
in the completed event.
|
||||
|
||||
==== Core ====
|
||||
* Fix the upload_plugin rpc method (was still using XML-RPC stuff)
|
||||
* Fix possible exception when upgrading from a 0.5 state file
|
||||
* Use metavar to modify the help output by optparse.
|
||||
* Partial fix for #1103 if the per-torrent option for stopping at a ratio has
|
||||
been unchecked, then do not stop it at the global setting.
|
||||
|
||||
==== Blocklist ====
|
||||
* Fix blocklist not working for all locales
|
||||
* Fix blocklist checking for updates when it shouldn't
|
||||
|
||||
=== Deluge 1.2.0_rc4 (24 November 2009) ===
|
||||
==== Core ====
|
||||
* Fix deleting old .fastresume files with fresh configs
|
||||
* Fix files list when using magnet uris
|
||||
* Fix loading the saved metadata when loading state with magnet uris
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix showing the 'Other' speed dialogs in Windows
|
||||
* Fix adding torrents from the Queued Torrents dialog
|
||||
* Fix disabling/enabling plugins after switching daemons
|
||||
* Reduce height of Add Torrent Dialog by ~80 pixels
|
||||
* Fix #1071 issue where Deluge will fail to start if there is a stale ipc lockfile
|
||||
* Fix autoconnecting to the next host in the list if the selected one isn't available
|
||||
* Fix endless loop when trying to autoconnect to an offline daemon
|
||||
* Fix exception on startup when the system tray icon is not enabled
|
||||
* Fix issue where some torrents with special characters could not be added
|
||||
* Fix issues adding magnet uris
|
||||
|
||||
==== Web ====
|
||||
* Fix installing the deluge-web manpage
|
||||
* Escape hyphens in the manpage
|
||||
|
||||
==== Console ====
|
||||
* Escape hyphens in the manpage
|
||||
* Make the delete key work
|
||||
* Allow ~ to be used in the path in the add command
|
||||
* Allow commands that are .pyc files to be used
|
||||
* Fix printing info, help, etc.. on the command line
|
||||
|
||||
==== Blocklist ====
|
||||
* Force blocklist to auto-detect format when a download / import is forced
|
||||
* Fix blocklist failing on certain PeerGuardian/SafePeer lists
|
||||
|
||||
=== Deluge 1.2.0_rc3 (01 November 2009) ===
|
||||
==== Core ====
|
||||
* Fix #1047 move completed does not work if saving to non default path
|
||||
* Fix renamed files not being utf-8 encoded
|
||||
* Fix torrent name being blank when renaming root folder to /
|
||||
* Do not include an 'announce-list' key in torrents when there is only one tracker
|
||||
|
||||
==== GtkUI ====
|
||||
* Replace & with & in the details tab to ensure there are no markup errors
|
||||
* Consider 0 unlimited when displaying limits in the statusbar
|
||||
* Fix adding torrents when not showing the add torrent dialog in Windows
|
||||
* Fix crash when removing multiple torrents
|
||||
|
||||
==== Web ====
|
||||
* Fix #1046 changing auto managed via the details tab
|
||||
* Fix setting torrent options when adding
|
||||
* Fix setting file priorities when adding
|
||||
* HTML escape the field values on the details tab
|
||||
* Fix #215, make infinite eta values display in the correct order
|
||||
* Fix displaying the protocol upload speed
|
||||
* Fix #990, showing 0 as a limit when it means unlimited in the statusbar
|
||||
|
||||
==== Console ====
|
||||
* Fix displaying non-ascii strings
|
||||
* Fix #1052 crash when issuing commands while not connected to a daemon
|
||||
* Fix crash when string length makes line longer than terminal width
|
||||
* Improve 'info' command draw speed
|
||||
|
||||
=== Deluge 1.2.0_rc2 (25 October 2009) ===
|
||||
==== GtkUI ====
|
||||
* Fix path errors when adding torrents externally in Windows
|
||||
* Fix localclient authentication by stripping the lines read from the auth file
|
||||
* Do not try to call doIteration() on the reactor if it has already stopped
|
||||
* Fix 'autostart localhost if needed' option
|
||||
* Fix starting plugins when the pluginmanager is started
|
||||
* Fix #799 translate connection status
|
||||
* Fix #215 ETA sort order
|
||||
|
||||
==== Core ====
|
||||
* Fix saving torrent state on fresh configs
|
||||
|
||||
==== Web ====
|
||||
* Fix changing of the allocation in the preferences.
|
||||
* Fix updating the Connection Manager when a host is added.
|
||||
* Add a `--fork` option to allow forking the webui to the background
|
||||
* Fix the statusbar menu limits
|
||||
* Fix setting the torrent options via the options tab
|
||||
* Fix the private flag in the options tab
|
||||
|
||||
==== Console ====
|
||||
* Fix exception when using the 'halt' command
|
||||
|
||||
==== Misc ====
|
||||
* Add man pages for deluge-console, deluge-gtk and deluge-web
|
||||
|
||||
==== Extractor ====
|
||||
* Fix issue where the plugin would not stop extracting files after being disabled
|
||||
* Add option to create torrent name sub-folders in extract folder
|
||||
|
||||
=== Deluge 1.2.0_rc1 (07 October 2009) ===
|
||||
==== Core ====
|
||||
* Implement new RPC protocol DelugeRPC replacing XMLRPC
|
||||
* Move to a twisted framework
|
||||
@ -459,6 +222,134 @@
|
||||
* Add new scripts for invoking UIs: deluge-gtk, deluge-web, deluge-console
|
||||
* Remove GeoIP database from the source tree
|
||||
|
||||
=== Deluge 1.1.9 - (15 June 2009) ===
|
||||
==== Core ====
|
||||
* Only move a torrent due to 'move on complete' when some data has been downloaded
|
||||
* Update libtorrent for CVE-2009-1760
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix #950 renaming a parent folder into multiple folders
|
||||
|
||||
==== WebUI ====
|
||||
* Fix remote torrent add
|
||||
|
||||
=== Deluge 1.1.8 - (21 May 2009) ===
|
||||
==== Core ====
|
||||
* Fix pause all/resume all
|
||||
* Torrent name is now changed when the root folder or file is renamed
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix high cpu usage when displaying speeds in titlebar
|
||||
* Fix showing non-utf8 encoded torrents in add torrent dialog -- this adds
|
||||
an additional dependency on chardet.
|
||||
* Fix exception when timing out trying to send notification email
|
||||
* Set some sane defaults for peers/file tabs column widths
|
||||
|
||||
==== WebUI ====
|
||||
* Fix starting when -l option is used
|
||||
|
||||
=== Deluge 1.1.7 - (25 April 2009) ===
|
||||
==== Core ====
|
||||
* Fix issue where cannot resume torrent after doing a 'Pause All'
|
||||
* Add workaround for 'address_v4 from unsigned long' bug experienced by users
|
||||
with 64-bit machines. This bug is fixed in libtorrent 0.14.3.
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix #883 segfault if locale is not using UTF-8 encoding
|
||||
* Fix for adding torrents with invalid filename encodings
|
||||
* Fix displaying IPv6 peers in the Peers tab
|
||||
* Fix starting the daemon in OS X
|
||||
* Fix loading improperly created torrents with mismatched encodings
|
||||
* Fix displaying improper progress when creating torrent
|
||||
|
||||
==== Windows ====
|
||||
* Fix freezing in create torrent dialog
|
||||
* Fix creating torrents in Windows
|
||||
* Fix free space check
|
||||
|
||||
=== Deluge 1.1.6 - (06 April 2009) ===
|
||||
==== Core ====
|
||||
* Fix udp trackers being classified as DHT source
|
||||
* Fix #855 force a resume on a torrent if a 'Force Recheck' is initiated
|
||||
* Fix #862 deluged crash when access http://localhost:58846
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix displaying torrents with non-utf8 encodings in add torrent dialog
|
||||
|
||||
==== WebUI ====
|
||||
* Fix #870 use proper config location for loading ssl cert
|
||||
|
||||
==== Misc ====
|
||||
* Add OpenSSL exception to license
|
||||
|
||||
=== Deluge 1.1.5 - (16 March 2009) ===
|
||||
==== Core ====
|
||||
* Fix config file saving when no current config file exists
|
||||
|
||||
==== GtkUI ====
|
||||
* Add 'Comments' field to the Details tab
|
||||
* Fix #841 maximum upload slots tooltip
|
||||
|
||||
=== Deluge 1.1.4 - (08 March 2009) ===
|
||||
==== Core ====
|
||||
* Fix displaying file errors when the torrent isn't paused
|
||||
* Fix issue where torrents being check would get removed due to "stop at ratio" rules
|
||||
* Fix #790 tracker hosts not correct for some .uk trackers
|
||||
* Make sure config files, resume data and state are fsync'd when saved. This should help prevent data losses on crashes/improper shutdowns.
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix hiding bottom pane when no tabs are enabled upon restart
|
||||
* Fix saving file priorities when switching torrents in the addtorrentdialog
|
||||
* Fix the allocate mode not being preserved when selecting different torrents in addtorrentdialog
|
||||
* Fix #655 issue where default torrent options wouldn't be set for new torrents added to the addtorrentdialog
|
||||
* Fix #817 email notifications fail to substitute format strings
|
||||
|
||||
==== Plugins ====
|
||||
* Label: Fix setting 'Move on completed' folder when connected to a remote daemon
|
||||
|
||||
=== Deluge 1.1.3 - (15 February 2009) ===
|
||||
==== Core ====
|
||||
* Fix issue where checking queue would stop
|
||||
* Fix announcing to SSL trackers
|
||||
|
||||
==== Misc ====
|
||||
* Fix issue when initializing gettext that would prevent deluge from starting
|
||||
* Fix logging exceptions when starting the daemon
|
||||
* Fix displaying errors when a torrent is Checking
|
||||
* Fix #790 tracker hosts not correct for some 3rd-level domain names
|
||||
|
||||
=== Deluge 1.1.2 - (31 January 2009) ===
|
||||
==== Core ====
|
||||
* Fix issue where torrents get stuck Checking
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix #761 use proper theme colours in sidebar
|
||||
* Fix saving files/peers tab state when no column is sorted
|
||||
|
||||
=== Deluge 1.1.1 - (24 January 2009) ===
|
||||
==== Core ====
|
||||
* Fix oldstateupgrader for those upgrading from 0.5.x
|
||||
* Fix setting Peer TOS byte
|
||||
* Fix setting outgoing ports
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix opening links from Help menu and others
|
||||
* Fix remembering sorted column in the torrent list
|
||||
* Fix saving Files tab and Peers tab state
|
||||
* Disable popup notification in preferences on Windows
|
||||
* Fix crashing in Add Torrent Dialog when removing torrents from the list
|
||||
* Do not allow duplicate torrents in the Add Torrent Dialog
|
||||
* Fix translating speed units in status tab when a per-torrent limit is set
|
||||
* Fix torrents not displaying properly after disconnecting and reconnecting to the daemon
|
||||
* Fix when sorting # column, downloads should be on top
|
||||
|
||||
==== Misc ====
|
||||
* Fix bdecoding some torrent files
|
||||
* Fix the -l, --logfile option
|
||||
* Fix #729 tracker icons not being saved in the correct location
|
||||
* Add support for more tracker icons
|
||||
* Fix being able to connect to a local daemon from another user account
|
||||
|
||||
=== Deluge 1.1.0 - "Time gas!" (10 January 2009) ===
|
||||
==== Core ====
|
||||
* Implement #79 ability to change outgoing port range
|
||||
|
22
DEPENDS
@ -1,30 +1,24 @@
|
||||
=== Core ===
|
||||
* python >= 2.6
|
||||
* python >= 2.5
|
||||
* 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.5
|
||||
|
||||
* libtorrent (rasterbar) >= 0.16.7
|
||||
|
||||
* If building libtorrent:
|
||||
* boost >= 1.40
|
||||
* openssl
|
||||
* zlib
|
||||
=== UIs ===
|
||||
* chardet
|
||||
|
||||
=== Gtk ===
|
||||
* pygtk >= 2.16
|
||||
* python-notify (libnotify python wrapper)
|
||||
* pygame
|
||||
* pygtk >= 2.12
|
||||
* librsvg
|
||||
* xdg-utils
|
||||
* python-notify (optional)
|
||||
* pygame (optional)
|
||||
|
||||
=== Web ===
|
||||
* mako
|
||||
|
23
MANIFEST.in
@ -1,23 +0,0 @@
|
||||
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
|
75
README
@ -1,3 +1,4 @@
|
||||
|
||||
==========================
|
||||
Deluge BitTorrent Client
|
||||
==========================
|
||||
@ -8,50 +9,86 @@ Authors:
|
||||
Andrew Resch
|
||||
Damien Churchill
|
||||
|
||||
For contributors and past developers see:
|
||||
AUTHORS
|
||||
For past developers and contributers see: http://dev.deluge-torrent.org/wiki/About
|
||||
|
||||
==========================
|
||||
Installation Instructions:
|
||||
License
|
||||
==========================
|
||||
|
||||
For detailed instructions see: http://dev.deluge-torrent.org/wiki/Installing/Source
|
||||
|
||||
Ensure build dependencies are installed, see DEPENDS for a full listing.
|
||||
|
||||
Build and install by running:
|
||||
|
||||
$ python setup.py build
|
||||
$ sudo python setup.py install
|
||||
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:
|
||||
==========================
|
||||
|
||||
Forum: http://forum.deluge-torrent.org
|
||||
IRC Channel: #deluge on irc.freenode.net
|
||||
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
|
||||
|
||||
==========================
|
||||
Installation Instructions:
|
||||
==========================
|
||||
|
||||
For more detailed instructions see: http://dev.deluge-torrent.org/wiki/Installing/Source
|
||||
|
||||
See: DEPENDS for a full list of dependencies.
|
||||
|
||||
First, make sure you have the proper build dependencies installed. On a normal
|
||||
Debian or Ubuntu system:
|
||||
|
||||
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
|
||||
|
||||
The names of the packages may vary depending on your OS / distro.
|
||||
|
||||
Once you have the needed libraries installed, build and install by running:
|
||||
|
||||
$ python setup.py build
|
||||
$ sudo python setup.py install
|
||||
|
||||
==========================
|
||||
FAQ
|
||||
==========================
|
||||
|
||||
For the full FAQ see: http://dev.deluge-torrent.org/wiki/Faq
|
||||
|
||||
How to start the various user-interfaces
|
||||
|
||||
Gtk:
|
||||
deluge or deluge-gtk
|
||||
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
|
||||
|
||||
See: http://dev.deluge-torrent.org/wiki/UserGuide/ThinClient
|
||||
* 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.
|
||||
|
||||
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
|
||||
|
@ -1,21 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Fixes glade files which may have set gtk stock labels set to translatable
|
||||
for x in `find . -name '*.glade' |grep -v '.git\|build'` ; do \
|
||||
for y in gtk-add gtk-apply gtk-bold gtk-cancel gtk-cdrom gtk-clear \
|
||||
gtk-close gtk-color-picker gtk-connect gtk-convert gtk-copy gtk-cut \
|
||||
gtk-delete gtk-dialog-error gtk-dialog-info gtk-dialog-question \
|
||||
gtk-dialog-warning gtk-dnd gtk-dnd-multiple gtk-edit gtk-execute gtk-find \
|
||||
gtk-find-and-replace gtk-floppy gtk-goto-bottom gtk-goto-first \
|
||||
gtk-goto-last gtk-goto-top gtk-go-back gtk-go-down gtk-go-forward \
|
||||
gtk-go-up gtk-help gtk-home gtk-index gtk-italic gtk-jump-to \
|
||||
gtk-justify-center gtk-justify-fill gtk-justify-left gtk-missing-image \
|
||||
gtk-new gtk-no gtk-ok gtk-open gtk-paste gtk-preferences gtk-print \
|
||||
gtk-print-preview gtk-properties gtk-quit gtk-redo gtk-refresh \
|
||||
gtk-remove gtk-revert-to-saved gtk-save gtk-save-as gtk-select-color \
|
||||
gtk-select-font gtk-sort-descending gtk-spell-check gtk-stop \
|
||||
gtk-strikethrough gtk-undelete gtk-underline gtk-undo gtk-yes \
|
||||
gtk-zoom-100 gtk-zoom-fit gtk-zoom-in gtk-zoom-out; do \
|
||||
sed -i "s/<property\ name\=\"label\"\ translatable\=\"yes\">$y<\/property>/<property\ name\=\"label\"\ translatable\=\"no\">$y<\/property>/g" $x; \
|
||||
done;\
|
||||
done
|
17
create_potfiles_in.py
Executable file → Normal file
@ -1,26 +1,17 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
# Paths to exclude
|
||||
EXCLUSIONS = [
|
||||
"deluge/scripts",
|
||||
"deluge/i18n",
|
||||
"deluge/scripts"
|
||||
]
|
||||
|
||||
POTFILE_IN = "deluge/i18n/POTFILES.in"
|
||||
|
||||
pattern = "deluge\/plugins\/.*\/build"
|
||||
compiled = re.compile(pattern)
|
||||
|
||||
sys.stdout.write("Creating " + POTFILE_IN + " ... ")
|
||||
sys.stdout.flush()
|
||||
print "Creating " + POTFILE_IN + " .."
|
||||
to_translate = []
|
||||
for (dirpath, dirnames, filenames) in os.walk("deluge"):
|
||||
for filename in filenames:
|
||||
if os.path.splitext(filename)[1] in (".py", ".glade", ".in") \
|
||||
and dirpath not in EXCLUSIONS \
|
||||
and not compiled.match(dirpath):
|
||||
if os.path.splitext(filename)[1] in (".py", ".glade") and dirpath not in EXCLUSIONS:
|
||||
to_translate.append(os.path.join(dirpath, filename))
|
||||
|
||||
f = open(POTFILE_IN, "wb")
|
||||
|
@ -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/ui/data/\
|
||||
for size in 16 22 24 32 36 48 64 72 96 128 192 256; do mkdir -p deluge/data/\
|
||||
icons/hicolor/${size}x${size}/apps; rsvg-convert -w ${size} -h ${size} \
|
||||
-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
|
||||
-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
|
||||
|
@ -1,4 +1 @@
|
||||
"""Deluge"""
|
||||
# this is a namespace package
|
||||
import pkg_resources
|
||||
pkg_resources.declare_namespace(__name__)
|
||||
|
@ -45,9 +45,9 @@ supports.
|
||||
|
||||
"""
|
||||
|
||||
REQUIRED_VERSION = "0.16.7.0"
|
||||
REQUIRED_VERSION = "0.14.5.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)
|
478
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,25 +37,16 @@
|
||||
"""Common functions for various parts of Deluge to use."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import subprocess
|
||||
import platform
|
||||
import chardet
|
||||
import logging
|
||||
import pkg_resources
|
||||
import gettext
|
||||
import locale
|
||||
import sys
|
||||
|
||||
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"):
|
||||
@ -71,6 +62,11 @@ 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,
|
||||
@ -90,6 +86,7 @@ LT_TORRENT_STATE = {
|
||||
7: "Checking Resume Data"
|
||||
}
|
||||
|
||||
|
||||
TORRENT_STATE = [
|
||||
"Allocating",
|
||||
"Checking",
|
||||
@ -104,15 +101,11 @@ FILE_PRIORITY = {
|
||||
0: "Do Not Download",
|
||||
1: "Normal Priority",
|
||||
2: "High Priority",
|
||||
3: "High Priority",
|
||||
4: "High Priority",
|
||||
5: "High Priority",
|
||||
6: "High Priority",
|
||||
7: "Highest Priority",
|
||||
5: "Highest Priority",
|
||||
"Do Not Download": 0,
|
||||
"Normal Priority": 1,
|
||||
"High Priority": 5,
|
||||
"Highest Priority": 7
|
||||
"High Priority": 2,
|
||||
"Highest Priority": 5
|
||||
}
|
||||
|
||||
def get_version():
|
||||
@ -133,26 +126,16 @@ def get_default_config_dir(filename=None):
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
|
||||
if windows_check():
|
||||
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)
|
||||
if filename:
|
||||
return os.path.join(os.environ.get("APPDATA"), "deluge", filename)
|
||||
else:
|
||||
return os.path.join(os.environ.get("APPDATA"), "deluge")
|
||||
else:
|
||||
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)
|
||||
if filename:
|
||||
return os.path.join(xdg.BaseDirectory.save_config_path("deluge"), filename)
|
||||
else:
|
||||
return xdg.BaseDirectory.save_config_path("deluge")
|
||||
|
||||
def get_default_download_dir():
|
||||
"""
|
||||
@ -161,20 +144,8 @@ def get_default_download_dir():
|
||||
|
||||
"""
|
||||
if windows_check():
|
||||
return os.path.join(os.path.expanduser("~"), 'Downloads')
|
||||
return os.path.expanduser("~")
|
||||
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():
|
||||
@ -209,7 +180,7 @@ def osx_check():
|
||||
|
||||
def get_pixmap(fname):
|
||||
"""
|
||||
Provides easy access to files in the deluge/ui/data/pixmaps folder within the Deluge egg
|
||||
Provides easy access to files in the deluge/data/pixmaps folder within the Deluge egg
|
||||
|
||||
:param fname: the filename to look for
|
||||
:type fname: string
|
||||
@ -217,18 +188,8 @@ def get_pixmap(fname):
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
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]))
|
||||
)
|
||||
return pkg_resources.resource_filename("deluge", os.path.join("data", \
|
||||
"pixmaps", fname))
|
||||
|
||||
def open_file(path):
|
||||
"""
|
||||
@ -239,9 +200,7 @@ def open_file(path):
|
||||
|
||||
"""
|
||||
if windows_check():
|
||||
os.startfile(path.decode("utf8"))
|
||||
elif osx_check():
|
||||
subprocess.Popen(["open", "%s" % path])
|
||||
os.startfile("%s" % path)
|
||||
else:
|
||||
subprocess.Popen(["xdg-open", "%s" % path])
|
||||
|
||||
@ -251,7 +210,7 @@ def open_url_in_browser(url):
|
||||
|
||||
:param url: the url to open
|
||||
:type url: string
|
||||
|
||||
|
||||
"""
|
||||
import webbrowser
|
||||
webbrowser.open(url)
|
||||
@ -275,36 +234,12 @@ def fsize(fsize_b):
|
||||
"""
|
||||
fsize_kb = fsize_b / 1024.0
|
||||
if fsize_kb < 1024:
|
||||
return "%.1f %s" % (fsize_kb, _("KiB"))
|
||||
return "%.1f KiB" % fsize_kb
|
||||
fsize_mb = fsize_kb / 1024.0
|
||||
if fsize_mb < 1024:
|
||||
return "%.1f %s" % (fsize_mb, _("MiB"))
|
||||
return "%.1f MiB" % fsize_mb
|
||||
fsize_gb = fsize_mb / 1024.0
|
||||
return "%.1f %s" % (fsize_gb, _("GiB"))
|
||||
|
||||
def fsize_short(fsize_b):
|
||||
"""
|
||||
Formats the bytes value into a string with K, M or G units
|
||||
|
||||
:param fsize_b: the filesize in bytes
|
||||
:type fsize_b: int
|
||||
:returns: formatted string in K, M or G units
|
||||
:rtype: string
|
||||
|
||||
**Usage**
|
||||
|
||||
>>> fsize(112245)
|
||||
'109.6 K'
|
||||
|
||||
"""
|
||||
fsize_kb = fsize_b / 1024.0
|
||||
if fsize_kb < 1024:
|
||||
return "%.1f %s" % (fsize_kb, _("K"))
|
||||
fsize_mb = fsize_kb / 1024.0
|
||||
if fsize_mb < 1024:
|
||||
return "%.1f %s" % (fsize_mb, _("M"))
|
||||
fsize_gb = fsize_mb / 1024.0
|
||||
return "%.1f %s" % (fsize_gb, _("G"))
|
||||
return "%.1f GiB" % fsize_gb
|
||||
|
||||
def fpcnt(dec):
|
||||
"""
|
||||
@ -338,14 +273,7 @@ def fspeed(bps):
|
||||
'42.1 KiB/s'
|
||||
|
||||
"""
|
||||
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"))
|
||||
return '%s/s' % (fsize(bps))
|
||||
|
||||
def fpeer(num_peers, total_peers):
|
||||
"""
|
||||
@ -412,21 +340,21 @@ def ftime(seconds):
|
||||
|
||||
def fdate(seconds):
|
||||
"""
|
||||
Formats a date time string in the locale's date representation based on the systems timezone
|
||||
Formats a date 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 datetime representation or "" if seconds < 0
|
||||
:returns: a string in the locale's date representation or "" if seconds < 0
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
if seconds < 0:
|
||||
return ""
|
||||
return time.strftime("%x %X", time.localtime(seconds))
|
||||
return time.strftime("%x", time.localtime(seconds))
|
||||
|
||||
def is_url(url):
|
||||
"""
|
||||
A simple test to check if the URL is valid
|
||||
A simple regex test to check if the URL is valid
|
||||
|
||||
:param url: the url to test
|
||||
:type url: string
|
||||
@ -439,7 +367,8 @@ def is_url(url):
|
||||
True
|
||||
|
||||
"""
|
||||
return url.partition('://')[0] in ("http", "https", "ftp", "udp")
|
||||
import re
|
||||
return bool(re.search('^(https?|ftp|udp)://', url))
|
||||
|
||||
def is_magnet(uri):
|
||||
"""
|
||||
@ -456,12 +385,34 @@ def is_magnet(uri):
|
||||
True
|
||||
|
||||
"""
|
||||
magnet_scheme = 'magnet:?'
|
||||
xt_param = 'xt=urn:btih:'
|
||||
if uri.startswith(magnet_scheme) and xt_param in uri:
|
||||
if uri[:20] == "magnet:?xt=urn:btih:":
|
||||
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
|
||||
@ -518,19 +469,20 @@ 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():
|
||||
from win32file import GetDiskFreeSpaceEx
|
||||
return GetDiskFreeSpaceEx(path)[0]
|
||||
import win32file
|
||||
sectors, bytes, free, total = map(long, win32file.GetDiskFreeSpace(path))
|
||||
return (free * sectors * bytes)
|
||||
else:
|
||||
disk_data = os.statvfs(path.encode("utf8"))
|
||||
block_size = disk_data.f_frsize
|
||||
disk_data = os.statvfs(path)
|
||||
block_size = disk_data.f_bsize
|
||||
return disk_data.f_bavail * block_size
|
||||
|
||||
def is_ip(ip):
|
||||
@ -551,290 +503,58 @@ def is_ip(ip):
|
||||
import socket
|
||||
#first we test ipv4
|
||||
try:
|
||||
if windows_check():
|
||||
if socket.inet_aton("%s" % (ip)):
|
||||
return True
|
||||
else:
|
||||
if socket.inet_pton(socket.AF_INET, "%s" % (ip)):
|
||||
return True
|
||||
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 windows_check():
|
||||
log.warning("ipv6 check unavailable on windows")
|
||||
if socket.inet_pton(socket.AF_INET6, "%s" % (ip)):
|
||||
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):
|
||||
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(".")]
|
||||
ver = ver.lower()
|
||||
vs = ver.split("_") if "_" in ver else ver.split("-")
|
||||
self.version = vs[0]
|
||||
self.suffix = None
|
||||
self.dev = False
|
||||
if len(vs) > 1:
|
||||
if vs[1].startswith(("rc", "a", "b", "c")):
|
||||
self.suffix = vs[1]
|
||||
if vs[-1].startswith('dev'):
|
||||
self.dev = vs[-1]
|
||||
for s in ("rc", "alpha", "beta", "dev"):
|
||||
if s in vs[1][:len(s)]:
|
||||
self.suffix = vs[1]
|
||||
|
||||
def __cmp__(self, ver):
|
||||
"""
|
||||
The comparison method.
|
||||
|
||||
|
||||
:param ver: the version to compare with
|
||||
:type ver: VersionSplit
|
||||
|
||||
|
||||
"""
|
||||
# PEP 386 versions with .devN precede release version
|
||||
if (bool(self.dev) != bool(ver.dev)):
|
||||
if self.dev != 'dev':
|
||||
self.dev = not self.dev
|
||||
if ver.dev != 'dev':
|
||||
ver.dev = not ver.dev
|
||||
|
||||
if self.version > ver.version or (self.suffix and self.suffix[:3] == "dev"):
|
||||
return 1
|
||||
if self.version < ver.version:
|
||||
return -1
|
||||
|
||||
# If there is no suffix we use z because we want final
|
||||
# to appear after alpha, beta, and rc alphabetically.
|
||||
v1 = [self.version, self.suffix or 'z', self.dev]
|
||||
v2 = [ver.version, ver.suffix or 'z', ver.dev]
|
||||
return cmp(v1, v2)
|
||||
|
||||
|
||||
# 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]
|
||||
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
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# component.py
|
||||
#
|
||||
# Copyright (C) 2007-2010 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2007, 2008 Andrew Resch <andrewresch@gmail.com>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
@ -33,413 +33,213 @@
|
||||
#
|
||||
#
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from twisted.internet.defer import maybeDeferred, succeed, DeferredList, fail
|
||||
from twisted.internet.task import LoopingCall
|
||||
from deluge.log import LOG as log
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class ComponentAlreadyRegistered(Exception):
|
||||
pass
|
||||
COMPONENT_STATE = [
|
||||
"Stopped",
|
||||
"Started",
|
||||
"Paused"
|
||||
]
|
||||
|
||||
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):
|
||||
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)
|
||||
# Register with the ComponentRegistry
|
||||
register(name, self, depend)
|
||||
self._interval = interval
|
||||
self._timer = None
|
||||
self._state = COMPONENT_STATE.index("Stopped")
|
||||
self._name = name
|
||||
|
||||
def __del__(self):
|
||||
if _ComponentRegistry:
|
||||
_ComponentRegistry.deregister(self)
|
||||
def get_state(self):
|
||||
return self._state
|
||||
|
||||
def _component_start_timer(self):
|
||||
if hasattr(self, "update"):
|
||||
self._component_timer = LoopingCall(self.update)
|
||||
self._component_timer.start(self._component_interval)
|
||||
|
||||
def _component_start(self):
|
||||
def on_start(result):
|
||||
self._component_state = "Started"
|
||||
self._component_starting_deferred = None
|
||||
self._component_start_timer()
|
||||
return True
|
||||
|
||||
def on_start_fail(result):
|
||||
self._component_state = "Stopped"
|
||||
self._component_starting_deferred = None
|
||||
log.error(result)
|
||||
return result
|
||||
|
||||
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!")
|
||||
|
||||
return d
|
||||
|
||||
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
|
||||
|
||||
def on_stop_fail(result):
|
||||
self._component_state = "Started"
|
||||
self._component_stopping_deferred = None
|
||||
log.error(result)
|
||||
return result
|
||||
|
||||
if self._component_state != "Stopped" and self._component_state != "Stopping":
|
||||
if hasattr(self, "stop"):
|
||||
self._component_state = "Stopping"
|
||||
d = maybeDeferred(self.stop)
|
||||
d.addCallback(on_stop)
|
||||
d.addErrback(on_stop_fail)
|
||||
self._component_stopping_deferred = d
|
||||
else:
|
||||
d = maybeDeferred(on_stop, None)
|
||||
|
||||
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 get_component_name(self):
|
||||
return self._name
|
||||
|
||||
def start(self):
|
||||
pass
|
||||
|
||||
def _start(self):
|
||||
self._state = COMPONENT_STATE.index("Started")
|
||||
if hasattr(self, "update"):
|
||||
self._timer = LoopingCall(self.update)
|
||||
self._timer.start(self._interval)
|
||||
|
||||
def stop(self):
|
||||
pass
|
||||
|
||||
def update(self):
|
||||
pass
|
||||
def _stop(self):
|
||||
self._state = COMPONENT_STATE.index("Stopped")
|
||||
try:
|
||||
self._timer.stop()
|
||||
except:
|
||||
pass
|
||||
|
||||
def _pause(self):
|
||||
self._state = COMPONENT_STATE.index("Paused")
|
||||
try:
|
||||
self._timer.stop()
|
||||
except:
|
||||
pass
|
||||
|
||||
def _resume(self):
|
||||
self._start()
|
||||
|
||||
def shutdown(self):
|
||||
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.
|
||||
"""
|
||||
class ComponentRegistry:
|
||||
def __init__(self):
|
||||
self.components = {}
|
||||
# Stores all of the components that are dependent on a particular component
|
||||
self.dependents = defaultdict(list)
|
||||
self.depend = {}
|
||||
|
||||
def register(self, obj):
|
||||
"""
|
||||
Registers a component object with the registry. This is done
|
||||
automatically when a Component object is instantiated.
|
||||
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
|
||||
|
||||
:param obj: the Component object
|
||||
:type obj: object
|
||||
|
||||
:raises ComponentAlreadyRegistered: if a component with the same name is already registered.
|
||||
|
||||
"""
|
||||
name = obj._component_name
|
||||
def deregister(self, name):
|
||||
"""Deregisters a component"""
|
||||
if name in self.components:
|
||||
raise ComponentAlreadyRegistered(
|
||||
"Component already registered with name %s" % name)
|
||||
log.debug("Deregistering Component: %s", name)
|
||||
self.stop_component(name)
|
||||
del self.components[name]
|
||||
|
||||
self.components[obj._component_name] = obj
|
||||
if obj._component_depend:
|
||||
for depend in obj._component_depend:
|
||||
self.dependents[depend].append(name)
|
||||
def get(self, name):
|
||||
"""Returns a reference to the component 'name'"""
|
||||
return self.components[name]
|
||||
|
||||
def deregister(self, obj):
|
||||
"""
|
||||
Deregisters a component from the registry. A stop will be
|
||||
issued to the component prior to deregistering it.
|
||||
def start(self):
|
||||
"""Starts all components"""
|
||||
for component in self.components.keys():
|
||||
self.start_component(component)
|
||||
|
||||
:param obj: the Component object
|
||||
:type obj: object
|
||||
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()
|
||||
|
||||
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 stop(self):
|
||||
"""Stops all components"""
|
||||
# We create a separate list of the keys and do an additional check to
|
||||
# make sure the key still exists in the components dict.
|
||||
# This is because components could be deregistered during a stop and
|
||||
# the dictionary would get modified while iterating through it.
|
||||
components = self.components.keys()
|
||||
for component in components:
|
||||
if component in self.components:
|
||||
self.stop_component(component)
|
||||
|
||||
def start(self, names=[]):
|
||||
"""
|
||||
Starts Components that are currently in a Stopped state and their
|
||||
dependencies. If *names* is specified, will only start those
|
||||
Components and their dependencies and if not it will start all
|
||||
registered components.
|
||||
def stop_component(self, component):
|
||||
if self.components[component].get_state() != \
|
||||
COMPONENT_STATE.index("Stopped"):
|
||||
log.debug("Stopping component %s..", component)
|
||||
self.components[component].stop()
|
||||
self.components[component]._stop()
|
||||
|
||||
:param names: a list of Components to start
|
||||
:type names: list
|
||||
def pause(self):
|
||||
"""Pauses all components. Stops calling update()"""
|
||||
for component in self.components.keys():
|
||||
self.pause_component(component)
|
||||
|
||||
:returns: a Deferred object that will fire once all Components have been sucessfully started
|
||||
:rtype: twisted.internet.defer.Deferred
|
||||
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()
|
||||
|
||||
"""
|
||||
# Start all the components if names is empty
|
||||
if not names:
|
||||
names = self.components.keys()
|
||||
elif isinstance(names, str):
|
||||
names = [names]
|
||||
def resume(self):
|
||||
"""Resumes all components. Starts calling update()"""
|
||||
for component in self.components.keys():
|
||||
self.resume_component(component)
|
||||
|
||||
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 resume_component(self, component):
|
||||
if self.components[component].get_state() == COMPONENT_STATE.index("Paused"):
|
||||
log.debug("Resuming component %s..", component)
|
||||
self.components[component]._resume()
|
||||
|
||||
def update(self):
|
||||
"""
|
||||
Updates all Components that are in a Started state.
|
||||
"""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
|
||||
|
||||
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)
|
||||
|
||||
"""
|
||||
for component in self.components.items():
|
||||
component.update()
|
||||
|
||||
_ComponentRegistry = ComponentRegistry()
|
||||
|
||||
deregister = _ComponentRegistry.deregister
|
||||
start = _ComponentRegistry.start
|
||||
stop = _ComponentRegistry.stop
|
||||
pause = _ComponentRegistry.pause
|
||||
resume = _ComponentRegistry.resume
|
||||
update = _ComponentRegistry.update
|
||||
shutdown = _ComponentRegistry.shutdown
|
||||
def register(name, obj, depend=None):
|
||||
"""Registers a component with the registry"""
|
||||
_ComponentRegistry.register(name, obj, depend)
|
||||
|
||||
def get(name):
|
||||
"""
|
||||
Return a reference to a component.
|
||||
def deregister(name):
|
||||
"""Deregisters a component"""
|
||||
_ComponentRegistry.deregister(name)
|
||||
|
||||
:param name: the Component name to get
|
||||
:type name: string
|
||||
def start(component=None):
|
||||
"""Starts all components"""
|
||||
if component == None:
|
||||
_ComponentRegistry.start()
|
||||
else:
|
||||
_ComponentRegistry.start_component(component)
|
||||
|
||||
:returns: the Component object
|
||||
:rtype: object
|
||||
def stop(component=None):
|
||||
"""Stops all or specified components"""
|
||||
if component == None:
|
||||
_ComponentRegistry.stop()
|
||||
else:
|
||||
_ComponentRegistry.stop_component(component)
|
||||
|
||||
:raises KeyError: if the Component does not exist
|
||||
def pause(component=None):
|
||||
"""Pauses all or specificed components"""
|
||||
if component == None:
|
||||
_ComponentRegistry.pause()
|
||||
else:
|
||||
_ComponentRegistry.pause_component(component)
|
||||
|
||||
"""
|
||||
return _ComponentRegistry.components[name]
|
||||
def resume(component=None):
|
||||
"""Resumes all or specificed components"""
|
||||
if component == None:
|
||||
_ComponentRegistry.resume()
|
||||
else:
|
||||
_ComponentRegistry.resume_component(component)
|
||||
|
||||
def update():
|
||||
"""Updates all components"""
|
||||
_ComponentRegistry.update()
|
||||
|
||||
def shutdown():
|
||||
"""Shutdowns all components"""
|
||||
_ComponentRegistry.shutdown()
|
||||
|
||||
def get(component):
|
||||
"""Return a reference to the component"""
|
||||
return _ComponentRegistry.get(component)
|
||||
|
@ -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,16 +68,14 @@ 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
|
||||
|
||||
@ -95,13 +93,13 @@ def prop(func):
|
||||
def find_json_objects(s):
|
||||
"""
|
||||
Find json objects in a string.
|
||||
|
||||
|
||||
:param s: the string to find json objects in
|
||||
:type s: string
|
||||
|
||||
|
||||
:returns: a list of tuples containing start and end locations of json objects in the string `s`
|
||||
:rtype: [(start, end), ...]
|
||||
|
||||
|
||||
"""
|
||||
objects = []
|
||||
opens = 0
|
||||
@ -121,8 +119,8 @@ def find_json_objects(s):
|
||||
start = index + offset + 1
|
||||
|
||||
return objects
|
||||
|
||||
|
||||
|
||||
|
||||
class Config(object):
|
||||
"""
|
||||
This class is used to access/create/modify config files
|
||||
@ -148,8 +146,7 @@ class Config(object):
|
||||
self._save_timer = None
|
||||
|
||||
if defaults:
|
||||
for key, value in defaults.iteritems():
|
||||
self.set_item(key, value)
|
||||
self.__config = dict(defaults)
|
||||
|
||||
# Load the config from file in the config_dir
|
||||
if config_dir:
|
||||
@ -159,9 +156,6 @@ class Config(object):
|
||||
|
||||
self.load()
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self.__config
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""
|
||||
See
|
||||
@ -190,10 +184,6 @@ 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))
|
||||
@ -207,10 +197,7 @@ 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:
|
||||
if oldtype == unicode:
|
||||
value = oldtype(value, "utf8")
|
||||
else:
|
||||
value = oldtype(value)
|
||||
value = oldtype(value)
|
||||
except ValueError:
|
||||
log.warning("Type '%s' invalid for '%s'", newtype, key)
|
||||
raise
|
||||
@ -260,38 +247,7 @@ what is currently in the config and it could not convert the value
|
||||
5
|
||||
|
||||
"""
|
||||
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)
|
||||
|
||||
return self.__config[key]
|
||||
|
||||
def register_change_callback(self, callback):
|
||||
"""
|
||||
@ -389,21 +345,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]
|
||||
@ -412,8 +368,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)
|
||||
|
||||
@ -437,24 +393,26 @@ 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
|
||||
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)
|
||||
self._save_timer.cancel()
|
||||
return
|
||||
except Exception, e:
|
||||
log.warning("Unable to open config file: %s", filename)
|
||||
|
||||
|
||||
|
||||
# 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 IOError, e:
|
||||
except Exception, e:
|
||||
log.error("Error writing new config file: %s", e)
|
||||
return False
|
||||
|
||||
@ -463,7 +421,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.warning("Unable to backup old config...")
|
||||
log.error("Error backing up old config..")
|
||||
|
||||
# The new config file has been written successfully, so let's move it over
|
||||
# the existing one.
|
||||
|
@ -34,14 +34,11 @@
|
||||
#
|
||||
|
||||
import os
|
||||
import logging
|
||||
|
||||
import deluge.common
|
||||
import deluge.log
|
||||
from deluge.log import LOG as log
|
||||
from deluge.config import Config
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class _ConfigManager:
|
||||
def __init__(self):
|
||||
log.debug("ConfigManager started..")
|
||||
@ -55,6 +52,7 @@ class _ConfigManager:
|
||||
return self.__config_directory
|
||||
|
||||
def __del__(self):
|
||||
log.debug("ConfigManager stopping..")
|
||||
del self.config_files
|
||||
|
||||
def set_config_dir(self, directory):
|
||||
@ -88,7 +86,6 @@ class _ConfigManager:
|
||||
# to reload based on the new config directory
|
||||
self.save()
|
||||
self.config_files = {}
|
||||
deluge.log.tweak_logging_levels()
|
||||
|
||||
return True
|
||||
|
||||
|
@ -41,14 +41,12 @@ 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
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
from deluge.log import LOG as log
|
||||
|
||||
class AlertManager(component.Component):
|
||||
def __init__(self):
|
||||
@ -67,18 +65,9 @@ class AlertManager(component.Component):
|
||||
|
||||
# handlers is a dictionary of lists {"alert_type": [handler1,h2,..]}
|
||||
self.handlers = {}
|
||||
self.delayed_calls = []
|
||||
self.wait_on_handler = False
|
||||
|
||||
def update(self):
|
||||
self.delayed_calls = [dc for dc in self.delayed_calls if dc.active()]
|
||||
self.handle_alerts(wait=self.wait_on_handler)
|
||||
|
||||
def stop(self):
|
||||
for dc in self.delayed_calls:
|
||||
if dc.active():
|
||||
dc.cancel()
|
||||
self.delayed_calls = []
|
||||
self.handle_alerts()
|
||||
|
||||
def register_handler(self, alert_type, handler):
|
||||
"""
|
||||
@ -118,17 +107,18 @@ 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
|
||||
"""
|
||||
alerts = self.session.pop_alerts()
|
||||
alert = self.session.pop_alert()
|
||||
# Loop through all alerts in the queue
|
||||
for alert in alerts:
|
||||
while alert is not None:
|
||||
alert_type = type(alert).__name__
|
||||
# Display the alert message
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug("%s: %s", alert_type, decode_string(alert.message()))
|
||||
log.debug("%s: %s", alert_type, 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:
|
||||
self.delayed_calls.append(reactor.callLater(0, handler, alert))
|
||||
reactor.callLater(0, handler, alert)
|
||||
else:
|
||||
handler(alert)
|
||||
|
||||
alert = self.session.pop_alert()
|
||||
|
@ -2,7 +2,6 @@
|
||||
# authmanager.py
|
||||
#
|
||||
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
@ -37,56 +36,27 @@
|
||||
import os
|
||||
import random
|
||||
import stat
|
||||
import shutil
|
||||
import logging
|
||||
|
||||
import deluge.component as component
|
||||
import deluge.configmanager as configmanager
|
||||
from deluge.common import (AUTH_LEVEL_ADMIN, AUTH_LEVEL_NONE, AUTH_LEVEL_NORMAL,
|
||||
AUTH_LEVEL_READONLY, AUTH_LEVEL_DEFAULT,
|
||||
create_localclient_account)
|
||||
import deluge.error
|
||||
|
||||
from deluge.error import AuthManagerError, AuthenticationRequired, BadLoginError
|
||||
from deluge.log import LOG as log
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
AUTH_LEVEL_NONE = 0
|
||||
AUTH_LEVEL_READONLY = 1
|
||||
AUTH_LEVEL_NORMAL = 5
|
||||
AUTH_LEVEL_ADMIN = 10
|
||||
|
||||
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__)
|
||||
AUTH_LEVEL_DEFAULT = AUTH_LEVEL_NORMAL
|
||||
|
||||
class BadLoginError(deluge.error.DelugeError):
|
||||
pass
|
||||
|
||||
class AuthManager(component.Component):
|
||||
def __init__(self):
|
||||
component.Component.__init__(self, "AuthManager", interval=10)
|
||||
component.Component.__init__(self, "AuthManager")
|
||||
self.__auth = {}
|
||||
self.__auth_modification_time = None
|
||||
|
||||
def start(self):
|
||||
self.__load_auth_file()
|
||||
@ -97,19 +67,6 @@ 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
|
||||
@ -119,127 +76,55 @@ class AuthManager(component.Component):
|
||||
:returns: int, the auth level for this user
|
||||
:rtype: int
|
||||
|
||||
:raises AuthenticationRequired: if aditional details are required to
|
||||
authenticate.
|
||||
:raises BadLoginError: if the username does not exist or password does
|
||||
not match.
|
||||
: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", username)
|
||||
raise BadLoginError("Username does not exist")
|
||||
|
||||
if self.__auth[username].password == password:
|
||||
if self.__auth[username][0] == password:
|
||||
# Return the users auth level
|
||||
return self.__auth[username].authlevel
|
||||
elif not password and self.__auth[username].password:
|
||||
raise AuthenticationRequired("Password is required", username)
|
||||
return int(self.__auth[username][1])
|
||||
else:
|
||||
raise BadLoginError("Password does not match", username)
|
||||
raise BadLoginError("Password does not match")
|
||||
|
||||
def has_account(self, username):
|
||||
return username in self.__auth
|
||||
|
||||
def get_known_accounts(self):
|
||||
def __create_localclient_account(self):
|
||||
"""
|
||||
Returns a list of known deluge usernames.
|
||||
Returns the string.
|
||||
"""
|
||||
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)
|
||||
# We create a 'localclient' account with a random password
|
||||
try:
|
||||
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 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()
|
||||
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()
|
||||
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"
|
||||
|
||||
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()
|
||||
localclient = self.__create_localclient_account()
|
||||
fd = open(auth_file, "w")
|
||||
fd.write(localclient)
|
||||
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()
|
||||
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line.startswith("#") or not line:
|
||||
# This line is a comment or empty
|
||||
if line.startswith("#"):
|
||||
# This is a comment line
|
||||
continue
|
||||
line = line.strip()
|
||||
try:
|
||||
lsplit = line.split(":")
|
||||
except Exception, e:
|
||||
@ -247,43 +132,15 @@ 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)
|
||||
if username == 'localclient':
|
||||
authlevel = AUTH_LEVEL_ADMIN
|
||||
else:
|
||||
authlevel = AUTH_LEVEL_DEFAULT
|
||||
# This is probably an old auth file
|
||||
save_and_reload = True
|
||||
log.warning("Your auth entry for %s contains no auth level, using AUTH_LEVEL_DEFAULT(%s)..", username, AUTH_LEVEL_DEFAULT)
|
||||
level = AUTH_LEVEL_DEFAULT
|
||||
elif len(lsplit) == 3:
|
||||
username, password, authlevel = lsplit
|
||||
username, password, level = 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
|
||||
|
||||
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)
|
||||
self.__auth[username.strip()] = (password.strip(), level)
|
||||
|
||||
if "localclient" not in self.__auth:
|
||||
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
|
||||
|
||||
open(auth_file, "a").write(self.__create_localclient_account())
|
||||
|
131
deluge/core/autoadd.py
Normal file
@ -0,0 +1,131 @@
|
||||
#
|
||||
# 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,7 +2,6 @@
|
||||
# core.py
|
||||
#
|
||||
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
@ -18,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
|
||||
@ -39,34 +38,36 @@ from deluge._libtorrent import lt
|
||||
import os
|
||||
import glob
|
||||
import base64
|
||||
import logging
|
||||
import shutil
|
||||
import threading
|
||||
import tempfile
|
||||
from urlparse import urljoin
|
||||
import pkg_resources
|
||||
import warnings
|
||||
|
||||
|
||||
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..")
|
||||
@ -76,41 +77,27 @@ class Core(component.Component):
|
||||
log.info("Starting libtorrent %s session..", lt.version)
|
||||
|
||||
# Create the client fingerprint
|
||||
version = deluge.common.VersionSplit(deluge.common.get_version()).version
|
||||
version = [int(value.split("-")[0]) for value in deluge.common.get_version().split(".")]
|
||||
while len(version) < 4:
|
||||
version.append(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)
|
||||
self.session = lt.session(lt.fingerprint("DE", *version), flags=0)
|
||||
|
||||
# Load the session state if available
|
||||
self.__load_session_state()
|
||||
|
||||
# Set the user agent
|
||||
self.settings = lt.session_settings()
|
||||
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
|
||||
self.settings.user_agent = "Deluge %s" % deluge.common.get_version()
|
||||
|
||||
# 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
|
||||
# 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)
|
||||
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()
|
||||
@ -119,6 +106,7 @@ 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
|
||||
@ -126,7 +114,6 @@ 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
|
||||
@ -141,12 +128,9 @@ 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()
|
||||
|
||||
@ -163,16 +147,16 @@ class Core(component.Component):
|
||||
def __save_session_state(self):
|
||||
"""Saves the libtorrent session state"""
|
||||
try:
|
||||
session_state = deluge.configmanager.get_config_dir("session.state")
|
||||
open(session_state, "wb").write(lt.bencode(self.session.save_state()))
|
||||
open(deluge.configmanager.get_config_dir("session.state"), "wb").write(
|
||||
lt.bencode(self.session.state()))
|
||||
except Exception, e:
|
||||
log.warning("Failed to save lt state: %s", e)
|
||||
|
||||
def __load_session_state(self):
|
||||
"""Loads the libtorrent session state"""
|
||||
try:
|
||||
session_state = deluge.configmanager.get_config_dir("session.state")
|
||||
self.session.load_state(lt.bdecode(open(session_state, "rb").read()))
|
||||
self.session.load_state(lt.bdecode(
|
||||
open(deluge.configmanager.get_config_dir("session.state"), "rb").read()))
|
||||
except Exception, e:
|
||||
log.warning("Failed to load lt state: %s", e)
|
||||
|
||||
@ -228,9 +212,7 @@ 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)
|
||||
@ -254,44 +236,20 @@ 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_download_success(filename):
|
||||
def on_get_file(filename):
|
||||
# We got the file, so add it to the session
|
||||
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
|
||||
)
|
||||
data = open(filename, "rb").read()
|
||||
return self.add_torrent_file(filename, base64.encodestring(data), options)
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
d = download_file(
|
||||
url, tempfile.mkstemp()[1], headers=headers, force_filename=True
|
||||
)
|
||||
d.addCallbacks(on_download_success, on_download_fail)
|
||||
d = download_file(url, url.split("/")[-1], headers=headers)
|
||||
d.addCallback(on_get_file)
|
||||
d.addErrback(on_get_file_error)
|
||||
return d
|
||||
|
||||
@export
|
||||
@ -330,6 +288,28 @@ 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):
|
||||
"""
|
||||
@ -426,43 +406,30 @@ class Core(component.Component):
|
||||
for torrent_id in torrent_ids:
|
||||
self.torrentmanager[torrent_id].resume()
|
||||
|
||||
def create_torrent_status(self, torrent_id, torrent_keys, plugin_keys, diff=False, update=False):
|
||||
try:
|
||||
status = self.torrentmanager[torrent_id].get_status(torrent_keys, diff, update=update)
|
||||
except KeyError:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
# Torrent was probaly removed meanwhile
|
||||
return {}
|
||||
@export
|
||||
def get_torrent_status(self, torrent_id, keys):
|
||||
# Build the status dictionary
|
||||
status = self.torrentmanager[torrent_id].get_status(keys)
|
||||
|
||||
# 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))
|
||||
# Get the leftover fields and ask the plugin manager to fill them
|
||||
leftover_fields = list(set(keys) - set(status.keys()))
|
||||
if len(leftover_fields) > 0:
|
||||
status.update(self.pluginmanager.get_status(torrent_id, leftover_fields))
|
||||
return status
|
||||
|
||||
@export
|
||||
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):
|
||||
def get_torrents_status(self, filter_dict, keys):
|
||||
"""
|
||||
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)
|
||||
|
||||
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
|
||||
# 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
|
||||
|
||||
@export
|
||||
def get_filter_tree(self , show_zero_hits=True, hide_cat=None):
|
||||
@ -509,7 +476,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], basestring):
|
||||
if isinstance(config[key], unicode) or isinstance(config[key], str):
|
||||
config[key] = config[key].encode("utf8")
|
||||
self.config[key] = config[key]
|
||||
|
||||
@ -523,6 +490,24 @@ 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"""
|
||||
@ -590,11 +575,6 @@ 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"""
|
||||
@ -625,31 +605,11 @@ class Core(component.Component):
|
||||
"""Sets the path for the torrent to be moved when completed"""
|
||||
return self.torrentmanager[torrent_id].set_move_completed_path(value)
|
||||
|
||||
@export(AUTH_LEVEL_ADMIN)
|
||||
def set_torrents_owner(self, torrent_ids, username):
|
||||
"""Set's the torrent owner.
|
||||
|
||||
:param torrent_id: the torrent_id of the torrent to remove
|
||||
:type torrent_id: string
|
||||
:param username: the new owner username
|
||||
:type username: string
|
||||
|
||||
:raises DelugeError: if the username is not known
|
||||
"""
|
||||
if not self.authmanager.has_account(username):
|
||||
raise DelugeError("Username \"%s\" is not known." % username)
|
||||
if isinstance(torrent_ids, basestring):
|
||||
torrent_ids = [torrent_ids]
|
||||
for torrent_id in torrent_ids:
|
||||
self.torrentmanager[torrent_id].set_owner(username)
|
||||
return None
|
||||
|
||||
@export
|
||||
def set_torrents_shared(self, torrent_ids, shared):
|
||||
if isinstance(torrent_ids, basestring):
|
||||
torrent_ids = [torrent_ids]
|
||||
for torrent_id in torrent_ids:
|
||||
self.torrentmanager[torrent_id].set_options({"shared": shared})
|
||||
def get_health(self):
|
||||
"""Returns True if we have established incoming connections"""
|
||||
warnings.warn("Use get_session_status().", DeprecationWarning)
|
||||
return self.session.status().has_incoming_connections
|
||||
|
||||
@export
|
||||
def get_path_size(self, path):
|
||||
@ -662,7 +622,7 @@ class Core(component.Component):
|
||||
webseeds, private, created_by, trackers, add_to_session):
|
||||
|
||||
log.debug("creating torrent..")
|
||||
threading.Thread(target=self._create_torrent_thread,
|
||||
threading.Thread(target=_create_torrent_thread,
|
||||
args=(
|
||||
path,
|
||||
tracker,
|
||||
@ -765,8 +725,7 @@ class Core(component.Component):
|
||||
@export
|
||||
def queue_top(self, torrent_ids):
|
||||
log.debug("Attempting to queue %s to top", 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):
|
||||
for torrent_id in torrent_ids:
|
||||
try:
|
||||
# If the queue method returns True, then we should emit a signal
|
||||
if self.torrentmanager.queue_top(torrent_id):
|
||||
@ -777,48 +736,35 @@ 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.
|
||||
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
|
||||
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)
|
||||
|
||||
@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.
|
||||
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
|
||||
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)
|
||||
|
||||
@export
|
||||
def queue_bottom(self, torrent_ids):
|
||||
log.debug("Attempting to queue %s to bottom", 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):
|
||||
for torrent_id in torrent_ids:
|
||||
try:
|
||||
# If the queue method returns True, then we should emit a signal
|
||||
if self.torrentmanager.queue_bottom(torrent_id):
|
||||
@ -841,27 +787,21 @@ 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(), timeout=30)
|
||||
d = getPage("http://deluge-torrent.org/test_port.php?port=%s" % self.get_listen_port())
|
||||
|
||||
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=None):
|
||||
def get_free_space(self, path):
|
||||
"""
|
||||
Returns the number of free bytes at path
|
||||
|
||||
:param path: the path to check free space at, if None, use the default
|
||||
download location
|
||||
:param path: the path to check free space at
|
||||
:type path: string
|
||||
|
||||
:returns: the number of free bytes at path
|
||||
@ -870,12 +810,7 @@ class Core(component.Component):
|
||||
:raises InvalidPathError: if the path is invalid
|
||||
|
||||
"""
|
||||
if not path:
|
||||
path = self.config["download_location"]
|
||||
try:
|
||||
return deluge.common.free_space(path)
|
||||
except InvalidPathError:
|
||||
return 0
|
||||
return deluge.common.free_space(path)
|
||||
|
||||
@export
|
||||
def get_libtorrent_version(self):
|
||||
@ -887,23 +822,3 @@ 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,7 +33,9 @@
|
||||
#
|
||||
|
||||
import os
|
||||
import logging
|
||||
import gettext
|
||||
import locale
|
||||
import pkg_resources
|
||||
from twisted.internet import reactor
|
||||
import twisted.internet.error
|
||||
|
||||
@ -41,19 +43,16 @@ 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:
|
||||
@ -63,8 +62,13 @@ class Daemon(object):
|
||||
|
||||
def process_running(pid):
|
||||
if deluge.common.windows_check():
|
||||
import win32process
|
||||
return pid in win32process.EnumProcesses()
|
||||
# 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()
|
||||
else:
|
||||
# We can just use os.kill on UNIX to test if the process is running
|
||||
try:
|
||||
@ -87,14 +91,26 @@ class Daemon(object):
|
||||
else:
|
||||
# This is a deluged!
|
||||
s.close()
|
||||
raise deluge.error.DaemonRunningError(
|
||||
"There is a deluge daemon running with this config "
|
||||
"directory!"
|
||||
)
|
||||
raise deluge.error.DaemonRunningError("There is a deluge daemon running with this config directory!")
|
||||
|
||||
# Initialize gettext
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, '')
|
||||
if hasattr(locale, "bindtextdomain"):
|
||||
locale.bindtextdomain("deluge", pkg_resources.resource_filename("deluge", "i18n"))
|
||||
if hasattr(locale, "textdomain"):
|
||||
locale.textdomain("deluge")
|
||||
gettext.bindtextdomain("deluge", pkg_resources.resource_filename("deluge", "i18n"))
|
||||
gettext.textdomain("deluge")
|
||||
gettext.install("deluge", pkg_resources.resource_filename("deluge", "i18n"))
|
||||
except Exception, e:
|
||||
log.error("Unable to initialize gettext/locale: %s", e)
|
||||
import __builtin__
|
||||
__builtin__.__dict__["_"] = lambda x: x
|
||||
|
||||
# Twisted catches signals to terminate, so just have it call the shutdown
|
||||
# method.
|
||||
reactor.addSystemEventTrigger("before", "shutdown", self._shutdown)
|
||||
reactor.addSystemEventTrigger("after", "shutdown", self.shutdown)
|
||||
|
||||
# Catch some Windows specific signals
|
||||
if deluge.common.windows_check():
|
||||
@ -104,7 +120,7 @@ class Daemon(object):
|
||||
def win_handler(ctrl_type):
|
||||
log.debug("ctrl_type: %s", ctrl_type)
|
||||
if ctrl_type == CTRL_CLOSE_EVENT or ctrl_type == CTRL_SHUTDOWN_EVENT:
|
||||
self._shutdown()
|
||||
self.__shutdown()
|
||||
return 1
|
||||
SetConsoleCtrlHandler(win_handler)
|
||||
|
||||
@ -117,14 +133,9 @@ 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(listen_interface=listen_interface)
|
||||
self.core = Core()
|
||||
|
||||
port = self.core.config["daemon_port"]
|
||||
if options and options.port:
|
||||
@ -166,16 +177,26 @@ class Daemon(object):
|
||||
reactor.callLater(0, reactor.stop)
|
||||
|
||||
def _shutdown(self, *args, **kwargs):
|
||||
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!")
|
||||
try:
|
||||
os.remove(deluge.configmanager.get_config_dir("deluged.pid"))
|
||||
except Exception, e:
|
||||
log.exception(e)
|
||||
log.error("Error removing deluged.pid!")
|
||||
|
||||
log.info("Waiting for components to shutdown..")
|
||||
d = component.shutdown()
|
||||
return d
|
||||
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()
|
||||
|
||||
@export()
|
||||
def get_method_list(self):
|
||||
@ -183,18 +204,3 @@ 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,10 +33,8 @@
|
||||
#
|
||||
#
|
||||
|
||||
import logging
|
||||
import deluge.component as component
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
from deluge.log import LOG as log
|
||||
|
||||
class EventManager(component.Component):
|
||||
def __init__(self):
|
||||
@ -55,10 +53,7 @@ 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)
|
||||
try:
|
||||
handler(*event.args)
|
||||
except Exception, e:
|
||||
log.error("Event handler %s failed in %s with exception %s", event.name, handler, e)
|
||||
handler(*event.args)
|
||||
|
||||
def register_event_handler(self, event, handler):
|
||||
"""
|
||||
|
@ -33,13 +33,12 @@
|
||||
#
|
||||
#
|
||||
|
||||
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.
|
||||
@ -78,27 +77,6 @@ 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")
|
||||
@ -113,8 +91,9 @@ def tracker_error_filter(torrent_ids, values):
|
||||
# Check all the torrent's tracker_status for 'Error:' and only return torrent_ids
|
||||
# that have this substring in their tracker_status
|
||||
for torrent_id in torrent_ids:
|
||||
if _("Error") + ":" in tm[torrent_id].get_status(["tracker_host"])["tracker_host"]:
|
||||
if "Error:" in tm[torrent_id].get_status(["tracker_status"])["tracker_status"]:
|
||||
filtered_torrent_ids.append(torrent_id)
|
||||
|
||||
return filtered_torrent_ids
|
||||
|
||||
class FilterManager(component.Component):
|
||||
@ -128,7 +107,6 @@ 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)
|
||||
@ -138,10 +116,6 @@ 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.
|
||||
@ -152,11 +126,12 @@ class FilterManager(component.Component):
|
||||
|
||||
#sanitize input: filter-value must be a list of strings
|
||||
for key, value in filter_dict.items():
|
||||
if isinstance(value, basestring):
|
||||
filter_dict[key] = [value]
|
||||
if isinstance(value, str):
|
||||
filter_dict[key] = [value]
|
||||
|
||||
|
||||
if "id"in filter_dict: #optimized filter for id:
|
||||
torrent_ids = list(filter_dict["id"])
|
||||
torrent_ids = filter_dict["id"]
|
||||
del filter_dict["id"]
|
||||
else:
|
||||
torrent_ids = self.torrents.get_torrent_list()
|
||||
@ -191,11 +166,13 @@ 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 = self.torrents[torrent_id].get_status(filter_dict.keys()) #status={key:value}
|
||||
status = status_func(torrent_id, filter_dict.keys()) #status={key:value}
|
||||
for field, values in filter_dict.iteritems():
|
||||
if (not status[field] in values) and torrent_id in torrent_ids:
|
||||
torrent_ids.remove(torrent_id)
|
||||
|
||||
return torrent_ids
|
||||
|
||||
def get_filter_tree(self, show_zero_hits=True, hide_cat=None):
|
||||
@ -204,22 +181,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)
|
||||
|
||||
torrent_keys, plugin_keys = self.torrents.separate_keys(tree_keys, torrent_ids)
|
||||
items = dict((field, self.tree_fields[field]()) for field in tree_keys)
|
||||
items = dict( (field, self.tree_fields[field]()) for field in tree_keys)
|
||||
|
||||
#count status fields.
|
||||
for torrent_id in list(torrent_ids):
|
||||
status = self.core.create_torrent_status(torrent_id, torrent_keys, plugin_keys) #status={key:value}
|
||||
status = status_func(torrent_id, tree_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:
|
||||
@ -260,8 +237,9 @@ 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 = self.torrents[torrent_id].get_status(["download_payload_rate", "upload_payload_rate"])
|
||||
status = get_status(torrent_id, ["download_payload_rate", "upload_payload_rate"])
|
||||
if status["download_payload_rate"] or status["upload_payload_rate"]:
|
||||
pass #ok
|
||||
else:
|
||||
|
@ -39,14 +39,12 @@ 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
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
from deluge.log import LOG as log
|
||||
|
||||
#start : http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/286203
|
||||
def makeFakeClass(module, name):
|
||||
|
@ -36,13 +36,13 @@
|
||||
|
||||
"""PluginManager for Core"""
|
||||
|
||||
import logging
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet.task import LoopingCall
|
||||
|
||||
from deluge.event import PluginEnabledEvent, PluginDisabledEvent
|
||||
import deluge.pluginmanagerbase
|
||||
import deluge.component as component
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
from deluge.log import LOG as log
|
||||
|
||||
class PluginManager(deluge.pluginmanagerbase.PluginManagerBase,
|
||||
component.Component):
|
||||
@ -96,7 +96,8 @@ class PluginManager(deluge.pluginmanagerbase.PluginManagerBase,
|
||||
try:
|
||||
status[field] = self.status_fields[field](torrent_id)
|
||||
except KeyError:
|
||||
pass
|
||||
log.warning("Status field %s is not registered with the\
|
||||
PluginManager.", field)
|
||||
return status
|
||||
|
||||
def register_status_field(self, field, function):
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# preferencesmanager.py
|
||||
#
|
||||
# Copyright (C) 2008-2010 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
@ -34,9 +34,10 @@
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
import logging
|
||||
import os.path
|
||||
import threading
|
||||
import pkg_resources
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet.task import LoopingCall
|
||||
|
||||
from deluge._libtorrent import lt
|
||||
@ -45,8 +46,7 @@ from deluge.event import *
|
||||
import deluge.configmanager
|
||||
import deluge.common
|
||||
import deluge.component as component
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
from deluge.log import LOG as log
|
||||
|
||||
DEFAULT_PREFS = {
|
||||
"send_info": False,
|
||||
@ -58,11 +58,9 @@ 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,
|
||||
@ -86,6 +84,8 @@ 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,9 +139,7 @@ DEFAULT_PREFS = {
|
||||
"rate_limit_ip_overhead": True,
|
||||
"geoip_db_location": "/usr/share/GeoIP/GeoIP.dat",
|
||||
"cache_size": 512,
|
||||
"cache_expiry": 60,
|
||||
"auto_manage_prefer_seeds": False,
|
||||
"shared": False
|
||||
"cache_expiry": 60
|
||||
}
|
||||
|
||||
class PreferencesManager(component.Component):
|
||||
@ -149,40 +147,92 @@ 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.new_release_timer = None
|
||||
self.settings = component.get("Core").settings
|
||||
|
||||
# Set the initial preferences on start-up
|
||||
for key in DEFAULT_PREFS:
|
||||
self.do_config_set_func(key, self.config[key])
|
||||
# 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)
|
||||
|
||||
self.config.register_change_callback(self._on_config_value_change)
|
||||
|
||||
def stop(self):
|
||||
if self.new_release_timer and self.new_release_timer.running:
|
||||
if self.new_release_timer:
|
||||
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):
|
||||
@ -196,9 +246,7 @@ class PreferencesManager(component.Component):
|
||||
# Only set the listen ports if random_port is not true
|
||||
if self.config["random_port"] is not True:
|
||||
log.debug("listen port range set to %s-%s", value[0], value[1])
|
||||
self.session.listen_on(
|
||||
value[0], value[1], str(self.config["listen_interface"])
|
||||
)
|
||||
self.session.listen_on(value[0], value[1], str(self.config["listen_interface"]))
|
||||
|
||||
def _on_set_listen_interface(self, key, value):
|
||||
# Call the random_port callback since it'll do what we need
|
||||
@ -220,15 +268,13 @@ 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.session_set_setting("outgoing_ports", (value[0], value[1]))
|
||||
self.settings.outgoing_ports = value[0], value[1]
|
||||
self.session.set_settings(self.settings)
|
||||
|
||||
def _on_set_random_outgoing_ports(self, key, value):
|
||||
if value:
|
||||
@ -237,11 +283,13 @@ class PreferencesManager(component.Component):
|
||||
def _on_set_peer_tos(self, key, value):
|
||||
log.debug("setting peer_tos to: %s", value)
|
||||
try:
|
||||
self.session_set_setting("peer_tos", chr(int(value, 16)))
|
||||
self.settings.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")
|
||||
@ -288,22 +336,7 @@ class PreferencesManager(component.Component):
|
||||
def _on_set_utpex(self, key, value):
|
||||
log.debug("utpex value set to %s", value)
|
||||
if value:
|
||||
# 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)
|
||||
self.session.add_extension(lt.create_ut_pex_plugin)
|
||||
|
||||
def _on_set_encryption(self, key, value):
|
||||
log.debug("encryption value %s set to %s..", key, value)
|
||||
@ -353,41 +386,53 @@ class PreferencesManager(component.Component):
|
||||
self.session.set_max_half_open_connections(value)
|
||||
|
||||
def _on_set_max_connections_per_second(self, key, value):
|
||||
self.session_set_setting("connection_speed", value)
|
||||
self.settings.connection_speed = 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_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_share_ratio_limit(self, key, value):
|
||||
log.debug("%s set to %s..", key, value)
|
||||
self.session_set_setting("share_ratio_limit", value)
|
||||
self.settings.share_ratio_limit = value
|
||||
self.session.set_settings(self.settings)
|
||||
|
||||
def _on_set_seed_time_ratio_limit(self, key, value):
|
||||
log.debug("%s set to %s..", key, value)
|
||||
self.session_set_setting("seed_time_ratio_limit", value)
|
||||
self.settings.seed_time_ratio_limit = value
|
||||
self.session.set_settings(self.settings)
|
||||
|
||||
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.session_set_setting("seed_time_limit", int(value * 60))
|
||||
self.settings.seed_time_limit = int(value * 60)
|
||||
self.session.set_settings(self.settings)
|
||||
|
||||
def _on_set_max_active_downloading(self, key, value):
|
||||
log.debug("%s set to %s..", key, value)
|
||||
self.session_set_setting("active_downloads", value)
|
||||
log.debug("active_downloads: %s", self.settings.active_downloads)
|
||||
self.settings.active_downloads = value
|
||||
self.session.set_settings(self.settings)
|
||||
|
||||
def _on_set_max_active_seeding(self, key, value):
|
||||
log.debug("%s set to %s..", key, value)
|
||||
self.session_set_setting("active_seeds", value)
|
||||
log.debug("active_seeds: %s", self.settings.active_seeds)
|
||||
self.settings.active_seeds = value
|
||||
self.session.set_settings(self.settings)
|
||||
|
||||
def _on_set_max_active_limit(self, key, value):
|
||||
log.debug("%s set to %s..", key, value)
|
||||
self.session_set_setting("active_limit", value)
|
||||
log.debug("active_limit: %s", self.settings.active_limit)
|
||||
self.settings.active_limit = value
|
||||
self.session.set_settings(self.settings)
|
||||
|
||||
def _on_set_dont_count_slow_torrents(self, key, value):
|
||||
log.debug("%s set to %s..", key, value)
|
||||
self.session_set_setting("dont_count_slow_torrents", value)
|
||||
self.settings.dont_count_slow_torrents = value
|
||||
self.session.set_settings(self.settings)
|
||||
|
||||
def _on_set_send_info(self, key, value):
|
||||
def _on_send_info(self, key, value):
|
||||
log.debug("Sending anonymous stats..")
|
||||
"""sends anonymous stats home"""
|
||||
class Send_Info_Thread(threading.Thread):
|
||||
@ -417,18 +462,18 @@ class PreferencesManager(component.Component):
|
||||
if value:
|
||||
Send_Info_Thread(self.config).start()
|
||||
|
||||
def _on_set_new_release_check(self, key, value):
|
||||
def _on_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 and self.new_release_timer.running:
|
||||
if self.new_release_timer:
|
||||
self.new_release_timer.stop()
|
||||
# Set a timer to check for a new release every 3 days
|
||||
self.new_release_timer = LoopingCall(
|
||||
self._on_set_new_release_check, "new_release_check", True)
|
||||
self._on_new_release_check, "new_release_check", True)
|
||||
self.new_release_timer.start(72 * 60 * 60, False)
|
||||
else:
|
||||
if self.new_release_timer and self.new_release_timer.running:
|
||||
if self.new_release_timer:
|
||||
self.new_release_timer.stop()
|
||||
|
||||
def _on_set_proxies(self, key, value):
|
||||
@ -443,20 +488,19 @@ class PreferencesManager(component.Component):
|
||||
log.debug("setting %s proxy settings", k)
|
||||
getattr(self.session, "set_%s_proxy" % k)(proxy_settings)
|
||||
|
||||
def _on_set_rate_limit_ip_overhead(self, key, value):
|
||||
def _on_rate_limit_ip_overhead(self, key, value):
|
||||
log.debug("%s: %s", key, value)
|
||||
self.session_set_setting("rate_limit_ip_overhead", value)
|
||||
self.settings.rate_limit_ip_overhead = value
|
||||
self.session.set_settings(self.settings)
|
||||
|
||||
def _on_set_geoip_db_location(self, key, value):
|
||||
def _on_geoip_db_location(self, key, value):
|
||||
log.debug("%s: %s", key, value)
|
||||
# Load the GeoIP DB for country look-ups if available
|
||||
geoip_db = ""
|
||||
if os.path.exists(value):
|
||||
geoip_db = value
|
||||
elif os.path.exists(deluge.common.resource_filename("deluge", os.path.join("data", "GeoIP.dat"))):
|
||||
geoip_db = deluge.common.resource_filename(
|
||||
"deluge", os.path.join("data", "GeoIP.dat")
|
||||
)
|
||||
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"))
|
||||
else:
|
||||
log.warning("Unable to find GeoIP database file!")
|
||||
|
||||
@ -467,14 +511,12 @@ class PreferencesManager(component.Component):
|
||||
log.error("Unable to load geoip database!")
|
||||
log.exception(e)
|
||||
|
||||
def _on_set_cache_size(self, key, value):
|
||||
def _on_cache_size(self, key, value):
|
||||
log.debug("%s: %s", key, value)
|
||||
self.session_set_setting("cache_size", value)
|
||||
self.settings.cache_size = value
|
||||
self.session.set_settings(self.settings)
|
||||
|
||||
def _on_set_cache_expiry(self, key, value):
|
||||
def _on_cache_expiry(self, key, value):
|
||||
log.debug("%s: %s", key, value)
|
||||
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)
|
||||
self.settings.cache_expiry = value
|
||||
self.session.set_settings(self.settings)
|
||||
|
@ -39,35 +39,25 @@ import sys
|
||||
import zlib
|
||||
import os
|
||||
import stat
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
from twisted.internet.protocol import Factory, Protocol
|
||||
from twisted.internet import reactor, defer
|
||||
from twisted.internet import ssl, reactor, defer
|
||||
|
||||
from OpenSSL import crypto, SSL
|
||||
from types import FunctionType
|
||||
|
||||
try:
|
||||
import rencode
|
||||
except ImportError:
|
||||
import deluge.rencode as rencode
|
||||
import deluge.rencode as rencode
|
||||
from deluge.log import LOG as log
|
||||
|
||||
import deluge.component as component
|
||||
import deluge.configmanager
|
||||
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
|
||||
from deluge.core.authmanager import AUTH_LEVEL_NONE, AUTH_LEVEL_DEFAULT
|
||||
|
||||
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
|
||||
@ -96,31 +86,11 @@ def export(auth_level=AUTH_LEVEL_DEFAULT):
|
||||
else:
|
||||
return wrap
|
||||
|
||||
class DelugeError(Exception):
|
||||
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 NotAuthorizedError(DelugeError):
|
||||
pass
|
||||
|
||||
class ServerContextFactory(object):
|
||||
def getContext(self):
|
||||
@ -136,34 +106,70 @@ class ServerContextFactory(object):
|
||||
ctx.use_privatekey_file(os.path.join(ssl_dir, "daemon.pkey"))
|
||||
return ctx
|
||||
|
||||
class DelugeRPCProtocol(DelugeTransferProtocol):
|
||||
class DelugeRPCProtocol(Protocol):
|
||||
__buffer = None
|
||||
|
||||
def message_received(self, request):
|
||||
def dataReceived(self, data):
|
||||
"""
|
||||
This method is called whenever a message is received from a client. The
|
||||
This method is called whenever data is received from a client. The
|
||||
only message that a client sends to the server is a RPC Request message.
|
||||
If the RPC Request message is valid, then the method is called in
|
||||
:meth:`dispatch`.
|
||||
|
||||
:param request: the request from the client.
|
||||
:type data: tuple
|
||||
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 type(request) is not tuple:
|
||||
log.debug("Received invalid message: type is not tuple")
|
||||
return
|
||||
if self.__buffer:
|
||||
# We have some data from the last dataReceived() so lets prepend it
|
||||
data = self.__buffer + data
|
||||
self.__buffer = None
|
||||
|
||||
if len(request) < 1:
|
||||
log.debug("Received invalid message: there are no items")
|
||||
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
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
def sendData(self, data):
|
||||
"""
|
||||
@ -171,18 +177,16 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
|
||||
|
||||
: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.transfer_message(data)
|
||||
self.transport.write(zlib.compress(rencode.dumps(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
|
||||
|
||||
@ -204,9 +208,6 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
|
||||
|
||||
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
|
||||
@ -228,55 +229,33 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
|
||||
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()
|
||||
|
||||
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":
|
||||
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":
|
||||
# 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, args[0])
|
||||
self.factory.authorized_sessions[self.transport.sessionno] = ret
|
||||
self.factory.session_protocols[self.transport.sessionno] = self
|
||||
except Exception, e:
|
||||
sendError()
|
||||
if not isinstance(e, _ClientSideRecreateError):
|
||||
log.exception(e)
|
||||
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.valid_session():
|
||||
log.debug("RPC dispatch daemon.set_event_interest")
|
||||
elif method == "daemon.set_event_interest" and self.transport.sessionno in self.factory.authorized_sessions:
|
||||
# This special case is to allow clients to set which events they are
|
||||
# interested in receiving.
|
||||
# We are expecting a sequence from the client.
|
||||
@ -291,24 +270,18 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
|
||||
finally:
|
||||
return
|
||||
|
||||
if method in self.factory.methods and self.valid_session():
|
||||
log.debug("RPC dispatch %s", method)
|
||||
if method in self.factory.methods and self.transport.sessionno in self.factory.authorized_sessions:
|
||||
try:
|
||||
method_auth_requirement = self.factory.methods[method]._rpcserver_auth_level
|
||||
auth_level = self.factory.authorized_sessions[self.transport.sessionno][0]
|
||||
auth_level = self.factory.authorized_sessions[self.transport.sessionno]
|
||||
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, 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
|
||||
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))
|
||||
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:
|
||||
@ -351,8 +324,6 @@ 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
|
||||
@ -362,7 +333,6 @@ class RPCServer(component.Component):
|
||||
# Holds the interested event list for the sessions
|
||||
self.factory.interested_events = {}
|
||||
|
||||
self.listen = listen
|
||||
if not listen:
|
||||
return
|
||||
|
||||
@ -406,17 +376,6 @@ 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.
|
||||
@ -440,66 +399,6 @@ 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.
|
||||
@ -509,7 +408,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.items():
|
||||
for session_id, interest in self.factory.interested_events.iteritems():
|
||||
if event.name in interest:
|
||||
log.debug("Emit Event: %s %s", event.name, event.args)
|
||||
# This session is interested so send a RPC_EVENT
|
||||
@ -517,29 +416,6 @@ 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
|
||||
@ -583,12 +459,8 @@ 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)
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 722 B After Width: | Height: | Size: 722 B |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 742 B After Width: | Height: | Size: 742 B |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 590 B After Width: | Height: | Size: 590 B |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 844 B After Width: | Height: | Size: 844 B |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 699 B After Width: | Height: | Size: 699 B |
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 722 B After Width: | Height: | Size: 722 B |
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 793 B After Width: | Height: | Size: 793 B |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 686 B After Width: | Height: | Size: 686 B |
2
deluge/data/pixmaps/flags/FLAGS_LICENCE
Normal file
@ -0,0 +1,2 @@
|
||||
From: http://famfamfam.com/lab/icons/flags/
|
||||
"These flag icons are available for free use for any purpose with no requirement for attribution."
|
Before Width: | Height: | Size: 643 B After Width: | Height: | Size: 643 B |
Before Width: | Height: | Size: 408 B After Width: | Height: | Size: 408 B |
Before Width: | Height: | Size: 604 B After Width: | Height: | Size: 604 B |
Before Width: | Height: | Size: 591 B After Width: | Height: | Size: 591 B |
Before Width: | Height: | Size: 643 B After Width: | Height: | Size: 643 B |
Before Width: | Height: | Size: 600 B After Width: | Height: | Size: 600 B |
Before Width: | Height: | Size: 497 B After Width: | Height: | Size: 497 B |
Before Width: | Height: | Size: 488 B After Width: | Height: | Size: 488 B |
Before Width: | Height: | Size: 428 B After Width: | Height: | Size: 428 B |
Before Width: | Height: | Size: 836 B After Width: | Height: | Size: 836 B |
Before Width: | Height: | Size: 506 B After Width: | Height: | Size: 506 B |
Before Width: | Height: | Size: 647 B After Width: | Height: | Size: 647 B |
Before Width: | Height: | Size: 403 B After Width: | Height: | Size: 403 B |
Before Width: | Height: | Size: 673 B After Width: | Height: | Size: 673 B |
Before Width: | Height: | Size: 524 B After Width: | Height: | Size: 524 B |
Before Width: | Height: | Size: 663 B After Width: | Height: | Size: 663 B |
Before Width: | Height: | Size: 589 B After Width: | Height: | Size: 589 B |
Before Width: | Height: | Size: 593 B After Width: | Height: | Size: 593 B |
Before Width: | Height: | Size: 585 B After Width: | Height: | Size: 585 B |
Before Width: | Height: | Size: 504 B After Width: | Height: | Size: 504 B |
Before Width: | Height: | Size: 449 B After Width: | Height: | Size: 449 B |
Before Width: | Height: | Size: 497 B After Width: | Height: | Size: 497 B |
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 462 B |
Before Width: | Height: | Size: 457 B After Width: | Height: | Size: 457 B |
Before Width: | Height: | Size: 675 B After Width: | Height: | Size: 675 B |
Before Width: | Height: | Size: 486 B After Width: | Height: | Size: 486 B |
Before Width: | Height: | Size: 611 B After Width: | Height: | Size: 611 B |
Before Width: | Height: | Size: 639 B After Width: | Height: | Size: 639 B |
Before Width: | Height: | Size: 500 B After Width: | Height: | Size: 500 B |
Before Width: | Height: | Size: 593 B After Width: | Height: | Size: 593 B |
Before Width: | Height: | Size: 526 B After Width: | Height: | Size: 526 B |
Before Width: | Height: | Size: 631 B After Width: | Height: | Size: 631 B |
Before Width: | Height: | Size: 512 B After Width: | Height: | Size: 512 B |
Before Width: | Height: | Size: 443 B After Width: | Height: | Size: 443 B |
Before Width: | Height: | Size: 514 B After Width: | Height: | Size: 514 B |
Before Width: | Height: | Size: 600 B After Width: | Height: | Size: 600 B |
Before Width: | Height: | Size: 628 B After Width: | Height: | Size: 628 B |
Before Width: | Height: | Size: 625 B After Width: | Height: | Size: 625 B |
Before Width: | Height: | Size: 528 B After Width: | Height: | Size: 528 B |