Merge branch 'master' into plugins-namespace
30
ChangeLog
@ -1,6 +1,12 @@
|
||||
=== Deluge 1.3.0 (In Development) ===
|
||||
* Improved Logging
|
||||
* Enforced the use of the "deluge.plugins" namespace to reduce package names clashing beetween regular packages and deluge plugins.
|
||||
* Removed the AutoAdd feature on the core. It's now handled with the AutoAdd
|
||||
plugin, which is also shipped with Deluge, and it does a better job and
|
||||
now, it even supports multiple users perfectly.
|
||||
* Authentication/Permission exceptions are now sent to clients and recreated
|
||||
there to allow acting upon them.
|
||||
* Enforced the use of the "deluge.plugins" namespace to reduce package
|
||||
names clashing beetween regular packages and deluge plugins.
|
||||
|
||||
==== Core ====
|
||||
* Implement #1063 option to delete torrent file copy on torrent removal - patch from Ghent
|
||||
@ -10,11 +16,29 @@
|
||||
* #1112: Fix renaming files in add torrent dialog
|
||||
* #1247: Fix deluge-gtk from hanging on shutdown
|
||||
* #995: Rewrote tracker_icons
|
||||
* 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.
|
||||
* Make the distinction between adding to the session new unmanaged torrents
|
||||
and torrents loaded from state. This will break backwards compatability.
|
||||
* Pass a copy of an event instead of passing the event arguments to the
|
||||
event handlers. This will break backwards compatability.
|
||||
* Allow changing ownership of torrents.
|
||||
* File modifications on the auth file are now detected and when they happen,
|
||||
the file is reloaded. Upon finding an old auth file with an old format, an
|
||||
upgrade to the new format is made, file saved, and reloaded.
|
||||
* Authentication no longer requires a username/password. If one or both of
|
||||
these is missing, an authentication error will be sent to the client
|
||||
which sould then ask the username/password to the user.
|
||||
* Implemented sequential downloads.
|
||||
* #378: Provide information about a torrent's pieces states
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix uncaught exception when closing deluge in classic mode
|
||||
* Allow changing ownership of torrents
|
||||
* Host entries in the Connection Manager UI are now editable. They're
|
||||
now also migrated from the old format were automatic localhost logins were
|
||||
possible, which no longer is, this fixes #1814.
|
||||
* Implemented sequential downloads UI handling.
|
||||
* #378: Allow showing a pieces bar instead of a regular progress bar in a
|
||||
torrent's status tab.
|
||||
|
||||
==== WebUI ====
|
||||
* Migrate to ExtJS 3.1
|
||||
|
||||
4
README
@ -15,9 +15,9 @@ For past developers and contributers see: http://dev.deluge-torrent.org/wiki/Abo
|
||||
License
|
||||
==========================
|
||||
Deluge is under the GNU GPLv3 license.
|
||||
Icon data/pixmaps/deluge.svg and derivatives in data/icons are copyright
|
||||
Icon ui/data/pixmaps/deluge.svg and derivatives in ui/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
|
||||
All other icons in ui/data/pixmaps are copyright Andrew Resch and are under
|
||||
the GNU GPLv3.
|
||||
|
||||
==========================
|
||||
|
||||
1
create_potfiles_in.py
Normal file → Executable file
@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
|
||||
# Paths to exclude
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
for size in 16 22 24 32 36 48 64 72 96 128 192 256; do mkdir -p deluge/data/\
|
||||
for size in 16 22 24 32 36 48 64 72 96 128 192 256; do mkdir -p deluge/ui/data/\
|
||||
icons/hicolor/${size}x${size}/apps; rsvg-convert -w ${size} -h ${size} \
|
||||
-o deluge/data/icons/hicolor/${size}x${size}/apps/deluge.png deluge/data/pixmaps\
|
||||
/deluge.svg; mkdir -p deluge/data/icons/scalable/apps/; cp deluge/data/pixmaps/\
|
||||
deluge.svg deluge/data/icons/scalable/apps/deluge.svg; done
|
||||
-o deluge/ui/data/icons/hicolor/${size}x${size}/apps/deluge.png deluge/ui/data/pixmaps\
|
||||
/deluge.svg; mkdir -p deluge/ui/data/icons/scalable/apps/; cp deluge/ui/data/pixmaps/\
|
||||
deluge.svg deluge/ui/data/icons/scalable/apps/deluge.svg; done
|
||||
|
||||
@ -40,7 +40,6 @@ import os
|
||||
import time
|
||||
import subprocess
|
||||
import platform
|
||||
import sys
|
||||
import chardet
|
||||
import logging
|
||||
|
||||
@ -167,6 +166,18 @@ def get_default_download_dir():
|
||||
if windows_check():
|
||||
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():
|
||||
@ -201,7 +212,7 @@ def osx_check():
|
||||
|
||||
def get_pixmap(fname):
|
||||
"""
|
||||
Provides easy access to files in the deluge/data/pixmaps folder within the Deluge egg
|
||||
Provides easy access to files in the deluge/ui/data/pixmaps folder within the Deluge egg
|
||||
|
||||
:param fname: the filename to look for
|
||||
:type fname: string
|
||||
@ -209,7 +220,7 @@ def get_pixmap(fname):
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
return pkg_resources.resource_filename("deluge", os.path.join("data", \
|
||||
return pkg_resources.resource_filename("deluge", os.path.join("ui/data", \
|
||||
"pixmaps", fname))
|
||||
|
||||
def open_file(path):
|
||||
@ -593,7 +604,7 @@ def utf8_encoded(s):
|
||||
|
||||
"""
|
||||
if isinstance(s, str):
|
||||
s = decode_string(s, locale.getpreferredencoding())
|
||||
s = decode_string(s)
|
||||
elif isinstance(s, unicode):
|
||||
s = s.encode("utf8", "ignore")
|
||||
return s
|
||||
@ -632,3 +643,43 @@ class VersionSplit(object):
|
||||
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()
|
||||
|
||||
@ -98,6 +98,9 @@ class Component(object):
|
||||
self._component_stopping_deferred = None
|
||||
_ComponentRegistry.register(self)
|
||||
|
||||
def __del__(self):
|
||||
_ComponentRegistry.deregister(self._component_name)
|
||||
|
||||
def _component_start_timer(self):
|
||||
if hasattr(self, "update"):
|
||||
self._component_timer = LoopingCall(self.update)
|
||||
@ -141,11 +144,18 @@ class Component(object):
|
||||
self._component_timer.stop()
|
||||
return True
|
||||
|
||||
def on_stop_fail(result):
|
||||
self._component_state = "Started"
|
||||
self._component_stopping_deferred = None
|
||||
log.error(result)
|
||||
return result
|
||||
|
||||
if self._component_state != "Stopped" and self._component_state != "Stopping":
|
||||
if hasattr(self, "stop"):
|
||||
self._component_state = "Stopping"
|
||||
d = maybeDeferred(self.stop)
|
||||
d.addCallback(on_stop)
|
||||
d.addErrback(on_stop_fail)
|
||||
self._component_stopping_deferred = d
|
||||
else:
|
||||
d = maybeDeferred(on_stop, None)
|
||||
|
||||
@ -268,6 +268,31 @@ what is currently in the config and it could not convert the value
|
||||
else:
|
||||
return self.__config[key]
|
||||
|
||||
def __delitem__(self, key):
|
||||
"""
|
||||
See
|
||||
:meth:`del_item`
|
||||
"""
|
||||
self.del_item(key)
|
||||
|
||||
def del_item(self, key):
|
||||
"""
|
||||
Deletes item with a specific key from the configuration.
|
||||
|
||||
:param key: the item which you wish to delete.
|
||||
:raises KeyError: if 'key' is not in the config dictionary
|
||||
|
||||
**Usage**
|
||||
>>> config = Config("test.conf", defaults={"test": 5})
|
||||
>>> del config["test"]
|
||||
"""
|
||||
del self.__config[key]
|
||||
# We set the save_timer for 5 seconds if not already set
|
||||
from twisted.internet import reactor
|
||||
if not self._save_timer or not self._save_timer.active():
|
||||
self._save_timer = reactor.callLater(5, self.save)
|
||||
|
||||
|
||||
def register_change_callback(self, callback):
|
||||
"""
|
||||
Registers a callback function that will be called when a value is changed in the config dictionary
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
# authmanager.py
|
||||
#
|
||||
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
@ -36,28 +37,56 @@
|
||||
import os
|
||||
import random
|
||||
import stat
|
||||
import shutil
|
||||
import logging
|
||||
|
||||
import deluge.component as component
|
||||
import deluge.configmanager as configmanager
|
||||
import deluge.error
|
||||
from deluge.common import (AUTH_LEVEL_ADMIN, AUTH_LEVEL_NONE, AUTH_LEVEL_NORMAL,
|
||||
AUTH_LEVEL_READONLY, AUTH_LEVEL_DEFAULT,
|
||||
create_localclient_account)
|
||||
|
||||
from deluge.error import AuthManagerError, AuthenticationRequired, BadLoginError
|
||||
|
||||
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_LEVEL_DEFAULT = AUTH_LEVEL_NORMAL
|
||||
AUTH_LEVELS_MAPPING_REVERSE = {}
|
||||
for key, value in AUTH_LEVELS_MAPPING.iteritems():
|
||||
AUTH_LEVELS_MAPPING_REVERSE[value] = key
|
||||
|
||||
class Account(object):
|
||||
__slots__ = ('username', 'password', 'authlevel')
|
||||
def __init__(self, username, password, authlevel):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.authlevel = authlevel
|
||||
|
||||
def data(self):
|
||||
return {
|
||||
'username': self.username,
|
||||
'password': self.password,
|
||||
'authlevel': AUTH_LEVELS_MAPPING_REVERSE[self.authlevel],
|
||||
'authlevel_int': self.authlevel
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return ('<Account username="%(username)s" authlevel=%(authlevel)s>' %
|
||||
self.__dict__)
|
||||
|
||||
class BadLoginError(deluge.error.DelugeError):
|
||||
pass
|
||||
|
||||
class AuthManager(component.Component):
|
||||
def __init__(self):
|
||||
component.Component.__init__(self, "AuthManager")
|
||||
component.Component.__init__(self, "AuthManager", interval=10)
|
||||
self.__auth = {}
|
||||
self.__auth_modification_time = None
|
||||
|
||||
def start(self):
|
||||
self.__load_auth_file()
|
||||
@ -68,6 +97,19 @@ class AuthManager(component.Component):
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def update(self):
|
||||
auth_file = configmanager.get_config_dir("auth")
|
||||
# Check for auth file and create if necessary
|
||||
if not os.path.exists(auth_file):
|
||||
log.info("Authfile not found, recreating it.")
|
||||
self.__load_auth_file()
|
||||
return
|
||||
|
||||
auth_file_modification_time = os.stat(auth_file).st_mtime
|
||||
if self.__auth_modification_time != auth_file_modification_time:
|
||||
log.info("Auth file changed, reloading it!")
|
||||
self.__load_auth_file()
|
||||
|
||||
def authorize(self, username, password):
|
||||
"""
|
||||
Authorizes users based on username and password
|
||||
@ -77,49 +119,121 @@ class AuthManager(component.Component):
|
||||
:returns: int, the auth level for this user
|
||||
:rtype: int
|
||||
|
||||
:raises BadLoginError: if the username does not exist or password does not match
|
||||
:raises AuthenticationRequired: if aditional details are required to
|
||||
authenticate.
|
||||
:raises BadLoginError: if the username does not exist or password does
|
||||
not match.
|
||||
|
||||
"""
|
||||
if not username:
|
||||
raise AuthenticationRequired(
|
||||
"Username and Password are required.", username
|
||||
)
|
||||
|
||||
if username not in self.__auth:
|
||||
# Let's try to re-load the file.. Maybe it's been updated
|
||||
self.__load_auth_file()
|
||||
if username not in self.__auth:
|
||||
raise BadLoginError("Username does not exist")
|
||||
raise BadLoginError("Username does not exist", username)
|
||||
|
||||
if self.__auth[username][0] == password:
|
||||
if self.__auth[username].password == password:
|
||||
# Return the users auth level
|
||||
return int(self.__auth[username][1])
|
||||
return self.__auth[username].authlevel
|
||||
elif not password and self.__auth[username].password:
|
||||
raise AuthenticationRequired("Password is required", username)
|
||||
else:
|
||||
raise BadLoginError("Password does not match")
|
||||
raise BadLoginError("Password does not match", username)
|
||||
|
||||
def __create_localclient_account(self):
|
||||
def has_account(self, username):
|
||||
return username in self.__auth
|
||||
|
||||
def get_known_accounts(self):
|
||||
"""
|
||||
Returns the string.
|
||||
Returns a list of known deluge usernames.
|
||||
"""
|
||||
# We create a 'localclient' account with a random password
|
||||
self.__load_auth_file()
|
||||
return [account.data() for account in self.__auth.values()]
|
||||
|
||||
def create_account(self, username, password, authlevel):
|
||||
if username in self.__auth:
|
||||
raise AuthManagerError("Username in use.", username)
|
||||
try:
|
||||
from hashlib import sha1 as sha_hash
|
||||
except ImportError:
|
||||
from sha import new as sha_hash
|
||||
return "localclient:" + sha_hash(str(random.random())).hexdigest() + ":" + str(AUTH_LEVEL_ADMIN) + "\n"
|
||||
self.__auth[username] = Account(username, password,
|
||||
AUTH_LEVELS_MAPPING[authlevel])
|
||||
self.write_auth_file()
|
||||
return True
|
||||
except Exception, err:
|
||||
log.exception(err)
|
||||
raise err
|
||||
|
||||
def __load_auth_file(self):
|
||||
auth_file = configmanager.get_config_dir("auth")
|
||||
# Check for auth file and create if necessary
|
||||
if not os.path.exists(auth_file):
|
||||
localclient = self.__create_localclient_account()
|
||||
fd = open(auth_file, "w")
|
||||
fd.write(localclient)
|
||||
def update_account(self, username, password, authlevel):
|
||||
if username not in self.__auth:
|
||||
raise AuthManagerError("Username not known", username)
|
||||
try:
|
||||
self.__auth[username].username = username
|
||||
self.__auth[username].password = password
|
||||
self.__auth[username].authlevel = AUTH_LEVELS_MAPPING[authlevel]
|
||||
self.write_auth_file()
|
||||
return True
|
||||
except Exception, err:
|
||||
log.exception(err)
|
||||
raise err
|
||||
|
||||
def remove_account(self, username):
|
||||
if username not in self.__auth:
|
||||
raise AuthManagerError("Username not known", username)
|
||||
elif username == component.get("RPCServer").get_session_user():
|
||||
raise AuthManagerError(
|
||||
"You cannot delete your own account while logged in!", username
|
||||
)
|
||||
|
||||
del self.__auth[username]
|
||||
self.write_auth_file()
|
||||
return True
|
||||
|
||||
def write_auth_file(self):
|
||||
old_auth_file = configmanager.get_config_dir("auth")
|
||||
new_auth_file = old_auth_file + '.new'
|
||||
bak_auth_file = old_auth_file + '.bak'
|
||||
# Let's first create a backup
|
||||
if os.path.exists(old_auth_file):
|
||||
shutil.copy2(old_auth_file, bak_auth_file)
|
||||
|
||||
try:
|
||||
fd = open(new_auth_file, "w")
|
||||
for account in self.__auth.values():
|
||||
fd.write(
|
||||
"%(username)s:%(password)s:%(authlevel_int)s\n" %
|
||||
account.data()
|
||||
)
|
||||
fd.flush()
|
||||
os.fsync(fd.fileno())
|
||||
fd.close()
|
||||
# Change the permissions on the file so only this user can read/write it
|
||||
os.chmod(auth_file, stat.S_IREAD | stat.S_IWRITE)
|
||||
f = [localclient]
|
||||
else:
|
||||
# Load the auth file into a dictionary: {username: password, ...}
|
||||
f = open(auth_file, "r").readlines()
|
||||
os.rename(new_auth_file, old_auth_file)
|
||||
except:
|
||||
# Something failed, let's restore the previous file
|
||||
if os.path.exists(bak_auth_file):
|
||||
os.rename(bak_auth_file, old_auth_file)
|
||||
|
||||
self.__load_auth_file()
|
||||
|
||||
def __load_auth_file(self):
|
||||
save_and_reload = False
|
||||
auth_file = configmanager.get_config_dir("auth")
|
||||
# Check for auth file and create if necessary
|
||||
if not os.path.exists(auth_file):
|
||||
create_localclient_account()
|
||||
return self.__load_auth_file()
|
||||
|
||||
auth_file_modification_time = os.stat(auth_file).st_mtime
|
||||
if self.__auth_modification_time is None:
|
||||
self.__auth_modification_time = auth_file_modification_time
|
||||
elif self.__auth_modification_time == auth_file_modification_time:
|
||||
# File didn't change, no need for re-parsing's
|
||||
return
|
||||
|
||||
# Load the auth file into a dictionary: {username: Account(...)}
|
||||
f = open(auth_file, "r").readlines()
|
||||
|
||||
for line in f:
|
||||
if line.startswith("#"):
|
||||
@ -133,15 +247,43 @@ class AuthManager(component.Component):
|
||||
continue
|
||||
if len(lsplit) == 2:
|
||||
username, password = lsplit
|
||||
log.warning("Your auth entry for %s contains no auth level, using AUTH_LEVEL_DEFAULT(%s)..", username, AUTH_LEVEL_DEFAULT)
|
||||
level = AUTH_LEVEL_DEFAULT
|
||||
log.warning("Your auth entry for %s contains no auth level, "
|
||||
"using AUTH_LEVEL_DEFAULT(%s)..", username,
|
||||
AUTH_LEVEL_DEFAULT)
|
||||
if username == 'localclient':
|
||||
authlevel = AUTH_LEVEL_ADMIN
|
||||
else:
|
||||
authlevel = AUTH_LEVEL_DEFAULT
|
||||
# This is probably an old auth file
|
||||
save_and_reload = True
|
||||
elif len(lsplit) == 3:
|
||||
username, password, level = lsplit
|
||||
username, password, authlevel = lsplit
|
||||
else:
|
||||
log.error("Your auth file is malformed: Incorrect number of fields!")
|
||||
log.error("Your auth file is malformed: "
|
||||
"Incorrect number of fields!")
|
||||
continue
|
||||
|
||||
self.__auth[username.strip()] = (password.strip(), level)
|
||||
username = username.strip()
|
||||
password = password.strip()
|
||||
try:
|
||||
authlevel = int(authlevel)
|
||||
except ValueError:
|
||||
try:
|
||||
authlevel = AUTH_LEVELS_MAPPING[authlevel]
|
||||
except KeyError:
|
||||
log.error("Your auth file is malformed: %r is not a valid auth "
|
||||
"level" % authlevel)
|
||||
continue
|
||||
|
||||
self.__auth[username] = Account(username, password, authlevel)
|
||||
|
||||
if "localclient" not in self.__auth:
|
||||
open(auth_file, "a").write(self.__create_localclient_account())
|
||||
create_localclient_account(True)
|
||||
return self.__load_auth_file()
|
||||
|
||||
|
||||
if save_and_reload:
|
||||
log.info("Re-writing auth file (upgrade)")
|
||||
self.write_auth_file()
|
||||
self.__auth_modification_time = auth_file_modification_time
|
||||
|
||||
|
||||
@ -1,137 +0,0 @@
|
||||
#
|
||||
# autoadd.py
|
||||
#
|
||||
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
# You may redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License, as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# deluge is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with deluge. If not, write to:
|
||||
# The Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# In addition, as a special exception, the copyright holders give
|
||||
# permission to link the code of portions of this program with the OpenSSL
|
||||
# library.
|
||||
# You must obey the GNU General Public License in all respects for all of
|
||||
# the code used other than OpenSSL. If you modify file(s) with this
|
||||
# exception, you may extend this exception to your version of the file(s),
|
||||
# but you are not obligated to do so. If you do not wish to do so, delete
|
||||
# this exception statement from your version. If you delete this exception
|
||||
# statement from all source files in the program, then also delete it here.
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
import logging
|
||||
|
||||
from deluge._libtorrent import lt
|
||||
|
||||
import deluge.component as component
|
||||
from deluge.configmanager import ConfigManager
|
||||
|
||||
MAX_NUM_ATTEMPTS = 10
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class AutoAdd(component.Component):
|
||||
def __init__(self):
|
||||
component.Component.__init__(self, "AutoAdd", depend=["TorrentManager"], interval=5)
|
||||
# Get the core config
|
||||
self.config = ConfigManager("core.conf")
|
||||
|
||||
# A list of filenames
|
||||
self.invalid_torrents = []
|
||||
# Filename:Attempts
|
||||
self.attempts = {}
|
||||
|
||||
# Register set functions
|
||||
self.config.register_set_function("autoadd_enable",
|
||||
self._on_autoadd_enable, apply_now=True)
|
||||
self.config.register_set_function("autoadd_location",
|
||||
self._on_autoadd_location)
|
||||
|
||||
def update(self):
|
||||
if not self.config["autoadd_enable"]:
|
||||
# We shouldn't be updating because autoadd is not enabled
|
||||
component.pause("AutoAdd")
|
||||
return
|
||||
|
||||
# Check the auto add folder for new torrents to add
|
||||
if not os.path.isdir(self.config["autoadd_location"]):
|
||||
log.warning("Invalid AutoAdd folder: %s", self.config["autoadd_location"])
|
||||
component.pause("AutoAdd")
|
||||
return
|
||||
|
||||
for filename in os.listdir(self.config["autoadd_location"]):
|
||||
if filename.split(".")[-1] == "torrent":
|
||||
try:
|
||||
filepath = os.path.join(self.config["autoadd_location"], filename)
|
||||
except UnicodeDecodeError, e:
|
||||
log.error("Unable to auto add torrent due to inproper filename encoding: %s", e)
|
||||
continue
|
||||
try:
|
||||
filedump = self.load_torrent(filepath)
|
||||
except (RuntimeError, Exception), e:
|
||||
# If the torrent is invalid, we keep track of it so that we
|
||||
# can try again on the next pass. This is because some
|
||||
# torrents may not be fully saved during the pass.
|
||||
log.debug("Torrent is invalid: %s", e)
|
||||
if filename in self.invalid_torrents:
|
||||
self.attempts[filename] += 1
|
||||
if self.attempts[filename] >= MAX_NUM_ATTEMPTS:
|
||||
os.rename(filepath, filepath + ".invalid")
|
||||
del self.attempts[filename]
|
||||
self.invalid_torrents.remove(filename)
|
||||
else:
|
||||
self.invalid_torrents.append(filename)
|
||||
self.attempts[filename] = 1
|
||||
continue
|
||||
|
||||
# The torrent looks good, so lets add it to the session
|
||||
component.get("TorrentManager").add(filedump=filedump, filename=filename)
|
||||
|
||||
os.remove(filepath)
|
||||
|
||||
def load_torrent(self, filename):
|
||||
try:
|
||||
log.debug("Attempting to open %s for add.", filename)
|
||||
_file = open(filename, "rb")
|
||||
filedump = _file.read()
|
||||
if not filedump:
|
||||
raise RuntimeError, "Torrent is 0 bytes!"
|
||||
_file.close()
|
||||
except IOError, e:
|
||||
log.warning("Unable to open %s: %s", filename, e)
|
||||
raise e
|
||||
|
||||
# Get the info to see if any exceptions are raised
|
||||
info = lt.torrent_info(lt.bdecode(filedump))
|
||||
|
||||
return filedump
|
||||
|
||||
def _on_autoadd_enable(self, key, value):
|
||||
log.debug("_on_autoadd_enable")
|
||||
if value:
|
||||
component.resume("AutoAdd")
|
||||
else:
|
||||
component.pause("AutoAdd")
|
||||
|
||||
def _on_autoadd_location(self, key, value):
|
||||
log.debug("_on_autoadd_location")
|
||||
# We need to resume the component just incase it was paused due to
|
||||
# an invalid autoadd location.
|
||||
if self.config["autoadd_enable"]:
|
||||
component.resume("AutoAdd")
|
||||
@ -2,6 +2,7 @@
|
||||
# core.py
|
||||
#
|
||||
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
@ -38,17 +39,13 @@ from deluge._libtorrent import lt
|
||||
import os
|
||||
import glob
|
||||
import base64
|
||||
import shutil
|
||||
import logging
|
||||
import threading
|
||||
import pkg_resources
|
||||
import warnings
|
||||
import tempfile
|
||||
from urlparse import urljoin
|
||||
|
||||
|
||||
from twisted.internet import reactor, defer
|
||||
from twisted.internet.task import LoopingCall
|
||||
import twisted.web.client
|
||||
import twisted.web.error
|
||||
|
||||
from deluge.httpdownloader import download_file
|
||||
|
||||
@ -57,12 +54,13 @@ 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
|
||||
@ -78,7 +76,8 @@ class Core(component.Component):
|
||||
log.info("Starting libtorrent %s session..", lt.version)
|
||||
|
||||
# Create the client fingerprint
|
||||
version = [int(value.split("-")[0]) for value in deluge.common.get_version().split(".")]
|
||||
version = [int(value.split("-")[0]) for value in
|
||||
deluge.common.get_version().split(".")]
|
||||
while len(version) < 4:
|
||||
version.append(0)
|
||||
|
||||
@ -89,10 +88,17 @@ class Core(component.Component):
|
||||
|
||||
# Set the user agent
|
||||
self.settings = lt.session_settings()
|
||||
self.settings.user_agent = "Deluge %s" % deluge.common.get_version()
|
||||
self.settings.user_agent = "Deluge/%(deluge_version)s Libtorrent/%(lt_version)s" % \
|
||||
{ 'deluge_version': deluge.common.get_version(),
|
||||
'lt_version': self.get_libtorrent_version().rpartition(".")[0] }
|
||||
|
||||
# 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_for_aligned_files
|
||||
self.settings.disk_io_read_mode = \
|
||||
lt.io_buffer_mode_t.disable_os_cache_for_aligned_files
|
||||
self.session.set_settings(self.settings)
|
||||
|
||||
# Load metadata extension
|
||||
@ -107,7 +113,6 @@ class Core(component.Component):
|
||||
self.pluginmanager = PluginManager(self)
|
||||
self.torrentmanager = TorrentManager()
|
||||
self.filtermanager = FilterManager(self)
|
||||
self.autoadd = AutoAdd()
|
||||
self.authmanager = AuthManager()
|
||||
|
||||
# New release check information
|
||||
@ -115,6 +120,8 @@ class Core(component.Component):
|
||||
|
||||
# Get the core config
|
||||
self.config = deluge.configmanager.ConfigManager("core.conf")
|
||||
self.config.run_converter((0, 1), 2, self.__migrate_config_1_to_2)
|
||||
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
|
||||
@ -148,19 +155,25 @@ class Core(component.Component):
|
||||
def __save_session_state(self):
|
||||
"""Saves the libtorrent session state"""
|
||||
try:
|
||||
open(deluge.configmanager.get_config_dir("session.state"), "wb").write(
|
||||
lt.bencode(self.session.state()))
|
||||
session_state = deluge.configmanager.get_config_dir("session.state")
|
||||
open(session_state, "wb").write(lt.bencode(self.session.state()))
|
||||
except Exception, e:
|
||||
log.warning("Failed to save lt state: %s", e)
|
||||
|
||||
def __load_session_state(self):
|
||||
"""Loads the libtorrent session state"""
|
||||
try:
|
||||
self.session.load_state(lt.bdecode(
|
||||
open(deluge.configmanager.get_config_dir("session.state"), "rb").read()))
|
||||
session_state = deluge.configmanager.get_config_dir("session.state")
|
||||
self.session.load_state(lt.bdecode(open(session_state, "rb").read()))
|
||||
except Exception, e:
|
||||
log.warning("Failed to load lt state: %s", e)
|
||||
|
||||
|
||||
def __migrate_config_1_to_2(self, config):
|
||||
if 'sequential_download' not in config:
|
||||
config['sequential_download'] = False
|
||||
return config
|
||||
|
||||
def save_dht_state(self):
|
||||
"""Saves the dht state to a file"""
|
||||
try:
|
||||
@ -213,7 +226,9 @@ class Core(component.Component):
|
||||
log.exception(e)
|
||||
|
||||
try:
|
||||
torrent_id = self.torrentmanager.add(filedump=filedump, options=options, filename=filename)
|
||||
torrent_id = self.torrentmanager.add(
|
||||
filedump=filedump, options=options, filename=filename
|
||||
)
|
||||
except Exception, e:
|
||||
log.error("There was an error adding the torrent file %s", filename)
|
||||
log.exception(e)
|
||||
@ -237,7 +252,7 @@ class Core(component.Component):
|
||||
:returns: a Deferred which returns the torrent_id as a str or None
|
||||
"""
|
||||
log.info("Attempting to add url %s", url)
|
||||
def on_get_file(filename):
|
||||
def on_download_success(filename):
|
||||
# We got the file, so add it to the session
|
||||
f = open(filename, "rb")
|
||||
data = f.read()
|
||||
@ -246,17 +261,35 @@ class Core(component.Component):
|
||||
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)
|
||||
return self.add_torrent_file(
|
||||
filename, base64.encodestring(data), options
|
||||
)
|
||||
|
||||
def on_get_file_error(failure):
|
||||
# Log the error and pass the failure onto the client
|
||||
log.error("Error occured downloading torrent from %s", url)
|
||||
log.error("Reason: %s", failure.getErrorMessage())
|
||||
return failure
|
||||
def on_download_fail(failure):
|
||||
if failure.check(twisted.web.error.PageRedirect):
|
||||
new_url = urljoin(url, failure.getErrorMessage().split(" to ")[1])
|
||||
result = download_file(
|
||||
new_url, tempfile.mkstemp()[1], headers=headers,
|
||||
force_filename=True
|
||||
)
|
||||
result.addCallbacks(on_download_success, on_download_fail)
|
||||
elif failure.check(twisted.web.client.PartialDownloadError):
|
||||
result = download_file(
|
||||
url, tempfile.mkstemp()[1], headers=headers,
|
||||
force_filename=True, allow_compression=False
|
||||
)
|
||||
result.addCallbacks(on_download_success, on_download_fail)
|
||||
else:
|
||||
# Log the error and pass the failure onto the client
|
||||
log.error("Error occured downloading torrent from %s", url)
|
||||
log.error("Reason: %s", failure.getErrorMessage())
|
||||
result = failure
|
||||
return result
|
||||
|
||||
d = download_file(url, tempfile.mkstemp()[1], headers=headers)
|
||||
d.addCallback(on_get_file)
|
||||
d.addErrback(on_get_file_error)
|
||||
d = download_file(
|
||||
url, tempfile.mkstemp()[1], headers=headers, force_filename=True
|
||||
)
|
||||
d.addCallbacks(on_download_success, on_download_fail)
|
||||
return d
|
||||
|
||||
@export
|
||||
@ -394,7 +427,11 @@ class Core(component.Component):
|
||||
@export
|
||||
def get_torrent_status(self, torrent_id, keys, diff=False):
|
||||
# Build the status dictionary
|
||||
status = self.torrentmanager[torrent_id].get_status(keys, diff)
|
||||
try:
|
||||
status = self.torrentmanager[torrent_id].get_status(keys, diff)
|
||||
except KeyError:
|
||||
# Torrent was probaly removed meanwhile
|
||||
return {}
|
||||
|
||||
# Get the leftover fields and ask the plugin manager to fill them
|
||||
leftover_fields = list(set(keys) - set(status.keys()))
|
||||
@ -542,6 +579,11 @@ class Core(component.Component):
|
||||
"""Sets a higher priority to the first and last pieces"""
|
||||
return self.torrentmanager[torrent_id].set_prioritize_first_last(value)
|
||||
|
||||
@export
|
||||
def set_torrent_sequential_download(self, torrent_id, value):
|
||||
"""Toggle sequencial pieces download"""
|
||||
return self.torrentmanager[torrent_id].set_sequential_download(value)
|
||||
|
||||
@export
|
||||
def set_torrent_auto_managed(self, torrent_id, value):
|
||||
"""Sets the auto managed flag for queueing purposes"""
|
||||
@ -572,6 +614,32 @@ class Core(component.Component):
|
||||
"""Sets the path for the torrent to be moved when completed"""
|
||||
return self.torrentmanager[torrent_id].set_move_completed_path(value)
|
||||
|
||||
@export(AUTH_LEVEL_ADMIN)
|
||||
def set_torrents_owner(self, torrent_ids, username):
|
||||
"""Set's the torrent owner.
|
||||
|
||||
:param torrent_id: the torrent_id of the torrent to remove
|
||||
:type torrent_id: string
|
||||
:param username: the new owner username
|
||||
:type username: string
|
||||
|
||||
:raises DelugeError: if the username is not known
|
||||
"""
|
||||
if not self.authmanager.has_account(username):
|
||||
raise DelugeError("Username \"%s\" is not known." % username)
|
||||
if isinstance(torrent_ids, basestring):
|
||||
torrent_ids = [torrent_ids]
|
||||
for torrent_id in torrent_ids:
|
||||
self.torrentmanager[torrent_id].set_owner(username)
|
||||
return None
|
||||
|
||||
@export
|
||||
def set_torrents_shared(self, torrent_ids, shared):
|
||||
if isinstance(torrent_ids, basestring):
|
||||
torrent_ids = [torrent_ids]
|
||||
for torrent_id in torrent_ids:
|
||||
self.torrentmanager[torrent_id].set_options({"shared": shared})
|
||||
|
||||
@export
|
||||
def get_path_size(self, path):
|
||||
"""Returns the size of the file or folder 'path' and -1 if the path is
|
||||
@ -754,7 +822,11 @@ class Core(component.Component):
|
||||
def on_get_page(result):
|
||||
return bool(int(result))
|
||||
|
||||
def logError(failure):
|
||||
log.warning("Error testing listen port: %s", failure)
|
||||
|
||||
d.addCallback(on_get_page)
|
||||
d.addErrback(logError)
|
||||
|
||||
return d
|
||||
|
||||
@ -790,3 +862,23 @@ class Core(component.Component):
|
||||
|
||||
"""
|
||||
return lt.version
|
||||
|
||||
@export(AUTH_LEVEL_ADMIN)
|
||||
def get_known_accounts(self):
|
||||
return self.authmanager.get_known_accounts()
|
||||
|
||||
@export(AUTH_LEVEL_NONE)
|
||||
def get_auth_levels_mappings(self):
|
||||
return (AUTH_LEVELS_MAPPING, AUTH_LEVELS_MAPPING_REVERSE)
|
||||
|
||||
@export(AUTH_LEVEL_ADMIN)
|
||||
def create_account(self, username, password, authlevel):
|
||||
return self.authmanager.create_account(username, password, authlevel)
|
||||
|
||||
@export(AUTH_LEVEL_ADMIN)
|
||||
def update_account(self, username, password, authlevel):
|
||||
return self.authmanager.update_account(username, password, authlevel)
|
||||
|
||||
@export(AUTH_LEVEL_ADMIN)
|
||||
def remove_account(self, username):
|
||||
return self.authmanager.remove_account(username)
|
||||
|
||||
@ -54,7 +54,9 @@ class Daemon(object):
|
||||
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:
|
||||
@ -93,7 +95,10 @@ 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:
|
||||
@ -191,15 +196,6 @@ class Daemon(object):
|
||||
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):
|
||||
"""
|
||||
|
||||
@ -55,7 +55,10 @@ class EventManager(component.Component):
|
||||
if event.name in self.handlers:
|
||||
for handler in self.handlers[event.name]:
|
||||
#log.debug("Running handler %s for event %s with args: %s", event.name, handler, event.args)
|
||||
handler(event.copy())
|
||||
try:
|
||||
handler(*event.args)
|
||||
except:
|
||||
log.error("Event handler %s failed in %s", event.name, handler)
|
||||
|
||||
def register_event_handler(self, event, handler):
|
||||
"""
|
||||
|
||||
@ -78,6 +78,27 @@ def filter_one_keyword(torrent_ids, keyword):
|
||||
yield torrent_id
|
||||
break
|
||||
|
||||
def filter_by_name(torrent_ids, search_string):
|
||||
all_torrents = component.get("TorrentManager").torrents
|
||||
try:
|
||||
search_string, match_case = search_string[0].split('::match')
|
||||
except ValueError:
|
||||
search_string = search_string[0]
|
||||
match_case = False
|
||||
|
||||
if match_case is False:
|
||||
search_string = search_string.lower()
|
||||
|
||||
for torrent_id in torrent_ids:
|
||||
torrent_name = all_torrents[torrent_id].get_name()
|
||||
if match_case is False:
|
||||
torrent_name = all_torrents[torrent_id].get_name().lower()
|
||||
else:
|
||||
torrent_name = all_torrents[torrent_id].get_name()
|
||||
|
||||
if search_string in torrent_name:
|
||||
yield torrent_id
|
||||
|
||||
def tracker_error_filter(torrent_ids, values):
|
||||
filtered_torrent_ids = []
|
||||
tm = component.get("TorrentManager")
|
||||
@ -108,6 +129,7 @@ class FilterManager(component.Component):
|
||||
self.torrents = core.torrentmanager
|
||||
self.registered_filters = {}
|
||||
self.register_filter("keyword", filter_keywords)
|
||||
self.register_filter("name", filter_by_name)
|
||||
self.tree_fields = {}
|
||||
|
||||
self.register_tree_field("state", self._init_state_tree)
|
||||
@ -136,7 +158,7 @@ class FilterManager(component.Component):
|
||||
|
||||
|
||||
if "id"in filter_dict: #optimized filter for id:
|
||||
torrent_ids = filter_dict["id"]
|
||||
torrent_ids = list(filter_dict["id"])
|
||||
del filter_dict["id"]
|
||||
else:
|
||||
torrent_ids = self.torrents.get_torrent_list()
|
||||
|
||||
@ -37,8 +37,6 @@
|
||||
"""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
|
||||
|
||||
@ -38,7 +38,6 @@ import os
|
||||
import logging
|
||||
import threading
|
||||
import pkg_resources
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet.task import LoopingCall
|
||||
|
||||
from deluge._libtorrent import lt
|
||||
@ -64,6 +63,7 @@ DEFAULT_PREFS = {
|
||||
"torrentfiles_location": deluge.common.get_default_download_dir(),
|
||||
"plugins_location": os.path.join(deluge.configmanager.get_config_dir(), "plugins"),
|
||||
"prioritize_first_last_pieces": False,
|
||||
"sequential_download": False,
|
||||
"random_port": True,
|
||||
"dht": True,
|
||||
"upnp": True,
|
||||
@ -87,8 +87,6 @@ DEFAULT_PREFS = {
|
||||
"max_upload_speed_per_torrent": -1,
|
||||
"max_download_speed_per_torrent": -1,
|
||||
"enabled_plugins": [],
|
||||
"autoadd_location": deluge.common.get_default_download_dir(),
|
||||
"autoadd_enable": False,
|
||||
"add_paused": False,
|
||||
"max_active_seeding": 5,
|
||||
"max_active_downloading": 3,
|
||||
@ -143,7 +141,7 @@ DEFAULT_PREFS = {
|
||||
"geoip_db_location": "/usr/share/GeoIP/GeoIP.dat",
|
||||
"cache_size": 512,
|
||||
"cache_expiry": 60,
|
||||
"public": False
|
||||
"shared": False
|
||||
}
|
||||
|
||||
class PreferencesManager(component.Component):
|
||||
@ -151,6 +149,11 @@ 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")
|
||||
@ -193,7 +196,9 @@ class PreferencesManager(component.Component):
|
||||
# Only set the listen ports if random_port is not true
|
||||
if self.config["random_port"] is not True:
|
||||
log.debug("listen port range set to %s-%s", value[0], value[1])
|
||||
self.session.listen_on(value[0], value[1], str(self.config["listen_interface"]))
|
||||
self.session.listen_on(
|
||||
value[0], value[1], str(self.config["listen_interface"])
|
||||
)
|
||||
|
||||
def _on_set_listen_interface(self, key, value):
|
||||
# Call the random_port callback since it'll do what we need
|
||||
@ -215,7 +220,10 @@ 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"]:
|
||||
@ -442,8 +450,12 @@ class PreferencesManager(component.Component):
|
||||
geoip_db = ""
|
||||
if os.path.exists(value):
|
||||
geoip_db = value
|
||||
elif os.path.exists(pkg_resources.resource_filename("deluge", os.path.join("data", "GeoIP.dat"))):
|
||||
geoip_db = pkg_resources.resource_filename("deluge", os.path.join("data", "GeoIP.dat"))
|
||||
elif os.path.exists(
|
||||
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!")
|
||||
|
||||
|
||||
@ -43,7 +43,7 @@ import logging
|
||||
import traceback
|
||||
|
||||
from twisted.internet.protocol import Factory, Protocol
|
||||
from twisted.internet import ssl, reactor, defer
|
||||
from twisted.internet import reactor, defer
|
||||
|
||||
from OpenSSL import crypto, SSL
|
||||
from types import FunctionType
|
||||
@ -55,7 +55,10 @@ except ImportError:
|
||||
|
||||
import deluge.component as component
|
||||
import deluge.configmanager
|
||||
from deluge.core.authmanager import AUTH_LEVEL_NONE, AUTH_LEVEL_DEFAULT
|
||||
from deluge.core.authmanager import (AUTH_LEVEL_NONE, AUTH_LEVEL_DEFAULT,
|
||||
AUTH_LEVEL_ADMIN)
|
||||
from deluge.error import (DelugeError, NotAuthorizedError,
|
||||
_ClientSideRecreateError, IncompatibleClient)
|
||||
|
||||
RPC_RESPONSE = 1
|
||||
RPC_ERROR = 2
|
||||
@ -117,12 +120,6 @@ def format_request(call):
|
||||
else:
|
||||
return s
|
||||
|
||||
class DelugeError(Exception):
|
||||
pass
|
||||
|
||||
class NotAuthorizedError(DelugeError):
|
||||
pass
|
||||
|
||||
class ServerContextFactory(object):
|
||||
def getContext(self):
|
||||
"""
|
||||
@ -180,7 +177,8 @@ class DelugeRPCProtocol(Protocol):
|
||||
|
||||
for call in request:
|
||||
if len(call) != 4:
|
||||
log.debug("Received invalid rpc request: number of items in request is %s", len(call))
|
||||
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)
|
||||
@ -201,7 +199,8 @@ class DelugeRPCProtocol(Protocol):
|
||||
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
|
||||
|
||||
@ -223,6 +222,9 @@ class DelugeRPCProtocol(Protocol):
|
||||
|
||||
log.info("Deluge client disconnected: %s", reason.value)
|
||||
|
||||
def valid_session(self):
|
||||
return self.transport.sessionno in self.factory.authorized_sessions
|
||||
|
||||
def dispatch(self, request_id, method, args, kwargs):
|
||||
"""
|
||||
This method is run when a RPC Request is made. It will run the local method
|
||||
@ -244,33 +246,49 @@ class DelugeRPCProtocol(Protocol):
|
||||
Sends an error response with the contents of the exception that was raised.
|
||||
"""
|
||||
exceptionType, exceptionValue, exceptionTraceback = sys.exc_info()
|
||||
try:
|
||||
self.sendData((
|
||||
RPC_ERROR,
|
||||
request_id,
|
||||
exceptionType.__name__,
|
||||
exceptionValue._args,
|
||||
exceptionValue._kwargs,
|
||||
"".join(traceback.format_tb(exceptionTraceback))
|
||||
))
|
||||
except Exception, err:
|
||||
log.error("An exception occurred while sending RPC_ERROR to "
|
||||
"client. Error to send(exception goes next): %s",
|
||||
"".join(traceback.format_tb(exceptionTraceback)))
|
||||
log.exception(err)
|
||||
|
||||
self.sendData((
|
||||
RPC_ERROR,
|
||||
request_id,
|
||||
(exceptionType.__name__,
|
||||
exceptionValue.args[0] if len(exceptionValue.args) == 1 else "",
|
||||
"".join(traceback.format_tb(exceptionTraceback)))
|
||||
))
|
||||
|
||||
if method == "daemon.login":
|
||||
if method == "daemon.info":
|
||||
# This is a special case and used in the initial connection process
|
||||
self.sendData((RPC_RESPONSE, request_id, deluge.common.get_version()))
|
||||
return
|
||||
elif method == "daemon.login":
|
||||
# This is a special case and used in the initial connection process
|
||||
# We need to authenticate the user here
|
||||
log.debug("RPC dispatch daemon.login")
|
||||
try:
|
||||
client_version = kwargs.pop('client_version', None)
|
||||
if client_version is None:
|
||||
raise IncompatibleClient(deluge.common.get_version())
|
||||
ret = component.get("AuthManager").authorize(*args, **kwargs)
|
||||
if ret:
|
||||
self.factory.authorized_sessions[self.transport.sessionno] = (ret, args[0])
|
||||
self.factory.session_protocols[self.transport.sessionno] = self
|
||||
except Exception, e:
|
||||
sendError()
|
||||
log.exception(e)
|
||||
if not isinstance(e, _ClientSideRecreateError):
|
||||
log.exception(e)
|
||||
else:
|
||||
self.sendData((RPC_RESPONSE, request_id, (ret)))
|
||||
if not ret:
|
||||
self.transport.loseConnection()
|
||||
finally:
|
||||
return
|
||||
elif method == "daemon.set_event_interest" and self.transport.sessionno in self.factory.authorized_sessions:
|
||||
elif method == "daemon.set_event_interest" and self.valid_session():
|
||||
log.debug("RPC dispatch daemon.set_event_interest")
|
||||
# This special case is to allow clients to set which events they are
|
||||
# interested in receiving.
|
||||
# We are expecting a sequence from the client.
|
||||
@ -285,21 +303,24 @@ class DelugeRPCProtocol(Protocol):
|
||||
finally:
|
||||
return
|
||||
|
||||
if method in self.factory.methods and self.transport.sessionno in self.factory.authorized_sessions:
|
||||
if method in self.factory.methods and self.valid_session():
|
||||
log.debug("RPC dispatch %s", method)
|
||||
try:
|
||||
method_auth_requirement = self.factory.methods[method]._rpcserver_auth_level
|
||||
auth_level = self.factory.authorized_sessions[self.transport.sessionno][0]
|
||||
if auth_level < method_auth_requirement:
|
||||
# This session is not allowed to call this method
|
||||
log.debug("Session %s is trying to call a method it is not authorized to call!", self.transport.sessionno)
|
||||
raise NotAuthorizedError("Auth level too low: %s < %s" % (auth_level, method_auth_requirement))
|
||||
log.debug("Session %s is trying to call a method it is not "
|
||||
"authorized to call!", self.transport.sessionno)
|
||||
raise NotAuthorizedError(auth_level, method_auth_requirement)
|
||||
# Set the session_id in the factory so that methods can know
|
||||
# which session is calling it.
|
||||
self.factory.session_id = self.transport.sessionno
|
||||
ret = self.factory.methods[method](*args, **kwargs)
|
||||
except Exception, e:
|
||||
sendError()
|
||||
# Don't bother printing out DelugeErrors, because they are just for the client
|
||||
# Don't bother printing out DelugeErrors, because they are just
|
||||
# for the client
|
||||
if not isinstance(e, DelugeError):
|
||||
log.exception("Exception calling RPC request: %s", e)
|
||||
else:
|
||||
@ -353,6 +374,7 @@ class RPCServer(component.Component):
|
||||
# Holds the interested event list for the sessions
|
||||
self.factory.interested_events = {}
|
||||
|
||||
self.listen = listen
|
||||
if not listen:
|
||||
return
|
||||
|
||||
@ -437,6 +459,8 @@ class RPCServer(component.Component):
|
||||
: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]
|
||||
@ -451,6 +475,8 @@ class RPCServer(component.Component):
|
||||
:returns: the auth level
|
||||
:rtype: int
|
||||
"""
|
||||
if not self.listen:
|
||||
return AUTH_LEVEL_ADMIN
|
||||
return self.factory.authorized_sessions[self.get_session_id()][0]
|
||||
|
||||
def get_rpc_auth_level(self, rpc):
|
||||
@ -486,8 +512,7 @@ class RPCServer(component.Component):
|
||||
# Find sessions interested in this event
|
||||
for session_id, interest in self.factory.interested_events.iteritems():
|
||||
if event.name in interest:
|
||||
log.debug("Emit Event: %s %s", event.name, zip(event.__slots__,
|
||||
event.args))
|
||||
log.debug("Emit Event: %s %s", event.name, event.args)
|
||||
# This session is interested so send a RPC_EVENT
|
||||
self.factory.session_protocols[session_id].sendData(
|
||||
(RPC_EVENT, event.name, event.args)
|
||||
@ -536,8 +561,12 @@ def generate_ssl_keys():
|
||||
|
||||
# Write out files
|
||||
ssl_dir = deluge.configmanager.get_config_dir("ssl")
|
||||
open(os.path.join(ssl_dir, "daemon.pkey"), "w").write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
|
||||
open(os.path.join(ssl_dir, "daemon.cert"), "w").write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
||||
open(os.path.join(ssl_dir, "daemon.pkey"), "w").write(
|
||||
crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
|
||||
)
|
||||
open(os.path.join(ssl_dir, "daemon.cert"), "w").write(
|
||||
crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
|
||||
)
|
||||
# Make the files only readable by this user
|
||||
for f in ("daemon.pkey", "daemon.cert"):
|
||||
os.chmod(os.path.join(ssl_dir, f), stat.S_IREAD | stat.S_IWRITE)
|
||||
|
||||
@ -55,22 +55,23 @@ class TorrentOptions(dict):
|
||||
def __init__(self):
|
||||
config = ConfigManager("core.conf").config
|
||||
options_conf_map = {
|
||||
"max_connections": "max_connections_per_torrent",
|
||||
"max_upload_slots": "max_upload_slots_per_torrent",
|
||||
"max_upload_speed": "max_upload_speed_per_torrent",
|
||||
"max_download_speed": "max_download_speed_per_torrent",
|
||||
"prioritize_first_last_pieces": "prioritize_first_last_pieces",
|
||||
"compact_allocation": "compact_allocation",
|
||||
"download_location": "download_location",
|
||||
"auto_managed": "auto_managed",
|
||||
"stop_at_ratio": "stop_seed_at_ratio",
|
||||
"stop_ratio": "stop_seed_ratio",
|
||||
"remove_at_ratio": "remove_seed_at_ratio",
|
||||
"move_completed": "move_completed",
|
||||
"move_completed_path": "move_completed_path",
|
||||
"add_paused": "add_paused",
|
||||
"public": "public"
|
||||
}
|
||||
"max_connections": "max_connections_per_torrent",
|
||||
"max_upload_slots": "max_upload_slots_per_torrent",
|
||||
"max_upload_speed": "max_upload_speed_per_torrent",
|
||||
"max_download_speed": "max_download_speed_per_torrent",
|
||||
"prioritize_first_last_pieces": "prioritize_first_last_pieces",
|
||||
"sequential_download": "sequential_download",
|
||||
"compact_allocation": "compact_allocation",
|
||||
"download_location": "download_location",
|
||||
"auto_managed": "auto_managed",
|
||||
"stop_at_ratio": "stop_seed_at_ratio",
|
||||
"stop_ratio": "stop_seed_ratio",
|
||||
"remove_at_ratio": "remove_seed_at_ratio",
|
||||
"move_completed": "move_completed",
|
||||
"move_completed_path": "move_completed_path",
|
||||
"add_paused": "add_paused",
|
||||
"shared": "shared"
|
||||
}
|
||||
for opt_k, conf_k in options_conf_map.iteritems():
|
||||
self[opt_k] = config[conf_k]
|
||||
self["file_priorities"] = []
|
||||
@ -188,8 +189,14 @@ class Torrent(object):
|
||||
else:
|
||||
self.owner = owner
|
||||
|
||||
# Keep track of last seen complete
|
||||
if state:
|
||||
self._last_seen_complete = state.last_seen_complete or 0.0
|
||||
else:
|
||||
self._last_seen_complete = 0.0
|
||||
|
||||
# Keep track if we're forcing a recheck of the torrent so that we can
|
||||
# repause it after its done if necessary
|
||||
# re-pause it after its done if necessary
|
||||
self.forcing_recheck = False
|
||||
self.forcing_recheck_paused = False
|
||||
|
||||
@ -206,17 +213,44 @@ class Torrent(object):
|
||||
"max_download_speed": self.set_max_download_speed,
|
||||
"max_upload_slots": self.handle.set_max_uploads,
|
||||
"max_upload_speed": self.set_max_upload_speed,
|
||||
"prioritize_first_last_pieces": self.set_prioritize_first_last
|
||||
"prioritize_first_last_pieces": self.set_prioritize_first_last,
|
||||
"sequential_download": self.set_sequential_download
|
||||
|
||||
}
|
||||
for (key, value) in options.items():
|
||||
if OPTIONS_FUNCS.has_key(key):
|
||||
OPTIONS_FUNCS[key](value)
|
||||
|
||||
self.options.update(options)
|
||||
|
||||
def get_options(self):
|
||||
return self.options
|
||||
|
||||
def get_name(self):
|
||||
if self.handle.has_metadata():
|
||||
name = self.torrent_info.file_at(0).path.split("/", 1)[0]
|
||||
if not name:
|
||||
name = self.torrent_info.name()
|
||||
try:
|
||||
return name.decode("utf8", "ignore")
|
||||
except UnicodeDecodeError:
|
||||
return name
|
||||
elif self.magnet:
|
||||
try:
|
||||
keys = dict([k.split('=') for k in self.magnet.split('?')[-1].split('&')])
|
||||
name = keys.get('dn')
|
||||
if not name:
|
||||
return self.torrent_id
|
||||
name = unquote(name).replace('+', ' ')
|
||||
try:
|
||||
return name.decode("utf8", "ignore")
|
||||
except UnicodeDecodeError:
|
||||
return name
|
||||
except:
|
||||
pass
|
||||
return self.torrent_id
|
||||
|
||||
def set_owner(self, account):
|
||||
self.owner = account
|
||||
|
||||
def set_max_connections(self, max_connections):
|
||||
self.options["max_connections"] = int(max_connections)
|
||||
@ -245,14 +279,30 @@ class Torrent(object):
|
||||
|
||||
def set_prioritize_first_last(self, prioritize):
|
||||
self.options["prioritize_first_last_pieces"] = prioritize
|
||||
if prioritize:
|
||||
if self.handle.has_metadata():
|
||||
if self.handle.get_torrent_info().num_files() == 1:
|
||||
# We only do this if one file is in the torrent
|
||||
priorities = [1] * self.handle.get_torrent_info().num_pieces()
|
||||
priorities[0] = 7
|
||||
priorities[-1] = 7
|
||||
self.handle.prioritize_pieces(priorities)
|
||||
if self.handle.has_metadata():
|
||||
if self.options["compact_allocation"]:
|
||||
log.debug("Setting first/last priority with compact "
|
||||
"allocation does not work!")
|
||||
return
|
||||
|
||||
paths = {}
|
||||
ti = self.handle.get_torrent_info()
|
||||
for n in range(ti.num_pieces()):
|
||||
slices = ti.map_block(n, 0, ti.piece_size(n))
|
||||
for slice in slices:
|
||||
fe = ti.file_at(slice.file_index)
|
||||
paths.setdefault(fe.path, []).append(n)
|
||||
|
||||
priorities = self.handle.piece_priorities()
|
||||
for pieces in paths.itervalues():
|
||||
two_percent = 2*100/len(pieces)
|
||||
for piece in pieces[:two_percent]+pieces[-two_percent:]:
|
||||
priorities[piece] = prioritize and 7 or 1
|
||||
self.handle.prioritize_pieces(priorities)
|
||||
|
||||
def set_sequential_download(self, set_sequencial):
|
||||
self.options["sequential_download"] = set_sequencial
|
||||
self.handle.set_sequential_download(set_sequencial)
|
||||
|
||||
def set_auto_managed(self, auto_managed):
|
||||
self.options["auto_managed"] = auto_managed
|
||||
@ -333,7 +383,7 @@ class Torrent(object):
|
||||
# Set the tracker list in the torrent object
|
||||
self.trackers = trackers
|
||||
if len(trackers) > 0:
|
||||
# Force a reannounce if there is at least 1 tracker
|
||||
# Force a re-announce if there is at least 1 tracker
|
||||
self.force_reannounce()
|
||||
|
||||
self.tracker_host = None
|
||||
@ -556,6 +606,16 @@ class Torrent(object):
|
||||
return host
|
||||
return ""
|
||||
|
||||
def get_last_seen_complete(self):
|
||||
"""
|
||||
Returns the time a torrent was last seen complete, ie, with all pieces
|
||||
available.
|
||||
"""
|
||||
if lt.version_minor > 15:
|
||||
return self.status.last_seen_complete
|
||||
self.calculate_last_seen_complete()
|
||||
return self._last_seen_complete
|
||||
|
||||
def get_status(self, keys, diff=False):
|
||||
"""
|
||||
Returns the status of the torrent based on the keys provided
|
||||
@ -584,7 +644,13 @@ class Torrent(object):
|
||||
if distributed_copies < 0:
|
||||
distributed_copies = 0.0
|
||||
|
||||
#if you add a key here->add it to core.py STATUS_KEYS too.
|
||||
# Calculate the seeds:peers ratio
|
||||
if self.status.num_incomplete == 0:
|
||||
# Use -1.0 to signify infinity
|
||||
seeds_peers_ratio = -1.0
|
||||
else:
|
||||
seeds_peers_ratio = self.status.num_complete / float(self.status.num_incomplete)
|
||||
|
||||
full_status = {
|
||||
"active_time": self.status.active_time,
|
||||
"all_time_download": self.status.all_time_download,
|
||||
@ -602,17 +668,21 @@ class Torrent(object):
|
||||
"message": self.statusmsg,
|
||||
"move_on_completed_path": self.options["move_completed_path"],
|
||||
"move_on_completed": self.options["move_completed"],
|
||||
"move_completed_path": self.options["move_completed_path"],
|
||||
"move_completed": self.options["move_completed"],
|
||||
"next_announce": self.status.next_announce.seconds,
|
||||
"num_peers": self.status.num_peers - self.status.num_seeds,
|
||||
"num_seeds": self.status.num_seeds,
|
||||
"owner": self.owner,
|
||||
"paused": self.status.paused,
|
||||
"prioritize_first_last": self.options["prioritize_first_last_pieces"],
|
||||
"sequential_download": self.options["sequential_download"],
|
||||
"progress": progress,
|
||||
"public": self.options["public"],
|
||||
"shared": self.options["shared"],
|
||||
"remove_at_ratio": self.options["remove_at_ratio"],
|
||||
"save_path": self.options["download_location"],
|
||||
"seeding_time": self.status.seeding_time,
|
||||
"seeds_peers_ratio": seeds_peers_ratio,
|
||||
"seed_rank": self.status.seed_rank,
|
||||
"state": self.state,
|
||||
"stop_at_ratio": self.options["stop_at_ratio"],
|
||||
@ -639,32 +709,6 @@ class Torrent(object):
|
||||
return self.torrent_info.comment()
|
||||
return ""
|
||||
|
||||
def ti_name():
|
||||
if self.handle.has_metadata():
|
||||
name = self.torrent_info.file_at(0).path.split("/", 1)[0]
|
||||
if not name:
|
||||
name = self.torrent_info.name()
|
||||
try:
|
||||
return name.decode("utf8", "ignore")
|
||||
except UnicodeDecodeError:
|
||||
return name
|
||||
|
||||
elif self.magnet:
|
||||
try:
|
||||
keys = dict([k.split('=') for k in self.magnet.split('?')[-1].split('&')])
|
||||
name = keys.get('dn')
|
||||
if not name:
|
||||
return self.torrent_id
|
||||
name = unquote(name).replace('+', ' ')
|
||||
try:
|
||||
return name.decode("utf8", "ignore")
|
||||
except UnicodeDecodeError:
|
||||
return name
|
||||
except:
|
||||
pass
|
||||
|
||||
return self.torrent_id
|
||||
|
||||
def ti_priv():
|
||||
if self.handle.has_metadata():
|
||||
return self.torrent_info.priv()
|
||||
@ -685,6 +729,10 @@ class Torrent(object):
|
||||
if self.handle.has_metadata():
|
||||
return self.torrent_info.piece_length()
|
||||
return 0
|
||||
def ti_pieces_info():
|
||||
if self.handle.has_metadata():
|
||||
return self.get_pieces_info()
|
||||
return None
|
||||
|
||||
fns = {
|
||||
"comment": ti_comment,
|
||||
@ -692,9 +740,10 @@ class Torrent(object):
|
||||
"file_progress": self.get_file_progress,
|
||||
"files": self.get_files,
|
||||
"is_seed": self.handle.is_seed,
|
||||
"name": ti_name,
|
||||
"name": self.get_name,
|
||||
"num_files": ti_num_files,
|
||||
"num_pieces": ti_num_pieces,
|
||||
"pieces": ti_pieces_info,
|
||||
"peers": self.get_peers,
|
||||
"piece_length": ti_piece_length,
|
||||
"private": ti_priv,
|
||||
@ -702,6 +751,7 @@ class Torrent(object):
|
||||
"ratio": self.get_ratio,
|
||||
"total_size": ti_total_size,
|
||||
"tracker_host": self.get_tracker_host,
|
||||
"last_seen_complete": self.get_last_seen_complete
|
||||
}
|
||||
|
||||
# Create the desired status dictionary and return it
|
||||
@ -745,6 +795,7 @@ class Torrent(object):
|
||||
self.handle.set_upload_limit(int(self.max_upload_speed * 1024))
|
||||
self.handle.set_download_limit(int(self.max_download_speed * 1024))
|
||||
self.handle.prioritize_files(self.file_priorities)
|
||||
self.handle.set_sequential_download(self.options["sequential_download"])
|
||||
self.handle.resolve_countries(True)
|
||||
|
||||
def pause(self):
|
||||
@ -807,16 +858,27 @@ class Torrent(object):
|
||||
|
||||
def move_storage(self, dest):
|
||||
"""Move a torrent's storage location"""
|
||||
if not os.path.exists(dest):
|
||||
|
||||
# Attempt to convert utf8 path to unicode
|
||||
# Note: Inconsistent encoding for 'dest', needs future investigation
|
||||
try:
|
||||
dest_u = unicode(dest, "utf-8")
|
||||
except TypeError:
|
||||
# String is already unicode
|
||||
dest_u = dest
|
||||
|
||||
if not os.path.exists(dest_u):
|
||||
try:
|
||||
# Try to make the destination path if it doesn't exist
|
||||
os.makedirs(dest)
|
||||
os.makedirs(dest_u)
|
||||
except IOError, e:
|
||||
log.exception(e)
|
||||
log.error("Could not move storage for torrent %s since %s does not exist and could not create the directory.", self.torrent_id, dest)
|
||||
log.error("Could not move storage for torrent %s since %s does "
|
||||
"not exist and could not create the directory.",
|
||||
self.torrent_id, dest_u)
|
||||
return False
|
||||
try:
|
||||
self.handle.move_storage(dest.encode("utf8"))
|
||||
self.handle.move_storage(dest_u)
|
||||
except:
|
||||
return False
|
||||
|
||||
@ -903,8 +965,8 @@ class Torrent(object):
|
||||
log.error("Attempting to rename a folder with an invalid folder name: %s", new_folder)
|
||||
return
|
||||
|
||||
if new_folder[-1:] != "/":
|
||||
new_folder += "/"
|
||||
# Make sure the new folder path is nice and has a trailing slash
|
||||
new_folder = os.path.normpath(new_folder) + "/"
|
||||
|
||||
wait_on_folder = (folder, new_folder, [])
|
||||
for f in self.get_files():
|
||||
@ -923,3 +985,52 @@ class Torrent(object):
|
||||
for key in self.prev_status.keys():
|
||||
if not self.rpcserver.is_session_valid(key):
|
||||
del self.prev_status[key]
|
||||
|
||||
def calculate_last_seen_complete(self):
|
||||
if self._last_seen_complete+60 > time.time():
|
||||
# Simple caching. Only calculate every 1 min at minimum
|
||||
return self._last_seen_complete
|
||||
|
||||
availability = self.handle.piece_availability()
|
||||
if filter(lambda x: x<1, availability):
|
||||
# Torrent does not have all the pieces
|
||||
return
|
||||
log.trace("Torrent %s has all the pieces. Setting last seen complete.",
|
||||
self.torrent_id)
|
||||
self._last_seen_complete = time.time()
|
||||
|
||||
def get_pieces_info(self):
|
||||
pieces = {}
|
||||
# First get the pieces availability.
|
||||
availability = self.handle.piece_availability()
|
||||
# Pieces from connected peers
|
||||
for peer_info in self.handle.get_peer_info():
|
||||
if peer_info.downloading_piece_index < 0:
|
||||
# No piece index, then we're not downloading anything from
|
||||
# this peer
|
||||
continue
|
||||
pieces[peer_info.downloading_piece_index] = 2
|
||||
|
||||
# Now, the rest of the pieces
|
||||
for idx, piece in enumerate(self.handle.status().pieces):
|
||||
if idx in pieces:
|
||||
# Piece beeing downloaded, handled above
|
||||
continue
|
||||
elif piece:
|
||||
# Completed Piece
|
||||
pieces[idx] = 3
|
||||
continue
|
||||
elif availability[idx] > 0:
|
||||
# Piece not downloaded nor beeing downloaded but available
|
||||
pieces[idx] = 1
|
||||
continue
|
||||
# If we reached here, it means the piece is missing, ie, there's
|
||||
# no known peer with this piece, or this piece has not been asked
|
||||
# for so far.
|
||||
pieces[idx] = 0
|
||||
|
||||
sorted_indexes = pieces.keys()
|
||||
sorted_indexes.sort()
|
||||
# Return only the piece states, no need for the piece index
|
||||
# Keep the order
|
||||
return [pieces[idx] for idx in sorted_indexes]
|
||||
|
||||
@ -42,8 +42,8 @@ import time
|
||||
import shutil
|
||||
import operator
|
||||
import logging
|
||||
import re
|
||||
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet.task import LoopingCall
|
||||
|
||||
from deluge._libtorrent import lt
|
||||
@ -52,6 +52,7 @@ from deluge.event import *
|
||||
from deluge.error import *
|
||||
import deluge.component as component
|
||||
from deluge.configmanager import ConfigManager, get_config_dir
|
||||
from deluge.core.authmanager import AUTH_LEVEL_ADMIN
|
||||
from deluge.core.torrent import Torrent
|
||||
from deluge.core.torrent import TorrentOptions
|
||||
import deluge.core.oldstateupgrader
|
||||
@ -73,6 +74,7 @@ class TorrentState:
|
||||
max_upload_speed=-1.0,
|
||||
max_download_speed=-1.0,
|
||||
prioritize_first_last=False,
|
||||
sequential_download=False,
|
||||
file_priorities=None,
|
||||
queue=None,
|
||||
auto_managed=True,
|
||||
@ -84,8 +86,9 @@ class TorrentState:
|
||||
move_completed_path=None,
|
||||
magnet=None,
|
||||
time_added=-1,
|
||||
owner="",
|
||||
public=False
|
||||
last_seen_complete=0.0, # 0 is the default returned when the info
|
||||
owner="", # does not exist on lt >= .16
|
||||
shared=False
|
||||
):
|
||||
self.torrent_id = torrent_id
|
||||
self.filename = filename
|
||||
@ -95,6 +98,7 @@ class TorrentState:
|
||||
self.is_finished = is_finished
|
||||
self.magnet = magnet
|
||||
self.time_added = time_added
|
||||
self.last_seen_complete = last_seen_complete
|
||||
self.owner = owner
|
||||
|
||||
# Options
|
||||
@ -106,6 +110,7 @@ class TorrentState:
|
||||
self.max_upload_speed = max_upload_speed
|
||||
self.max_download_speed = max_download_speed
|
||||
self.prioritize_first_last = prioritize_first_last
|
||||
self.sequential_download = sequential_download
|
||||
self.file_priorities = file_priorities
|
||||
self.auto_managed = auto_managed
|
||||
self.stop_ratio = stop_ratio
|
||||
@ -113,7 +118,7 @@ class TorrentState:
|
||||
self.remove_at_ratio = remove_at_ratio
|
||||
self.move_completed = move_completed
|
||||
self.move_completed_path = move_completed_path
|
||||
self.public = public
|
||||
self.shared = shared
|
||||
|
||||
class TorrentManagerState:
|
||||
def __init__(self):
|
||||
@ -127,7 +132,8 @@ class TorrentManager(component.Component):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
component.Component.__init__(self, "TorrentManager", interval=5, depend=["CorePluginManager"])
|
||||
component.Component.__init__(self, "TorrentManager", interval=5,
|
||||
depend=["CorePluginManager"])
|
||||
log.debug("TorrentManager init..")
|
||||
# Set the libtorrent session
|
||||
self.session = component.get("Core").session
|
||||
@ -142,6 +148,7 @@ class TorrentManager(component.Component):
|
||||
|
||||
# Create the torrents dict { torrent_id: Torrent }
|
||||
self.torrents = {}
|
||||
self.last_seen_complete_loop = None
|
||||
|
||||
# This is a list of torrent_id when we shutdown the torrentmanager.
|
||||
# We use this list to determine if all active torrents have been paused
|
||||
@ -214,6 +221,9 @@ class TorrentManager(component.Component):
|
||||
self.save_resume_data_timer = LoopingCall(self.save_resume_data)
|
||||
self.save_resume_data_timer.start(190)
|
||||
|
||||
if self.last_seen_complete_loop:
|
||||
self.last_seen_complete_loop.start(60)
|
||||
|
||||
def stop(self):
|
||||
# Stop timers
|
||||
if self.save_state_timer.running:
|
||||
@ -222,6 +232,9 @@ class TorrentManager(component.Component):
|
||||
if self.save_resume_data_timer.running:
|
||||
self.save_resume_data_timer.stop()
|
||||
|
||||
if self.last_seen_complete_loop:
|
||||
self.last_seen_complete_loop.stop()
|
||||
|
||||
# Save state on shutdown
|
||||
self.save_state()
|
||||
|
||||
@ -261,9 +274,12 @@ class TorrentManager(component.Component):
|
||||
|
||||
def update(self):
|
||||
for torrent_id, torrent in self.torrents.items():
|
||||
if torrent.options["stop_at_ratio"] and torrent.state not in ("Checking", "Allocating", "Paused", "Queued"):
|
||||
# If the global setting is set, but the per-torrent isn't.. Just skip to the next torrent
|
||||
# This is so that a user can turn-off the stop at ratio option on a per-torrent basis
|
||||
if torrent.options["stop_at_ratio"] and torrent.state not in (
|
||||
"Checking", "Allocating", "Paused", "Queued"):
|
||||
# If the global setting is set, but the per-torrent isn't..
|
||||
# Just skip to the next torrent.
|
||||
# This is so that a user can turn-off the stop at ratio option
|
||||
# on a per-torrent basis
|
||||
if not torrent.options["stop_at_ratio"]:
|
||||
continue
|
||||
if torrent.get_ratio() >= torrent.options["stop_ratio"] and torrent.is_finished:
|
||||
@ -279,7 +295,16 @@ class TorrentManager(component.Component):
|
||||
|
||||
def get_torrent_list(self):
|
||||
"""Returns a list of torrent_ids"""
|
||||
return self.torrents.keys()
|
||||
torrent_ids = self.torrents.keys()
|
||||
if component.get("RPCServer").get_session_auth_level() == AUTH_LEVEL_ADMIN:
|
||||
return torrent_ids
|
||||
|
||||
current_user = component.get("RPCServer").get_session_user()
|
||||
for torrent_id in torrent_ids[:]:
|
||||
torrent_status = self[torrent_id].get_status(["owner", "shared"])
|
||||
if torrent_status["owner"] != current_user and torrent_status["shared"] == False:
|
||||
torrent_ids.pop(torrent_ids.index(torrent_id))
|
||||
return torrent_ids
|
||||
|
||||
def get_torrent_info_from_file(self, filepath):
|
||||
"""Returns a torrent_info for the file specified or None"""
|
||||
@ -319,7 +344,8 @@ class TorrentManager(component.Component):
|
||||
log.warning("Unable to delete the fastresume file: %s", e)
|
||||
|
||||
def add(self, torrent_info=None, state=None, options=None, save_state=True,
|
||||
filedump=None, filename=None, magnet=None, resume_data=None):
|
||||
filedump=None, filename=None, magnet=None, resume_data=None,
|
||||
owner='localclient'):
|
||||
"""Add a torrent to the manager and returns it's torrent_id"""
|
||||
|
||||
if torrent_info is None and state is None and filedump is None and magnet is None:
|
||||
@ -348,6 +374,7 @@ class TorrentManager(component.Component):
|
||||
options["max_upload_speed"] = state.max_upload_speed
|
||||
options["max_download_speed"] = state.max_download_speed
|
||||
options["prioritize_first_last_pieces"] = state.prioritize_first_last
|
||||
options["sequential_download"] = state.sequential_download
|
||||
options["file_priorities"] = state.file_priorities
|
||||
options["compact_allocation"] = state.compact
|
||||
options["download_location"] = state.save_path
|
||||
@ -358,7 +385,7 @@ class TorrentManager(component.Component):
|
||||
options["move_completed"] = state.move_completed
|
||||
options["move_completed_path"] = state.move_completed_path
|
||||
options["add_paused"] = state.paused
|
||||
options["public"] = state.public
|
||||
options["shared"] = state.shared
|
||||
|
||||
ti = self.get_torrent_info_from_file(
|
||||
os.path.join(get_config_dir(),
|
||||
@ -396,7 +423,6 @@ class TorrentManager(component.Component):
|
||||
torrent_info.rename_file(index, utf8_encoded(name))
|
||||
|
||||
add_torrent_params["ti"] = torrent_info
|
||||
add_torrent_params["resume_data"] = ""
|
||||
|
||||
#log.info("Adding torrent: %s", filename)
|
||||
log.debug("options: %s", options)
|
||||
@ -437,7 +463,12 @@ class TorrentManager(component.Component):
|
||||
# Set auto_managed to False because the torrent is paused
|
||||
handle.auto_managed(False)
|
||||
# Create a Torrent object
|
||||
owner = state.owner if state else component.get("RPCServer").get_session_user()
|
||||
owner = state.owner if state else (
|
||||
owner if owner else component.get("RPCServer").get_session_user()
|
||||
)
|
||||
account_exists = component.get("AuthManager").has_account(owner)
|
||||
if not account_exists:
|
||||
owner = 'localclient'
|
||||
torrent = Torrent(handle, options, state, filename, magnet, owner)
|
||||
# Add the torrent object to the dictionary
|
||||
self.torrents[torrent.torrent_id] = torrent
|
||||
@ -484,10 +515,10 @@ class TorrentManager(component.Component):
|
||||
component.get("EventManager").emit(
|
||||
TorrentAddedEvent(torrent.torrent_id, from_state)
|
||||
)
|
||||
log.info("Torrent %s %s by user: %s",
|
||||
log.info("Torrent %s from user \"%s\" %s",
|
||||
torrent.get_status(["name"])["name"],
|
||||
(from_state and "added" or "loaded"),
|
||||
component.get("RPCServer").get_session_user())
|
||||
torrent.get_status(["owner"])["owner"],
|
||||
(from_state and "added" or "loaded"))
|
||||
return torrent.torrent_id
|
||||
|
||||
def load_torrent(self, torrent_id):
|
||||
@ -568,7 +599,7 @@ class TorrentManager(component.Component):
|
||||
# Remove the torrent from deluge's session
|
||||
try:
|
||||
del self.torrents[torrent_id]
|
||||
except KeyError, ValueError:
|
||||
except (KeyError, ValueError):
|
||||
return False
|
||||
|
||||
# Save the session state
|
||||
@ -576,7 +607,8 @@ class TorrentManager(component.Component):
|
||||
|
||||
# Emit the signal to the clients
|
||||
component.get("EventManager").emit(TorrentRemovedEvent(torrent_id))
|
||||
log.info("Torrent %s removed by user: %s", torrent_name, component.get("RPCServer").get_session_user())
|
||||
log.info("Torrent %s removed by user: %s", torrent_name,
|
||||
component.get("RPCServer").get_session_user())
|
||||
return True
|
||||
|
||||
def load_state(self):
|
||||
@ -616,6 +648,17 @@ class TorrentManager(component.Component):
|
||||
log.error("Torrent state file is either corrupt or incompatible! %s", e)
|
||||
break
|
||||
|
||||
|
||||
if lt.version_minor < 16:
|
||||
log.debug("libtorrent version is lower than 0.16. Start looping "
|
||||
"callback to calculate last_seen_complete info.")
|
||||
def calculate_last_seen_complete():
|
||||
for torrent in self.torrents.values():
|
||||
torrent.calculate_last_seen_complete()
|
||||
self.last_seen_complete_loop = LoopingCall(
|
||||
calculate_last_seen_complete
|
||||
)
|
||||
|
||||
component.get("EventManager").emit(SessionStartedEvent())
|
||||
|
||||
def save_state(self):
|
||||
@ -640,6 +683,7 @@ class TorrentManager(component.Component):
|
||||
torrent.options["max_upload_speed"],
|
||||
torrent.options["max_download_speed"],
|
||||
torrent.options["prioritize_first_last_pieces"],
|
||||
torrent.options["sequential_download"],
|
||||
torrent.options["file_priorities"],
|
||||
torrent.get_queue_position(),
|
||||
torrent.options["auto_managed"],
|
||||
@ -651,8 +695,9 @@ class TorrentManager(component.Component):
|
||||
torrent.options["move_completed_path"],
|
||||
torrent.magnet,
|
||||
torrent.time_added,
|
||||
torrent.get_last_seen_complete(),
|
||||
torrent.owner,
|
||||
torrent.options["public"]
|
||||
torrent.options["shared"]
|
||||
)
|
||||
state.torrents.append(torrent_state)
|
||||
|
||||
@ -747,6 +792,38 @@ class TorrentManager(component.Component):
|
||||
except IOError:
|
||||
log.warning("Error trying to save fastresume file")
|
||||
|
||||
def remove_empty_folders(self, torrent_id, folder):
|
||||
"""
|
||||
Recursively removes folders but only if they are empty.
|
||||
Cleans up after libtorrent folder renames.
|
||||
|
||||
"""
|
||||
if torrent_id not in self.torrents:
|
||||
raise InvalidTorrentError("torrent_id is not in session")
|
||||
|
||||
info = self.torrents[torrent_id].get_status(['save_path'])
|
||||
# Regex removes leading slashes that causes join function to ignore save_path
|
||||
folder_full_path = os.path.join(info['save_path'], re.sub("^/*", "", folder))
|
||||
folder_full_path = os.path.normpath(folder_full_path)
|
||||
|
||||
try:
|
||||
if not os.listdir(folder_full_path):
|
||||
os.removedirs(folder_full_path)
|
||||
log.debug("Removed Empty Folder %s", folder_full_path)
|
||||
else:
|
||||
for root, dirs, files in os.walk(folder_full_path, topdown=False):
|
||||
for name in dirs:
|
||||
try:
|
||||
os.removedirs(os.path.join(root, name))
|
||||
log.debug("Removed Empty Folder %s", os.path.join(root, name))
|
||||
except OSError as (errno, strerror):
|
||||
if errno == 39:
|
||||
# Error raised if folder is not empty
|
||||
log.debug("%s", strerror)
|
||||
|
||||
except OSError as (errno, strerror):
|
||||
log.debug("Cannot Remove Folder: %s (ErrNo %s)", strerror, errno)
|
||||
|
||||
def queue_top(self, torrent_id):
|
||||
"""Queue torrent to top"""
|
||||
if self.torrents[torrent_id].get_queue_position() == 0:
|
||||
@ -1008,6 +1085,8 @@ class TorrentManager(component.Component):
|
||||
if len(wait_on_folder[2]) == 1:
|
||||
# This is the last alert we were waiting for, time to send signal
|
||||
component.get("EventManager").emit(TorrentFolderRenamedEvent(torrent_id, wait_on_folder[0], wait_on_folder[1]))
|
||||
# Empty folders are removed after libtorrent folder renames
|
||||
self.remove_empty_folders(torrent_id, wait_on_folder[0])
|
||||
del torrent.waiting_on_folder_rename[i]
|
||||
self.save_resume_data((torrent_id,))
|
||||
break
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=Deluge BitTorrent Client
|
||||
GenericName=Bittorrent Client
|
||||
Comment=Transfer files using the Bittorrent protocol
|
||||
Exec=deluge-gtk
|
||||
Icon=deluge
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Network;
|
||||
StartupNotify=true
|
||||
MimeType=application/x-bittorrent;
|
||||
@ -2,6 +2,7 @@
|
||||
# error.py
|
||||
#
|
||||
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
@ -35,7 +36,21 @@
|
||||
|
||||
|
||||
class DelugeError(Exception):
|
||||
pass
|
||||
def _get_message(self):
|
||||
return self._message
|
||||
def _set_message(self, message):
|
||||
self._message = message
|
||||
message = property(_get_message, _set_message)
|
||||
del _get_message, _set_message
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
inst = super(DelugeError, cls).__new__(cls, *args, **kwargs)
|
||||
inst._args = args
|
||||
inst._kwargs = kwargs
|
||||
return inst
|
||||
|
||||
class NoCoreError(DelugeError):
|
||||
pass
|
||||
@ -48,3 +63,49 @@ class InvalidTorrentError(DelugeError):
|
||||
|
||||
class InvalidPathError(DelugeError):
|
||||
pass
|
||||
|
||||
class _ClientSideRecreateError(DelugeError):
|
||||
pass
|
||||
|
||||
class IncompatibleClient(_ClientSideRecreateError):
|
||||
def __init__(self, daemon_version):
|
||||
self.daemon_version = daemon_version
|
||||
self.message = _(
|
||||
"Your deluge client is not compatible with the daemon. "
|
||||
"Please upgrade your client to %(daemon_version)s"
|
||||
) % dict(daemon_version=self.daemon_version)
|
||||
|
||||
class NotAuthorizedError(_ClientSideRecreateError):
|
||||
|
||||
def __init__(self, current_level, required_level):
|
||||
self.message = _(
|
||||
"Auth level too low: %(current_level)s < %(required_level)s" %
|
||||
dict(current_level=current_level, required_level=required_level)
|
||||
)
|
||||
self.current_level = current_level
|
||||
self.required_level = required_level
|
||||
|
||||
|
||||
class _UsernameBasedPasstroughError(_ClientSideRecreateError):
|
||||
|
||||
def _get_username(self):
|
||||
return self._username
|
||||
def _set_username(self, username):
|
||||
self._username = username
|
||||
username = property(_get_username, _set_username)
|
||||
del _get_username, _set_username
|
||||
|
||||
def __init__(self, message, username):
|
||||
super(_UsernameBasedPasstroughError, self).__init__(message)
|
||||
self.message = message
|
||||
self.username = username
|
||||
|
||||
|
||||
class BadLoginError(_UsernameBasedPasstroughError):
|
||||
pass
|
||||
|
||||
class AuthenticationRequired(_UsernameBasedPasstroughError):
|
||||
pass
|
||||
|
||||
class AuthManagerError(_UsernameBasedPasstroughError):
|
||||
pass
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
# event.py
|
||||
#
|
||||
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2010 Pedro Algarvio <pedro@algarvio.me>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
@ -48,8 +47,6 @@ class DelugeEventMetaClass(type):
|
||||
"""
|
||||
This metaclass simply keeps a list of all events classes created.
|
||||
"""
|
||||
__slots__ = ()
|
||||
|
||||
def __init__(cls, name, bases, dct):
|
||||
super(DelugeEventMetaClass, cls).__init__(name, bases, dct)
|
||||
if name != "DelugeEvent":
|
||||
@ -65,26 +62,23 @@ class DelugeEvent(object):
|
||||
:type args: list
|
||||
|
||||
"""
|
||||
__slots__ = ()
|
||||
__metaclass__ = DelugeEventMetaClass
|
||||
|
||||
def _get_name(self):
|
||||
return self.__class__.__name__
|
||||
name = property(fget=_get_name)
|
||||
|
||||
def _get_args(self):
|
||||
return [getattr(self, arg) for arg in self.__slots__]
|
||||
args = property(fget=_get_args)
|
||||
if not hasattr(self, "_args"):
|
||||
return []
|
||||
return self._args
|
||||
|
||||
def copy(self):
|
||||
return self.__class__(*self.args)
|
||||
name = property(fget=_get_name)
|
||||
args = property(fget=_get_args)
|
||||
|
||||
class TorrentAddedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a new torrent is successfully added to the session.
|
||||
"""
|
||||
__slots__ = ('torrent_id', 'from_state')
|
||||
|
||||
def __init__(self, torrent_id, from_state):
|
||||
"""
|
||||
:param torrent_id: the torrent_id of the torrent that was added
|
||||
@ -92,41 +86,34 @@ class TorrentAddedEvent(DelugeEvent):
|
||||
:param from_state: was the torrent loaded from state? Or is it a new torrent.
|
||||
:type from_state: bool
|
||||
"""
|
||||
self.torrent_id = torrent_id
|
||||
self.from_state = from_state
|
||||
self._args = [torrent_id, from_state]
|
||||
|
||||
class TorrentRemovedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a torrent has been removed from the session.
|
||||
"""
|
||||
__slots__ = ('torrent_id',)
|
||||
|
||||
def __init__(self, torrent_id):
|
||||
"""
|
||||
:param torrent_id: the torrent_id
|
||||
:type torrent_id: string
|
||||
"""
|
||||
self.torrent_id = torrent_id
|
||||
self._args = [torrent_id]
|
||||
|
||||
class PreTorrentRemovedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a torrent is about to be removed from the session.
|
||||
"""
|
||||
__slots__ = ('torrent_id',)
|
||||
|
||||
def __init__(self, torrent_id):
|
||||
"""
|
||||
:param torrent_id: the torrent_id
|
||||
:type torrent_id: string
|
||||
"""
|
||||
self.torrent_id = torrent_id
|
||||
self._args = [torrent_id]
|
||||
|
||||
class TorrentStateChangedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a torrent changes state.
|
||||
"""
|
||||
__slots__ = ('torrent_id', 'state')
|
||||
|
||||
def __init__(self, torrent_id, state):
|
||||
"""
|
||||
:param torrent_id: the torrent_id
|
||||
@ -134,20 +121,18 @@ class TorrentStateChangedEvent(DelugeEvent):
|
||||
:param state: the new state
|
||||
:type state: string
|
||||
"""
|
||||
self.torrent_id = torrent_id
|
||||
self.state = state
|
||||
self._args = [torrent_id, state]
|
||||
|
||||
class TorrentQueueChangedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when the queue order has changed.
|
||||
"""
|
||||
pass
|
||||
|
||||
class TorrentFolderRenamedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a folder within a torrent has been renamed.
|
||||
"""
|
||||
__slots__ = ('torrent_id', 'old', 'new')
|
||||
|
||||
def __init__(self, torrent_id, old, new):
|
||||
"""
|
||||
:param torrent_id: the torrent_id
|
||||
@ -157,54 +142,44 @@ class TorrentFolderRenamedEvent(DelugeEvent):
|
||||
:param new: the new folder name
|
||||
:type new: string
|
||||
"""
|
||||
self.torrent_id = torrent_id
|
||||
self.old = old
|
||||
self.new = new
|
||||
self._args = [torrent_id, old, new]
|
||||
|
||||
class TorrentFileRenamedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a file within a torrent has been renamed.
|
||||
"""
|
||||
__slots__ = ('torrent_id', 'index', 'filename')
|
||||
|
||||
def __init__(self, torrent_id, index, filename):
|
||||
def __init__(self, torrent_id, index, name):
|
||||
"""
|
||||
:param torrent_id: the torrent_id
|
||||
:type torrent_id: string
|
||||
:param index: the index of the file
|
||||
:type index: int
|
||||
:param filename: the new filename
|
||||
:type filename: string
|
||||
:param name: the new filename
|
||||
:type name: string
|
||||
"""
|
||||
self.torrent_id = torrent_id
|
||||
self.index = index
|
||||
self.filename = filename
|
||||
self._args = [torrent_id, index, name]
|
||||
|
||||
class TorrentFinishedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a torrent finishes downloading.
|
||||
"""
|
||||
__slots__ = ('torrent_id',)
|
||||
|
||||
def __init__(self, torrent_id):
|
||||
"""
|
||||
:param torrent_id: the torrent_id
|
||||
:type torrent_id: string
|
||||
"""
|
||||
self.torrent_id = torrent_id
|
||||
self._args = [torrent_id]
|
||||
|
||||
class TorrentResumedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a torrent resumes from a paused state.
|
||||
"""
|
||||
__slots__ = ('torrent_id',)
|
||||
|
||||
def __init__(self, torrent_id):
|
||||
"""
|
||||
:param torrent_id: the torrent_id
|
||||
:type torrent_id: string
|
||||
"""
|
||||
self.torrent_id = torrent_id
|
||||
self._args = [torrent_id]
|
||||
|
||||
class TorrentFileCompletedEvent(DelugeEvent):
|
||||
"""
|
||||
@ -213,8 +188,6 @@ class TorrentFileCompletedEvent(DelugeEvent):
|
||||
This will only work with libtorrent 0.15 or greater.
|
||||
|
||||
"""
|
||||
__slots__ = ('torrent_id', 'index')
|
||||
|
||||
def __init__(self, torrent_id, index):
|
||||
"""
|
||||
:param torrent_id: the torrent_id
|
||||
@ -222,75 +195,61 @@ class TorrentFileCompletedEvent(DelugeEvent):
|
||||
:param index: the file index
|
||||
:type index: int
|
||||
"""
|
||||
self.torrent_id = torrent_id
|
||||
self.index = index
|
||||
self._args = [torrent_id, index]
|
||||
|
||||
class NewVersionAvailableEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a more recent version of Deluge is available.
|
||||
"""
|
||||
__slots__ = ('new_release',)
|
||||
|
||||
def __init__(self, new_release):
|
||||
"""
|
||||
:param new_release: the new version that is available
|
||||
:type new_release: string
|
||||
"""
|
||||
self.new_release = new_release
|
||||
self._args = [new_release]
|
||||
|
||||
class SessionStartedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a session has started. This typically only happens once when
|
||||
the daemon is initially started.
|
||||
"""
|
||||
pass
|
||||
|
||||
class SessionPausedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when the session has been paused.
|
||||
"""
|
||||
pass
|
||||
|
||||
class SessionResumedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when the session has been resumed.
|
||||
"""
|
||||
pass
|
||||
|
||||
class ConfigValueChangedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a config value changes in the Core.
|
||||
"""
|
||||
__slots__ = ('key', 'value')
|
||||
|
||||
def __init__(self, key, value):
|
||||
"""
|
||||
:param key: the key that changed
|
||||
:type key: string
|
||||
:param value: the new value of the `:param:key`
|
||||
"""
|
||||
self.key = key
|
||||
self.value = value
|
||||
self._args = [key, value]
|
||||
|
||||
class PluginEnabledEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a plugin is enabled in the Core.
|
||||
"""
|
||||
__slots__ = ('plugin_name',)
|
||||
|
||||
def __init__(self, plugin_name):
|
||||
"""
|
||||
:param plugin_name: the plugin name
|
||||
:type plugin_name: string
|
||||
"""
|
||||
self.plugin_name = plugin_name
|
||||
self._args = [plugin_name]
|
||||
|
||||
class PluginDisabledEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a plugin is disabled in the Core.
|
||||
"""
|
||||
__slots__ = ('plugin_name',)
|
||||
|
||||
def __init__(self, plugin_name):
|
||||
"""
|
||||
:param plugin_name: the plugin name
|
||||
:type plugin_name: string
|
||||
"""
|
||||
self.plugin_name = plugin_name
|
||||
self._args = [plugin_name]
|
||||
|
||||
|
||||
@ -48,9 +48,9 @@ __all__ = ["setupLogger", "setLoggerLevel", "getPluginLogger", "LOG"]
|
||||
LoggingLoggerClass = logging.getLoggerClass()
|
||||
|
||||
if 'dev' in common.get_version():
|
||||
DEFAULT_LOGGING_FORMAT = "%%(asctime)s.%%(msecs)03.0f [%%(name)-%ds:%%(lineno)-4d][%%(levelname)-8s] %%(message)s"
|
||||
DEFAULT_LOGGING_FORMAT = "%%(asctime)s.%%(msecs)03.0f [%%(levelname)-8s][%%(name)-%ds:%%(lineno)-4d] %%(message)s"
|
||||
else:
|
||||
DEFAULT_LOGGING_FORMAT = "%%(asctime)s [%%(name)-%ds][%%(levelname)-8s] %%(message)s"
|
||||
DEFAULT_LOGGING_FORMAT = "%%(asctime)s [%%(levelname)-8s][%%(name)-%ds] %%(message)s"
|
||||
MAX_LOGGER_NAME_LENGTH = 3
|
||||
|
||||
class Logging(LoggingLoggerClass):
|
||||
@ -117,6 +117,7 @@ class Logging(LoggingLoggerClass):
|
||||
return rv
|
||||
|
||||
levels = {
|
||||
"none": logging.NOTSET,
|
||||
"info": logging.INFO,
|
||||
"warn": logging.WARNING,
|
||||
"warning": logging.WARNING,
|
||||
@ -140,8 +141,10 @@ def setupLogger(level="error", filename=None, filemode="w"):
|
||||
|
||||
if logging.getLoggerClass() is not Logging:
|
||||
logging.setLoggerClass(Logging)
|
||||
logging.addLevelName(5, 'TRACE')
|
||||
logging.addLevelName(1, 'GARBAGE')
|
||||
|
||||
level = levels.get(level, "error")
|
||||
level = levels.get(level, logging.ERROR)
|
||||
|
||||
rootLogger = logging.getLogger()
|
||||
|
||||
@ -149,7 +152,7 @@ def setupLogger(level="error", filename=None, filemode="w"):
|
||||
import logging.handlers
|
||||
handler = logging.handlers.RotatingFileHandler(
|
||||
filename, filemode,
|
||||
maxBytes=5*1024*1024, # 5 Mb
|
||||
maxBytes=50*1024*1024, # 50 Mb
|
||||
backupCount=3,
|
||||
encoding='utf-8',
|
||||
delay=0
|
||||
@ -162,6 +165,7 @@ def setupLogger(level="error", filename=None, filemode="w"):
|
||||
)
|
||||
else:
|
||||
handler = logging.StreamHandler()
|
||||
|
||||
handler.setLevel(level)
|
||||
|
||||
formatter = logging.Formatter(
|
||||
@ -263,21 +267,30 @@ class __BackwardsCompatibleLOG(object):
|
||||
import warnings
|
||||
logger_name = 'deluge'
|
||||
stack = inspect.stack()
|
||||
module_stack = stack.pop(1)
|
||||
stack.pop(0) # The logging call from this module
|
||||
module_stack = stack.pop(0) # The module that called the log function
|
||||
caller_module = inspect.getmodule(module_stack[0])
|
||||
# In some weird cases caller_module might be None, try to continue
|
||||
caller_module_name = getattr(caller_module, '__name__', '')
|
||||
warnings.warn_explicit(DEPRECATION_WARNING, DeprecationWarning,
|
||||
module_stack[1], module_stack[2],
|
||||
caller_module.__name__)
|
||||
for member in stack:
|
||||
module = inspect.getmodule(member[0])
|
||||
if not module:
|
||||
continue
|
||||
if module.__name__ in ('deluge.plugins.pluginbase',
|
||||
'deluge.plugins.init'):
|
||||
logger_name += '.plugin.%s' % caller_module.__name__
|
||||
# Monkey Patch The Plugin Module
|
||||
caller_module.log = logging.getLogger(logger_name)
|
||||
break
|
||||
caller_module_name)
|
||||
if caller_module:
|
||||
for member in stack:
|
||||
module = inspect.getmodule(member[0])
|
||||
if not module:
|
||||
continue
|
||||
if module.__name__ in ('deluge.plugins.pluginbase',
|
||||
'deluge.plugins.init'):
|
||||
logger_name += '.plugin.%s' % caller_module_name
|
||||
# Monkey Patch The Plugin Module
|
||||
caller_module.log = logging.getLogger(logger_name)
|
||||
break
|
||||
else:
|
||||
logging.getLogger(logger_name).warning(
|
||||
"Unable to monkey-patch the calling module's `log` attribute! "
|
||||
"You should really update and rebuild your plugins..."
|
||||
)
|
||||
return getattr(logging.getLogger(logger_name), name)
|
||||
|
||||
LOG = __BackwardsCompatibleLOG()
|
||||
|
||||
@ -149,14 +149,20 @@ this should be an IP address", metavar="IFACE",
|
||||
parser.add_option("-u", "--ui-interface", dest="ui_interface",
|
||||
help="Interface daemon will listen for UI connections on, this should be\
|
||||
an IP address", metavar="IFACE", action="store", type="str")
|
||||
parser.add_option("-d", "--do-not-daemonize", dest="donot",
|
||||
help="Do not daemonize", action="store_true", default=False)
|
||||
if not (deluge.common.windows_check() or deluge.common.osx_check()):
|
||||
parser.add_option("-d", "--do-not-daemonize", dest="donot",
|
||||
help="Do not daemonize", action="store_true", default=False)
|
||||
parser.add_option("-c", "--config", dest="config",
|
||||
help="Set the config location", action="store", type="str")
|
||||
parser.add_option("-l", "--logfile", dest="logfile",
|
||||
help="Set the logfile location", action="store", type="str")
|
||||
parser.add_option("-P", "--pidfile", dest="pidfile",
|
||||
help="Use pidfile to store process id", action="store", type="str")
|
||||
if not deluge.common.windows_check():
|
||||
parser.add_option("-U", "--user", dest="user",
|
||||
help="User to switch to. Only use it when starting as root", action="store", type="str")
|
||||
parser.add_option("-g", "--group", dest="group",
|
||||
help="Group to switch to. Only use it when starting as root", action="store", type="str")
|
||||
parser.add_option("-L", "--loglevel", dest="loglevel",
|
||||
help="Set the log level: none, info, warning, error, critical, debug", action="store", type="str")
|
||||
parser.add_option("-q", "--quiet", dest="quiet",
|
||||
@ -197,24 +203,30 @@ this should be an IP address", metavar="IFACE",
|
||||
open(options.pidfile, "wb").write("%s\n" % os.getpid())
|
||||
|
||||
# If the donot daemonize is set, then we just skip the forking
|
||||
if not options.donot:
|
||||
# Windows check, we log to the config folder by default
|
||||
if deluge.common.windows_check() or deluge.common.osx_check():
|
||||
open_logfile()
|
||||
write_pidfile()
|
||||
else:
|
||||
if os.fork() == 0:
|
||||
os.setsid()
|
||||
if os.fork() == 0:
|
||||
open_logfile()
|
||||
write_pidfile()
|
||||
else:
|
||||
os._exit(0)
|
||||
else:
|
||||
os._exit(0)
|
||||
else:
|
||||
# Do not daemonize
|
||||
write_pidfile()
|
||||
if not (deluge.common.windows_check() or deluge.common.osx_check() or options.donot):
|
||||
if os.fork():
|
||||
# We've forked and this is now the parent process, so die!
|
||||
os._exit(0)
|
||||
os.setsid()
|
||||
# Do second fork
|
||||
if os.fork():
|
||||
os._exit(0)
|
||||
|
||||
# Write pid file before chuid
|
||||
write_pidfile()
|
||||
|
||||
if options.user:
|
||||
if not options.user.isdigit():
|
||||
import pwd
|
||||
options.user = pwd.getpwnam(options.user)[2]
|
||||
os.setuid(options.user)
|
||||
if options.group:
|
||||
if not options.group.isdigit():
|
||||
import grp
|
||||
options.group = grp.getgrnam(options.group)[2]
|
||||
os.setuid(options.group)
|
||||
|
||||
open_logfile()
|
||||
|
||||
# Setup the logger
|
||||
try:
|
||||
|
||||
@ -38,7 +38,7 @@ import os
|
||||
from hashlib import sha1 as sha
|
||||
|
||||
from deluge.common import get_path_size
|
||||
from deluge.bencode import bencode, bdecode
|
||||
from deluge.bencode import bencode
|
||||
|
||||
class InvalidPath(Exception):
|
||||
"""
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
# core.py
|
||||
#
|
||||
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
|
||||
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
||||
#
|
||||
# Basic plugin template created by:
|
||||
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
||||
@ -43,6 +44,7 @@ from deluge.log import getPluginLogger
|
||||
from deluge.plugins.pluginbase import CorePluginBase
|
||||
import deluge.component as component
|
||||
import deluge.configmanager
|
||||
from deluge.common import AUTH_LEVEL_ADMIN
|
||||
from deluge.core.rpcserver import export
|
||||
from twisted.internet.task import LoopingCall, deferLater
|
||||
from twisted.internet import reactor
|
||||
@ -59,6 +61,8 @@ OPTIONS_AVAILABLE = { #option: builtin
|
||||
"enabled":False,
|
||||
"path":False,
|
||||
"append_extension":False,
|
||||
"copy_torrent": False,
|
||||
"delete_copy_torrent_toggle": False,
|
||||
"abspath":False,
|
||||
"download_location":True,
|
||||
"max_download_speed":True,
|
||||
@ -74,13 +78,16 @@ OPTIONS_AVAILABLE = { #option: builtin
|
||||
"move_completed_path":True,
|
||||
"label":False,
|
||||
"add_paused":True,
|
||||
"queue_to_top":False
|
||||
"queue_to_top":False,
|
||||
"owner": "localclient"
|
||||
}
|
||||
|
||||
MAX_NUM_ATTEMPTS = 10
|
||||
|
||||
class AutoaddOptionsChangedEvent(DelugeEvent):
|
||||
"""Emitted when the options for the plugin are changed."""
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def CheckInput(cond, message):
|
||||
if not cond:
|
||||
@ -91,36 +98,31 @@ class Core(CorePluginBase):
|
||||
|
||||
#reduce typing, assigning some values to self...
|
||||
self.config = deluge.configmanager.ConfigManager("autoadd.conf", DEFAULT_PREFS)
|
||||
self.config.run_converter((0, 1), 2, self.__migrate_config_1_to_2)
|
||||
self.config.save()
|
||||
self.watchdirs = self.config["watchdirs"]
|
||||
self.core_cfg = deluge.configmanager.ConfigManager("core.conf")
|
||||
|
||||
component.get("EventManager").register_event_handler(
|
||||
"PreTorrentRemovedEvent", self.__on_pre_torrent_removed
|
||||
)
|
||||
|
||||
# Dict of Filename:Attempts
|
||||
self.invalid_torrents = {}
|
||||
# Loopingcall timers for each enabled watchdir
|
||||
self.update_timers = {}
|
||||
# If core autoadd folder is enabled, move it to the plugin
|
||||
if self.core_cfg.config.get('autoadd_enable'):
|
||||
# Disable core autoadd
|
||||
self.core_cfg['autoadd_enable'] = False
|
||||
self.core_cfg.save()
|
||||
# Check if core autoadd folder is already added in plugin
|
||||
for watchdir in self.watchdirs:
|
||||
if os.path.abspath(self.core_cfg['autoadd_location']) == watchdir['abspath']:
|
||||
watchdir['enabled'] = True
|
||||
break
|
||||
else:
|
||||
# didn't find core watchdir, add it
|
||||
self.add({'path':self.core_cfg['autoadd_location'], 'enabled':True})
|
||||
deferLater(reactor, 5, self.enable_looping)
|
||||
|
||||
def enable_looping(self):
|
||||
#Enable all looping calls for enabled watchdirs here
|
||||
# Enable all looping calls for enabled watchdirs here
|
||||
for watchdir_id, watchdir in self.watchdirs.iteritems():
|
||||
if watchdir['enabled']:
|
||||
self.enable_watchdir(watchdir_id)
|
||||
|
||||
def disable(self):
|
||||
#disable all running looping calls
|
||||
component.get("EventManager").deregister_event_handler(
|
||||
"PreTorrentRemovedEvent", self.__on_pre_torrent_removed
|
||||
)
|
||||
for loopingcall in self.update_timers.itervalues():
|
||||
loopingcall.stop()
|
||||
self.config.save()
|
||||
@ -128,21 +130,25 @@ class Core(CorePluginBase):
|
||||
def update(self):
|
||||
pass
|
||||
|
||||
@export()
|
||||
@export
|
||||
def set_options(self, watchdir_id, options):
|
||||
"""Update the options for a watch folder."""
|
||||
watchdir_id = str(watchdir_id)
|
||||
options = self._make_unicode(options)
|
||||
CheckInput(watchdir_id in self.watchdirs , _("Watch folder does not exist."))
|
||||
CheckInput(
|
||||
watchdir_id in self.watchdirs, _("Watch folder does not exist.")
|
||||
)
|
||||
if options.has_key('path'):
|
||||
options['abspath'] = os.path.abspath(options['path'])
|
||||
CheckInput(os.path.isdir(options['abspath']), _("Path does not exist."))
|
||||
CheckInput(
|
||||
os.path.isdir(options['abspath']), _("Path does not exist.")
|
||||
)
|
||||
for w_id, w in self.watchdirs.iteritems():
|
||||
if options['abspath'] == w['abspath'] and watchdir_id != w_id:
|
||||
raise Exception("Path is already being watched.")
|
||||
for key in options.keys():
|
||||
if not key in OPTIONS_AVAILABLE:
|
||||
if not key in [key2+'_toggle' for key2 in OPTIONS_AVAILABLE.iterkeys()]:
|
||||
if key not in OPTIONS_AVAILABLE:
|
||||
if key not in [key2+'_toggle' for key2 in OPTIONS_AVAILABLE.iterkeys()]:
|
||||
raise Exception("autoadd: Invalid options key:%s" % key)
|
||||
#disable the watch loop if it was active
|
||||
if watchdir_id in self.update_timers:
|
||||
@ -168,16 +174,19 @@ class Core(CorePluginBase):
|
||||
raise e
|
||||
|
||||
# Get the info to see if any exceptions are raised
|
||||
info = lt.torrent_info(lt.bdecode(filedump))
|
||||
lt.torrent_info(lt.bdecode(filedump))
|
||||
|
||||
return filedump
|
||||
|
||||
def update_watchdir(self, watchdir_id):
|
||||
"""Check the watch folder for new torrents to add."""
|
||||
log.trace("Updating watchdir id: %s", watchdir_id)
|
||||
watchdir_id = str(watchdir_id)
|
||||
watchdir = self.watchdirs[watchdir_id]
|
||||
if not watchdir['enabled']:
|
||||
# We shouldn't be updating because this watchdir is not enabled
|
||||
log.debug("Watchdir id %s is not enabled. Disabling it.",
|
||||
watchdir_id)
|
||||
self.disable_watchdir(watchdir_id)
|
||||
return
|
||||
|
||||
@ -190,19 +199,23 @@ class Core(CorePluginBase):
|
||||
opts = {}
|
||||
if 'stop_at_ratio_toggle' in watchdir:
|
||||
watchdir['stop_ratio_toggle'] = watchdir['stop_at_ratio_toggle']
|
||||
# We default to True wher reading _toggle values, so a config
|
||||
# We default to True when reading _toggle values, so a config
|
||||
# without them is valid, and applies all its settings.
|
||||
for option, value in watchdir.iteritems():
|
||||
if OPTIONS_AVAILABLE.get(option):
|
||||
if watchdir.get(option+'_toggle', True):
|
||||
opts[option] = value
|
||||
for filename in os.listdir(watchdir["abspath"]):
|
||||
if filename.split(".")[-1] == "torrent":
|
||||
try:
|
||||
filepath = os.path.join(watchdir["abspath"], filename)
|
||||
except UnicodeDecodeError, e:
|
||||
log.error("Unable to auto add torrent due to inproper filename encoding: %s", e)
|
||||
continue
|
||||
try:
|
||||
filepath = os.path.join(watchdir["abspath"], filename)
|
||||
except UnicodeDecodeError, e:
|
||||
log.error("Unable to auto add torrent due to improper "
|
||||
"filename encoding: %s", e)
|
||||
continue
|
||||
if os.path.isdir(filepath):
|
||||
# Skip directories
|
||||
continue
|
||||
elif os.path.splitext(filename)[1] == ".torrent":
|
||||
try:
|
||||
filedump = self.load_torrent(filepath)
|
||||
except (RuntimeError, Exception), e:
|
||||
@ -213,6 +226,10 @@ class Core(CorePluginBase):
|
||||
if filename in self.invalid_torrents:
|
||||
self.invalid_torrents[filename] += 1
|
||||
if self.invalid_torrents[filename] >= MAX_NUM_ATTEMPTS:
|
||||
log.warning(
|
||||
"Maximum attempts reached while trying to add the "
|
||||
"torrent file with the path %s", filepath
|
||||
)
|
||||
os.rename(filepath, filepath + ".invalid")
|
||||
del self.invalid_torrents[filename]
|
||||
else:
|
||||
@ -220,7 +237,10 @@ class Core(CorePluginBase):
|
||||
continue
|
||||
|
||||
# The torrent looks good, so lets add it to the session.
|
||||
torrent_id = component.get("TorrentManager").add(filedump=filedump, filename=filename, options=opts)
|
||||
torrent_id = component.get("TorrentManager").add(
|
||||
filedump=filedump, filename=filename, options=opts,
|
||||
owner=watchdir.get("owner", "localclient")
|
||||
)
|
||||
# If the torrent added successfully, set the extra options.
|
||||
if torrent_id:
|
||||
if 'Label' in component.get("CorePluginManager").get_enabled_plugins():
|
||||
@ -234,43 +254,72 @@ class Core(CorePluginBase):
|
||||
component.get("TorrentManager").queue_top(torrent_id)
|
||||
else:
|
||||
component.get("TorrentManager").queue_bottom(torrent_id)
|
||||
# Rename or delete the torrent once added to deluge.
|
||||
|
||||
# Rename, copy or delete the torrent once added to deluge.
|
||||
if watchdir.get('append_extension_toggle'):
|
||||
if not watchdir.get('append_extension'):
|
||||
watchdir['append_extension'] = ".added"
|
||||
os.rename(filepath, filepath + watchdir['append_extension'])
|
||||
elif watchdir.get('copy_torrent_toggle'):
|
||||
copy_torrent_path = watchdir['copy_torrent']
|
||||
copy_torrent_file = os.path.join(copy_torrent_path, filename)
|
||||
log.debug("Moving added torrent file \"%s\" to \"%s\"",
|
||||
os.path.basename(filepath), copy_torrent_path)
|
||||
try:
|
||||
os.rename(filepath, copy_torrent_file)
|
||||
except OSError, why:
|
||||
if why.errno == 18:
|
||||
# This can happen for different mount points
|
||||
from shutil import copyfile
|
||||
try:
|
||||
copyfile(filepath, copy_torrent_file)
|
||||
os.remove(filepath)
|
||||
except OSError:
|
||||
# Last Resort!
|
||||
try:
|
||||
open(copy_torrent_file, 'wb').write(
|
||||
open(filepath, 'rb').read()
|
||||
)
|
||||
os.remove(filepath)
|
||||
except OSError, why:
|
||||
raise why
|
||||
else:
|
||||
raise why
|
||||
else:
|
||||
os.remove(filepath)
|
||||
|
||||
def on_update_watchdir_error(self, failure, watchdir_id):
|
||||
"""Disables any watch folders with unhandled exceptions."""
|
||||
"""Disables any watch folders with un-handled exceptions."""
|
||||
self.disable_watchdir(watchdir_id)
|
||||
log.error("Disabling '%s', error during update: %s" % (self.watchdirs[watchdir_id]["path"], failure))
|
||||
log.error("Disabling '%s', error during update: %s",
|
||||
self.watchdirs[watchdir_id]["path"], failure)
|
||||
|
||||
@export
|
||||
def enable_watchdir(self, watchdir_id):
|
||||
watchdir_id = str(watchdir_id)
|
||||
w_id = str(watchdir_id)
|
||||
# Enable the looping call
|
||||
if watchdir_id not in self.update_timers or not self.update_timers[watchdir_id].running:
|
||||
self.update_timers[watchdir_id] = LoopingCall(self.update_watchdir, watchdir_id)
|
||||
self.update_timers[watchdir_id].start(5).addErrback(self.on_update_watchdir_error, watchdir_id)
|
||||
if w_id not in self.update_timers or not self.update_timers[w_id].running:
|
||||
self.update_timers[w_id] = LoopingCall(self.update_watchdir, w_id)
|
||||
self.update_timers[w_id].start(5).addErrback(
|
||||
self.on_update_watchdir_error, w_id
|
||||
)
|
||||
# Update the config
|
||||
if not self.watchdirs[watchdir_id]['enabled']:
|
||||
self.watchdirs[watchdir_id]['enabled'] = True
|
||||
if not self.watchdirs[w_id]['enabled']:
|
||||
self.watchdirs[w_id]['enabled'] = True
|
||||
self.config.save()
|
||||
component.get("EventManager").emit(AutoaddOptionsChangedEvent())
|
||||
|
||||
@export
|
||||
def disable_watchdir(self, watchdir_id):
|
||||
watchdir_id = str(watchdir_id)
|
||||
w_id = str(watchdir_id)
|
||||
# Disable the looping call
|
||||
if watchdir_id in self.update_timers:
|
||||
if self.update_timers[watchdir_id].running:
|
||||
self.update_timers[watchdir_id].stop()
|
||||
del self.update_timers[watchdir_id]
|
||||
if w_id in self.update_timers:
|
||||
if self.update_timers[w_id].running:
|
||||
self.update_timers[w_id].stop()
|
||||
del self.update_timers[w_id]
|
||||
# Update the config
|
||||
if self.watchdirs[watchdir_id]['enabled']:
|
||||
self.watchdirs[watchdir_id]['enabled'] = False
|
||||
if self.watchdirs[w_id]['enabled']:
|
||||
self.watchdirs[w_id]['enabled'] = False
|
||||
self.config.save()
|
||||
component.get("EventManager").emit(AutoaddOptionsChangedEvent())
|
||||
|
||||
@ -288,9 +337,24 @@ class Core(CorePluginBase):
|
||||
"""Returns the config dictionary."""
|
||||
return self.config.config
|
||||
|
||||
@export()
|
||||
@export
|
||||
def get_watchdirs(self):
|
||||
return self.watchdirs.keys()
|
||||
rpcserver = component.get("RPCServer")
|
||||
session_user = rpcserver.get_session_user()
|
||||
session_auth_level = rpcserver.get_session_auth_level()
|
||||
if session_auth_level == AUTH_LEVEL_ADMIN:
|
||||
log.debug("Current logged in user %s is an ADMIN, send all "
|
||||
"watchdirs", session_user)
|
||||
return self.watchdirs
|
||||
|
||||
watchdirs = {}
|
||||
for watchdir_id, watchdir in self.watchdirs.iteritems():
|
||||
if watchdir.get("owner", "localclient") == session_user:
|
||||
watchdirs[watchdir_id] = watchdir
|
||||
|
||||
log.debug("Current logged in user %s is not an ADMIN, send only "
|
||||
"his watchdirs: %s", session_user, watchdirs.keys())
|
||||
return watchdirs
|
||||
|
||||
def _make_unicode(self, options):
|
||||
opts = {}
|
||||
@ -300,13 +364,16 @@ class Core(CorePluginBase):
|
||||
opts[key] = options[key]
|
||||
return opts
|
||||
|
||||
@export()
|
||||
@export
|
||||
def add(self, options={}):
|
||||
"""Add a watch folder."""
|
||||
options = self._make_unicode(options)
|
||||
abswatchdir = os.path.abspath(options['path'])
|
||||
CheckInput(os.path.isdir(abswatchdir) , _("Path does not exist."))
|
||||
CheckInput(os.access(abswatchdir, os.R_OK|os.W_OK), "You must have read and write access to watch folder.")
|
||||
CheckInput(
|
||||
os.access(abswatchdir, os.R_OK|os.W_OK),
|
||||
"You must have read and write access to watch folder."
|
||||
)
|
||||
if abswatchdir in [wd['abspath'] for wd in self.watchdirs.itervalues()]:
|
||||
raise Exception("Path is already being watched.")
|
||||
options.setdefault('enabled', False)
|
||||
@ -324,9 +391,43 @@ class Core(CorePluginBase):
|
||||
def remove(self, watchdir_id):
|
||||
"""Remove a watch folder."""
|
||||
watchdir_id = str(watchdir_id)
|
||||
CheckInput(watchdir_id in self.watchdirs, "Unknown Watchdir: %s" % self.watchdirs)
|
||||
CheckInput(watchdir_id in self.watchdirs,
|
||||
"Unknown Watchdir: %s" % self.watchdirs)
|
||||
if self.watchdirs[watchdir_id]['enabled']:
|
||||
self.disable_watchdir(watchdir_id)
|
||||
del self.watchdirs[watchdir_id]
|
||||
self.config.save()
|
||||
component.get("EventManager").emit(AutoaddOptionsChangedEvent())
|
||||
|
||||
def __migrate_config_1_to_2(self, config):
|
||||
for watchdir_id in config['watchdirs'].iterkeys():
|
||||
config['watchdirs'][watchdir_id]['owner'] = 'localclient'
|
||||
return config
|
||||
|
||||
def __on_pre_torrent_removed(self, torrent_id):
|
||||
try:
|
||||
torrent = component.get("TorrentManager")[torrent_id]
|
||||
except KeyError:
|
||||
log.warning("Unable to remove torrent file for torrent id %s. It"
|
||||
"was already deleted from the TorrentManager",
|
||||
torrent_id)
|
||||
return
|
||||
torrent_fname = torrent.filename
|
||||
for watchdir in self.watchdirs.itervalues():
|
||||
if not watchdir.get('copy_torrent_toggle', False):
|
||||
# This watchlist does copy torrents
|
||||
continue
|
||||
elif not watchdir.get('delete_copy_torrent_toggle', False):
|
||||
# This watchlist is not set to delete finished torrents
|
||||
continue
|
||||
copy_torrent_path = watchdir['copy_torrent']
|
||||
torrent_fname_path = os.path.join(copy_torrent_path, torrent_fname)
|
||||
if os.path.isfile(torrent_fname_path):
|
||||
try:
|
||||
os.remove(torrent_fname_path)
|
||||
log.info("Removed torrent file \"%s\" from \"%s\"",
|
||||
torrent_fname, copy_torrent_path)
|
||||
break
|
||||
except OSError, e:
|
||||
log.info("Failed to removed torrent file \"%s\" from "
|
||||
"\"%s\": %s", torrent_fname, copy_torrent_path, e)
|
||||
|
||||
@ -41,6 +41,7 @@ import gtk
|
||||
|
||||
from deluge.log import getPluginLogger
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.gtkui import dialogs
|
||||
from deluge.plugins.pluginbase import GtkPluginBase
|
||||
import deluge.component as component
|
||||
import deluge.common
|
||||
@ -50,12 +51,19 @@ from common import get_resource
|
||||
|
||||
log = getPluginLogger(__name__)
|
||||
|
||||
class IncompatibleOption(Exception):
|
||||
pass
|
||||
|
||||
class OptionsDialog():
|
||||
spin_ids = ["max_download_speed", "max_upload_speed", "stop_ratio"]
|
||||
spin_int_ids = ["max_upload_slots", "max_connections"]
|
||||
chk_ids = ["stop_at_ratio", "remove_at_ratio", "move_completed", "add_paused", "auto_managed", "queue_to_top"]
|
||||
chk_ids = ["stop_at_ratio", "remove_at_ratio", "move_completed",
|
||||
"add_paused", "auto_managed", "queue_to_top"]
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
self.accounts = gtk.ListStore(str)
|
||||
self.labels = gtk.ListStore(str)
|
||||
self.core_config = {}
|
||||
|
||||
def show(self, options={}, watchdir_id=None):
|
||||
self.glade = gtk.glade.XML(get_resource("autoadd_options.glade"))
|
||||
@ -64,14 +72,11 @@ class OptionsDialog():
|
||||
"on_opts_apply":self.on_apply,
|
||||
"on_opts_cancel":self.on_cancel,
|
||||
"on_options_dialog_close":self.on_cancel,
|
||||
"on_error_ok":self.on_error_ok,
|
||||
"on_error_dialog_close":self.on_error_ok,
|
||||
"on_toggle_toggled":self.on_toggle_toggled
|
||||
})
|
||||
self.dialog = self.glade.get_widget("options_dialog")
|
||||
self.dialog.set_transient_for(component.get("Preferences").pref_dialog)
|
||||
self.err_dialog = self.glade.get_widget("error_dialog")
|
||||
self.err_dialog.set_transient_for(self.dialog)
|
||||
|
||||
if watchdir_id:
|
||||
#We have an existing watchdir_id, we are editing
|
||||
self.glade.get_widget('opts_add_button').hide()
|
||||
@ -87,12 +92,36 @@ class OptionsDialog():
|
||||
self.dialog.run()
|
||||
|
||||
def load_options(self, options):
|
||||
self.glade.get_widget('enabled').set_active(options.get('enabled', False))
|
||||
self.glade.get_widget('append_extension_toggle').set_active(options.get('append_extension_toggle', False))
|
||||
self.glade.get_widget('append_extension').set_text(options.get('append_extension', '.added'))
|
||||
self.glade.get_widget('download_location_toggle').set_active(options.get('download_location_toggle', False))
|
||||
self.glade.get_widget('label').set_text(options.get('label', ''))
|
||||
self.glade.get_widget('enabled').set_active(options.get('enabled', True))
|
||||
self.glade.get_widget('append_extension_toggle').set_active(
|
||||
options.get('append_extension_toggle', False)
|
||||
)
|
||||
self.glade.get_widget('append_extension').set_text(
|
||||
options.get('append_extension', '.added')
|
||||
)
|
||||
self.glade.get_widget('download_location_toggle').set_active(
|
||||
options.get('download_location_toggle', False)
|
||||
)
|
||||
self.glade.get_widget('copy_torrent_toggle').set_active(
|
||||
options.get('copy_torrent_toggle', False)
|
||||
)
|
||||
self.glade.get_widget('delete_copy_torrent_toggle').set_active(
|
||||
options.get('delete_copy_torrent_toggle', False)
|
||||
)
|
||||
self.accounts.clear()
|
||||
self.labels.clear()
|
||||
combobox = self.glade.get_widget('OwnerCombobox')
|
||||
combobox_render = gtk.CellRendererText()
|
||||
combobox.pack_start(combobox_render, True)
|
||||
combobox.add_attribute(combobox_render, 'text', 0)
|
||||
combobox.set_model(self.accounts)
|
||||
|
||||
label_widget = self.glade.get_widget('label')
|
||||
label_widget.child.set_text(options.get('label', ''))
|
||||
label_widget.set_model(self.labels)
|
||||
label_widget.set_text_column(0)
|
||||
self.glade.get_widget('label_toggle').set_active(options.get('label_toggle', False))
|
||||
|
||||
for id in self.spin_ids + self.spin_int_ids:
|
||||
self.glade.get_widget(id).set_value(options.get(id, 0))
|
||||
self.glade.get_widget(id+'_toggle').set_active(options.get(id+'_toggle', False))
|
||||
@ -105,30 +134,115 @@ class OptionsDialog():
|
||||
self.glade.get_widget('isnt_queue_to_top').set_active(True)
|
||||
if not options.get('auto_managed', True):
|
||||
self.glade.get_widget('isnt_auto_managed').set_active(True)
|
||||
for field in ['move_completed_path', 'path', 'download_location']:
|
||||
for field in ['move_completed_path', 'path', 'download_location',
|
||||
'copy_torrent']:
|
||||
if client.is_localhost():
|
||||
self.glade.get_widget(field+"_chooser").set_filename(options.get(field, os.path.expanduser("~")))
|
||||
self.glade.get_widget(field+"_chooser").set_current_folder(
|
||||
options.get(field, os.path.expanduser("~"))
|
||||
)
|
||||
self.glade.get_widget(field+"_chooser").show()
|
||||
self.glade.get_widget(field+"_entry").hide()
|
||||
else:
|
||||
self.glade.get_widget(field+"_entry").set_text(options.get(field, ""))
|
||||
self.glade.get_widget(field+"_entry").set_text(
|
||||
options.get(field, "")
|
||||
)
|
||||
self.glade.get_widget(field+"_entry").show()
|
||||
self.glade.get_widget(field+"_chooser").hide()
|
||||
self.set_sensitive()
|
||||
|
||||
def on_core_config(config):
|
||||
if client.is_localhost():
|
||||
self.glade.get_widget('download_location_chooser').set_current_folder(
|
||||
options.get('download_location', config["download_location"])
|
||||
)
|
||||
if options.get('move_completed_toggle', config["move_completed"]):
|
||||
self.glade.get_widget('move_completed_toggle').set_active(True)
|
||||
self.glade.get_widget('move_completed_path_chooser').set_current_folder(
|
||||
options.get('move_completed_path', config["move_completed_path"])
|
||||
)
|
||||
if options.get('copy_torrent_toggle', config["copy_torrent_file"]):
|
||||
self.glade.get_widget('copy_torrent_toggle').set_active(True)
|
||||
self.glade.get_widget('copy_torrent_chooser').set_current_folder(
|
||||
options.get('copy_torrent', config["torrentfiles_location"])
|
||||
)
|
||||
else:
|
||||
self.glade.get_widget('download_location_entry').set_text(
|
||||
options.get('download_location', config["download_location"])
|
||||
)
|
||||
if options.get('move_completed_toggle', config["move_completed"]):
|
||||
self.glade.get_widget('move_completed_toggle').set_active(
|
||||
options.get('move_completed_toggle', False)
|
||||
)
|
||||
self.glade.get_widget('move_completed_path_entry').set_text(
|
||||
options.get('move_completed_path', config["move_completed_path"])
|
||||
)
|
||||
if options.get('copy_torrent_toggle', config["copy_torrent_file"]):
|
||||
self.glade.get_widget('copy_torrent_toggle').set_active(True)
|
||||
self.glade.get_widget('copy_torrent_entry').set_text(
|
||||
options.get('copy_torrent', config["torrentfiles_location"])
|
||||
)
|
||||
|
||||
if options.get('delete_copy_torrent_toggle', config["del_copy_torrent_file"]):
|
||||
self.glade.get_widget('delete_copy_torrent_toggle').set_active(True)
|
||||
|
||||
if not options:
|
||||
client.core.get_config().addCallback(on_core_config)
|
||||
|
||||
def on_accounts(accounts, owner):
|
||||
log.debug("Got Accounts")
|
||||
selected_iter = None
|
||||
for account in accounts:
|
||||
iter = self.accounts.append()
|
||||
self.accounts.set_value(
|
||||
iter, 0, account['username']
|
||||
)
|
||||
if account['username'] == owner:
|
||||
selected_iter = iter
|
||||
self.glade.get_widget('OwnerCombobox').set_active_iter(selected_iter)
|
||||
|
||||
def on_accounts_failure(failure):
|
||||
log.debug("Failed to get accounts!!! %s", failure)
|
||||
iter = self.accounts.append()
|
||||
self.accounts.set_value(iter, 0, client.get_auth_user())
|
||||
self.glade.get_widget('OwnerCombobox').set_active(0)
|
||||
self.glade.get_widget('OwnerCombobox').set_sensitive(False)
|
||||
|
||||
def on_labels(labels):
|
||||
log.debug("Got Labels: %s", labels)
|
||||
for label in labels:
|
||||
self.labels.set_value(self.labels.append(), 0, label)
|
||||
label_widget = self.glade.get_widget('label')
|
||||
label_widget.set_model(self.labels)
|
||||
label_widget.set_text_column(0)
|
||||
|
||||
def on_failure(failure):
|
||||
log.exception(failure)
|
||||
|
||||
def on_get_enabled_plugins(result):
|
||||
if 'Label' in result:
|
||||
self.glade.get_widget('label_frame').show()
|
||||
client.label.get_labels().addCallback(on_labels).addErrback(on_failure)
|
||||
else:
|
||||
self.glade.get_widget('label_frame').hide()
|
||||
self.glade.get_widget('label_toggle').set_active(False)
|
||||
|
||||
client.core.get_enabled_plugins().addCallback(on_get_enabled_plugins)
|
||||
if client.get_auth_level() == deluge.common.AUTH_LEVEL_ADMIN:
|
||||
client.core.get_known_accounts().addCallback(
|
||||
on_accounts, options.get('owner', client.get_auth_user())
|
||||
).addErrback(on_accounts_failure)
|
||||
else:
|
||||
iter = self.accounts.append()
|
||||
self.accounts.set_value(iter, 0, client.get_auth_user())
|
||||
self.glade.get_widget('OwnerCombobox').set_active(0)
|
||||
self.glade.get_widget('OwnerCombobox').set_sensitive(False)
|
||||
|
||||
def set_sensitive(self):
|
||||
maintoggles = ['download_location', 'append_extension', 'move_completed', 'label', \
|
||||
'max_download_speed', 'max_upload_speed', 'max_connections', \
|
||||
'max_upload_slots', 'add_paused', 'auto_managed', 'stop_at_ratio', 'queue_to_top']
|
||||
maintoggles = ['download_location', 'append_extension',
|
||||
'move_completed', 'label', 'max_download_speed',
|
||||
'max_upload_speed', 'max_connections',
|
||||
'max_upload_slots', 'add_paused', 'auto_managed',
|
||||
'stop_at_ratio', 'queue_to_top', 'copy_torrent']
|
||||
[self.on_toggle_toggled(self.glade.get_widget(x+'_toggle')) for x in maintoggles]
|
||||
|
||||
def on_toggle_toggled(self, tb):
|
||||
@ -139,6 +253,10 @@ class OptionsDialog():
|
||||
self.glade.get_widget('download_location_entry').set_sensitive(isactive)
|
||||
elif toggle == 'append_extension':
|
||||
self.glade.get_widget('append_extension').set_sensitive(isactive)
|
||||
elif toggle == 'copy_torrent':
|
||||
self.glade.get_widget('copy_torrent_entry').set_sensitive(isactive)
|
||||
self.glade.get_widget('copy_torrent_chooser').set_sensitive(isactive)
|
||||
self.glade.get_widget('delete_copy_torrent_toggle').set_sensitive(isactive)
|
||||
elif toggle == 'move_completed':
|
||||
self.glade.get_widget('move_completed_path_chooser').set_sensitive(isactive)
|
||||
self.glade.get_widget('move_completed_path_entry').set_sensitive(isactive)
|
||||
@ -170,23 +288,29 @@ class OptionsDialog():
|
||||
self.glade.get_widget('remove_at_ratio').set_sensitive(isactive)
|
||||
|
||||
def on_apply(self, Event=None):
|
||||
client.autoadd.set_options(str(self.watchdir_id), self.generate_opts()).addCallbacks(self.on_added, self.on_error_show)
|
||||
try:
|
||||
options = self.generate_opts()
|
||||
client.autoadd.set_options(
|
||||
str(self.watchdir_id), options
|
||||
).addCallbacks(self.on_added, self.on_error_show)
|
||||
except IncompatibleOption, err:
|
||||
dialogs.ErrorDialog(_("Incompatible Option"), str(err), self.dialog).run()
|
||||
|
||||
|
||||
def on_error_show(self, result):
|
||||
self.glade.get_widget('error_label').set_text(result.value.exception_msg)
|
||||
self.err_dialog = self.glade.get_widget('error_dialog')
|
||||
self.err_dialog.set_transient_for(self.dialog)
|
||||
d = dialogs.ErrorDialog(_("Error"), result.value.exception_msg, self.dialog)
|
||||
result.cleanFailure()
|
||||
self.err_dialog.show()
|
||||
d.run()
|
||||
|
||||
def on_added(self, result):
|
||||
self.dialog.destroy()
|
||||
|
||||
def on_error_ok(self, Event=None):
|
||||
self.err_dialog.hide()
|
||||
|
||||
def on_add(self, Event=None):
|
||||
client.autoadd.add(self.generate_opts()).addCallbacks(self.on_added, self.on_error_show)
|
||||
try:
|
||||
options = self.generate_opts()
|
||||
client.autoadd.add(options).addCallbacks(self.on_added, self.on_error_show)
|
||||
except IncompatibleOption, err:
|
||||
dialogs.ErrorDialog(_("Incompatible Option"), str(err), self.dialog).run()
|
||||
|
||||
def on_cancel(self, Event=None):
|
||||
self.dialog.destroy()
|
||||
@ -197,17 +321,30 @@ class OptionsDialog():
|
||||
options['enabled'] = self.glade.get_widget('enabled').get_active()
|
||||
if client.is_localhost():
|
||||
options['path'] = self.glade.get_widget('path_chooser').get_filename()
|
||||
options['download_location'] = self.glade.get_widget('download_location_chooser').get_filename()
|
||||
options['move_completed_path'] = self.glade.get_widget('move_completed_path_chooser').get_filename()
|
||||
options['download_location'] = self.glade.get_widget(
|
||||
'download_location_chooser').get_filename()
|
||||
options['move_completed_path'] = self.glade.get_widget(
|
||||
'move_completed_path_chooser').get_filename()
|
||||
options['copy_torrent'] = self.glade.get_widget(
|
||||
'copy_torrent_chooser').get_filename()
|
||||
else:
|
||||
options['path'] = self.glade.get_widget('path_entry').get_text()
|
||||
options['download_location'] = self.glade.get_widget('download_location_entry').get_text()
|
||||
options['move_completed_path'] = self.glade.get_widget('move_completed_path_entry').get_text()
|
||||
options['append_extension_toggle'] = self.glade.get_widget('append_extension_toggle').get_active()
|
||||
options['download_location'] = self.glade.get_widget(
|
||||
'download_location_entry').get_text()
|
||||
options['move_completed_path'] = self.glade.get_widget(
|
||||
'move_completed_path_entry').get_text()
|
||||
options['copy_torrent'] = self.glade.get_widget(
|
||||
'copy_torrent_entry').get_text()
|
||||
|
||||
options['label'] = self.glade.get_widget('label').child.get_text().lower()
|
||||
options['append_extension'] = self.glade.get_widget('append_extension').get_text()
|
||||
options['download_location_toggle'] = self.glade.get_widget('download_location_toggle').get_active()
|
||||
options['label'] = self.glade.get_widget('label').get_text().lower()
|
||||
options['label_toggle'] = self.glade.get_widget('label_toggle').get_active()
|
||||
options['owner'] = self.accounts[
|
||||
self.glade.get_widget('OwnerCombobox').get_active()][0]
|
||||
|
||||
for key in ['append_extension_toggle', 'download_location_toggle',
|
||||
'label_toggle', 'copy_torrent_toggle',
|
||||
'delete_copy_torrent_toggle']:
|
||||
options[key] = self.glade.get_widget(key).get_active()
|
||||
|
||||
for id in self.spin_ids:
|
||||
options[id] = self.glade.get_widget(id).get_value()
|
||||
@ -218,6 +355,10 @@ class OptionsDialog():
|
||||
for id in self.chk_ids:
|
||||
options[id] = self.glade.get_widget(id).get_active()
|
||||
options[id+'_toggle'] = self.glade.get_widget(id+'_toggle').get_active()
|
||||
|
||||
if options['copy_torrent_toggle'] and options['path'] == options['copy_torrent']:
|
||||
raise IncompatibleOption(_("\"Watch Folder\" directory and \"Copy of .torrent"
|
||||
" files to\" directory cannot be the same!"))
|
||||
return options
|
||||
|
||||
|
||||
@ -232,9 +373,15 @@ class GtkUI(GtkPluginBase):
|
||||
})
|
||||
self.opts_dialog = OptionsDialog()
|
||||
|
||||
component.get("PluginManager").register_hook("on_apply_prefs", self.on_apply_prefs)
|
||||
component.get("PluginManager").register_hook("on_show_prefs", self.on_show_prefs)
|
||||
client.register_event_handler("AutoaddOptionsChangedEvent", self.on_options_changed_event)
|
||||
component.get("PluginManager").register_hook(
|
||||
"on_apply_prefs", self.on_apply_prefs
|
||||
)
|
||||
component.get("PluginManager").register_hook(
|
||||
"on_show_prefs", self.on_show_prefs
|
||||
)
|
||||
client.register_event_handler(
|
||||
"AutoaddOptionsChangedEvent", self.on_options_changed_event
|
||||
)
|
||||
|
||||
self.watchdirs = {}
|
||||
|
||||
@ -255,38 +402,55 @@ class GtkUI(GtkPluginBase):
|
||||
self.create_columns(self.treeView)
|
||||
sw.add(self.treeView)
|
||||
sw.show_all()
|
||||
component.get("Preferences").add_page("AutoAdd", self.glade.get_widget("prefs_box"))
|
||||
component.get("Preferences").add_page(
|
||||
"AutoAdd", self.glade.get_widget("prefs_box")
|
||||
)
|
||||
self.on_show_prefs()
|
||||
|
||||
|
||||
def disable(self):
|
||||
component.get("Preferences").remove_page("AutoAdd")
|
||||
component.get("PluginManager").deregister_hook("on_apply_prefs", self.on_apply_prefs)
|
||||
component.get("PluginManager").deregister_hook("on_show_prefs", self.on_show_prefs)
|
||||
component.get("PluginManager").deregister_hook(
|
||||
"on_apply_prefs", self.on_apply_prefs
|
||||
)
|
||||
component.get("PluginManager").deregister_hook(
|
||||
"on_show_prefs", self.on_show_prefs
|
||||
)
|
||||
|
||||
def create_model(self):
|
||||
|
||||
store = gtk.ListStore(str, bool, str)
|
||||
store = gtk.ListStore(str, bool, str, str)
|
||||
for watchdir_id, watchdir in self.watchdirs.iteritems():
|
||||
store.append([watchdir_id, watchdir['enabled'], watchdir['path']])
|
||||
store.append([
|
||||
watchdir_id, watchdir['enabled'],
|
||||
watchdir.get('owner', 'localclient'), watchdir['path']
|
||||
])
|
||||
return store
|
||||
|
||||
def create_columns(self, treeView):
|
||||
rendererToggle = gtk.CellRendererToggle()
|
||||
column = gtk.TreeViewColumn("On", rendererToggle, activatable=True, active=1)
|
||||
column = gtk.TreeViewColumn(
|
||||
_("Active"), rendererToggle, activatable=1, active=1
|
||||
)
|
||||
column.set_sort_column_id(1)
|
||||
treeView.append_column(column)
|
||||
tt = gtk.Tooltip()
|
||||
tt.set_text('Double-click to toggle')
|
||||
tt.set_text(_('Double-click to toggle'))
|
||||
treeView.set_tooltip_cell(tt, None, None, rendererToggle)
|
||||
|
||||
rendererText = gtk.CellRendererText()
|
||||
column = gtk.TreeViewColumn("Path", rendererText, text=2)
|
||||
column = gtk.TreeViewColumn(_("Owner"), rendererText, text=2)
|
||||
column.set_sort_column_id(2)
|
||||
treeView.append_column(column)
|
||||
tt2 = gtk.Tooltip()
|
||||
tt2.set_text('Double-click to edit')
|
||||
#treeView.set_tooltip_cell(tt2, None, column, None)
|
||||
tt2.set_text(_('Double-click to edit'))
|
||||
treeView.set_has_tooltip(True)
|
||||
|
||||
rendererText = gtk.CellRendererText()
|
||||
column = gtk.TreeViewColumn(_("Path"), rendererText, text=3)
|
||||
column.set_sort_column_id(3)
|
||||
treeView.append_column(column)
|
||||
tt2 = gtk.Tooltip()
|
||||
tt2.set_text(_('Double-click to edit'))
|
||||
treeView.set_has_tooltip(True)
|
||||
|
||||
def load_watchdir_list(self):
|
||||
@ -309,7 +473,7 @@ class GtkUI(GtkPluginBase):
|
||||
tree, tree_id = self.treeView.get_selection().get_selected()
|
||||
watchdir_id = str(self.store.get_value(tree_id, 0))
|
||||
if watchdir_id:
|
||||
if col and col.get_title() == 'On':
|
||||
if col and col.get_title() == _("Active"):
|
||||
if self.watchdirs[watchdir_id]['enabled']:
|
||||
client.autoadd.disable_watchdir(watchdir_id)
|
||||
else:
|
||||
@ -332,17 +496,21 @@ class GtkUI(GtkPluginBase):
|
||||
client.autoadd.set_options(watchdir_id, watchdir)
|
||||
|
||||
def on_show_prefs(self):
|
||||
client.autoadd.get_config().addCallback(self.cb_get_config)
|
||||
client.autoadd.get_watchdirs().addCallback(self.cb_get_config)
|
||||
|
||||
def on_options_changed_event(self, event):
|
||||
client.autoadd.get_config().addCallback(self.cb_get_config)
|
||||
def on_options_changed_event(self):
|
||||
client.autoadd.get_watchdirs().addCallback(self.cb_get_config)
|
||||
|
||||
def cb_get_config(self, config):
|
||||
def cb_get_config(self, watchdirs):
|
||||
"""callback for on show_prefs"""
|
||||
self.watchdirs = config.get('watchdirs', {})
|
||||
log.trace("Got whatchdirs from core: %s", watchdirs)
|
||||
self.watchdirs = watchdirs or {}
|
||||
self.store.clear()
|
||||
for watchdir_id, watchdir in self.watchdirs.iteritems():
|
||||
self.store.append([watchdir_id, watchdir['enabled'], watchdir['path']])
|
||||
self.store.append([
|
||||
watchdir_id, watchdir['enabled'],
|
||||
watchdir.get('owner', 'localclient'), watchdir['path']
|
||||
])
|
||||
# Disable the remove and edit buttons, because nothing in the store is selected
|
||||
self.glade.get_widget('remove_button').set_sensitive(False)
|
||||
self.glade.get_widget('edit_button').set_sensitive(False)
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
# setup.py
|
||||
#
|
||||
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
|
||||
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
||||
#
|
||||
# Basic plugin template created by:
|
||||
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
||||
@ -40,10 +41,10 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
__plugin_name__ = "AutoAdd"
|
||||
__author__ = "Chase Sterling"
|
||||
__author_email__ = "chase.sterling@gmail.com"
|
||||
__author__ = "Chase Sterling, Pedro Algarvio"
|
||||
__author_email__ = "chase.sterling@gmail.com, pedro@algarvio.me"
|
||||
__version__ = "1.02"
|
||||
__url__ = "http://forum.deluge-torrent.org/viewtopic.php?f=9&t=26775"
|
||||
__url__ = "http://dev.deluge-torrent.org/wiki/Plugins/AutoAdd"
|
||||
__license__ = "GPLv3"
|
||||
__description__ = "Monitors folders for .torrent files."
|
||||
__long_description__ = """"""
|
||||
|
||||
@ -64,19 +64,15 @@ class ExecuteCommandAddedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a new command is added.
|
||||
"""
|
||||
__slots__ = ('command_id', 'event', 'command')
|
||||
def __init__(self, command_id, event, command):
|
||||
self.command_id = command_id
|
||||
self.event = event
|
||||
self.command = command
|
||||
self._args = [command_id, event, command]
|
||||
|
||||
class ExecuteCommandRemovedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a command is removed.
|
||||
"""
|
||||
__slots__ = ('command_id',)
|
||||
def __init__(self, command_id):
|
||||
self.command_id = command_id
|
||||
self._args = [command_id]
|
||||
|
||||
class Core(CorePluginBase):
|
||||
def enable(self):
|
||||
@ -86,17 +82,17 @@ class Core(CorePluginBase):
|
||||
|
||||
# Go through the commands list and register event handlers
|
||||
for command in self.config["commands"]:
|
||||
event_name = command[EXECUTE_EVENT]
|
||||
if event_name in self.registered_events:
|
||||
event = command[EXECUTE_EVENT]
|
||||
if event in self.registered_events:
|
||||
continue
|
||||
|
||||
def create_event_handler(event_name):
|
||||
def event_handler(event):
|
||||
self.execute_commands(event.torrent_id, event_name)
|
||||
def create_event_handler(event):
|
||||
def event_handler(torrent_id):
|
||||
self.execute_commands(torrent_id, event)
|
||||
return event_handler
|
||||
event_handler = create_event_handler(event_name)
|
||||
event_manager.register_event_handler(EVENT_MAP[event_name], event_handler)
|
||||
self.registered_events[event_name] = event_handler
|
||||
event_handler = create_event_handler(event)
|
||||
event_manager.register_event_handler(EVENT_MAP[event], event_handler)
|
||||
self.registered_events[event] = event_handler
|
||||
|
||||
log.debug("Execute core plugin enabled!")
|
||||
|
||||
|
||||
@ -161,13 +161,13 @@ class ExecutePreferences(object):
|
||||
command = widget.get_text()
|
||||
client.execute.save_command(command_id, event, command)
|
||||
|
||||
def on_command_added_event(self, event):
|
||||
log.debug("Adding command %s: %s", event.event, event.command)
|
||||
self.add_command(event.command_id, event.event, event.command)
|
||||
def on_command_added_event(self, command_id, event, command):
|
||||
log.debug("Adding command %s: %s", event, command)
|
||||
self.add_command(command_id, event, command)
|
||||
|
||||
def on_command_removed_event(self, event):
|
||||
log.debug("Removing command %s", event.command_id)
|
||||
self.remove_command(event.command_id)
|
||||
def on_command_removed_event(self, command_id):
|
||||
log.debug("Removing command %s", command_id)
|
||||
self.remove_command(command_id)
|
||||
|
||||
class GtkUI(GtkPluginBase):
|
||||
|
||||
|
||||
@ -77,14 +77,14 @@ class Core(CorePluginBase):
|
||||
def update(self):
|
||||
pass
|
||||
|
||||
def _on_torrent_finished(self, event):
|
||||
def _on_torrent_finished(self, torrent_id):
|
||||
"""
|
||||
This is called when a torrent finishes. We need to check to see if there
|
||||
are any files to extract.
|
||||
"""
|
||||
# Get the save path
|
||||
save_path = component.get("TorrentManager")[event.torrent_id].get_status(["save_path"])["save_path"]
|
||||
files = component.get("TorrentManager")[event.torrent_id].get_files()
|
||||
save_path = component.get("TorrentManager")[torrent_id].get_status(["save_path"])["save_path"]
|
||||
files = component.get("TorrentManager")[torrent_id].get_files()
|
||||
for f in files:
|
||||
ext = os.path.splitext(f["path"])
|
||||
if ext[1] in (".gz", ".bz2", ".lzma"):
|
||||
@ -100,22 +100,22 @@ class Core(CorePluginBase):
|
||||
|
||||
# Now that we have the cmd, lets run it to extract the files
|
||||
fp = os.path.join(save_path, f["path"])
|
||||
|
||||
|
||||
# Get the destination path
|
||||
dest = self.config["extract_path"]
|
||||
if self.config["use_name_folder"]:
|
||||
name = component.get("TorrentManager")[event.torrent_id].get_status(["name"])["name"]
|
||||
name = component.get("TorrentManager")[torrent_id].get_status(["name"])["name"]
|
||||
dest = os.path.join(dest, name)
|
||||
|
||||
# Create the destination folder if it doesn't exist
|
||||
# Create the destination folder if it doesn't exist
|
||||
if not os.path.exists(dest):
|
||||
try:
|
||||
os.makedirs(dest)
|
||||
except Exception, e:
|
||||
log.error("Error creating destination folder: %s", e)
|
||||
return
|
||||
|
||||
log.debug("Extracting to %s", dest)
|
||||
|
||||
log.debug("Extracting to %s", dest)
|
||||
def on_extract_success(result, torrent_id):
|
||||
# XXX: Emit an event
|
||||
log.debug("Extract was successful for %s", torrent_id)
|
||||
@ -126,8 +126,8 @@ class Core(CorePluginBase):
|
||||
|
||||
# Run the command and add some callbacks
|
||||
d = getProcessValue(cmd[0], cmd[1].split() + [str(fp)], {}, str(dest))
|
||||
d.addCallback(on_extract_success, event.torrent_id)
|
||||
d.addErrback(on_extract_failed, event.torrent_id)
|
||||
d.addCallback(on_extract_success, torrent_id)
|
||||
d.addErrback(on_extract_failed, torrent_id)
|
||||
|
||||
@export
|
||||
def set_config(self, config):
|
||||
|
||||
@ -55,14 +55,12 @@ class LowDiskSpaceEvent(DelugeEvent):
|
||||
"""Triggered when the available space for a specific path is getting
|
||||
too low.
|
||||
"""
|
||||
__slots__ = ('percents_dict',)
|
||||
|
||||
def __init__(self, percents_dict):
|
||||
"""
|
||||
:param percents: dictionary of path keys with their respecive
|
||||
occupation percentages.
|
||||
"""
|
||||
self.percents_dict = percents_dict
|
||||
self._args = [percents_dict]
|
||||
|
||||
DEFAULT_PREFS = {
|
||||
"enabled": False,
|
||||
@ -174,25 +172,25 @@ class Core(CorePluginBase):
|
||||
free_percent = free_blocks * 100 / total_blocks
|
||||
return free_percent
|
||||
|
||||
def __custom_email_notification(self, event):
|
||||
def __custom_email_notification(self, ocupied_percents):
|
||||
|
||||
subject = _("Low Disk Space Warning")
|
||||
message = _("You're running low on disk space:\n")
|
||||
|
||||
for path, ocupied_percent in event.percents_dict.iteritems():
|
||||
for path, ocupied_percent in ocupied_percents.iteritems():
|
||||
message += _(' %s%% ocupation in %s\n') % (ocupied_percent, path)
|
||||
# "\"%s\"%% space occupation on %s") % (ocupied_percent, path)
|
||||
return subject, message
|
||||
|
||||
def __on_plugin_enabled(self, event):
|
||||
if event.plugin_name == 'Notifications':
|
||||
def __on_plugin_enabled(self, plugin_name):
|
||||
if plugin_name == 'Notifications':
|
||||
component.get("CorePlugin.Notifications"). \
|
||||
register_custom_email_notification(
|
||||
"LowDiskSpaceEvent", self.__custom_email_notification
|
||||
)
|
||||
|
||||
def __on_plugin_disabled(self, event):
|
||||
if event.plugin_name == 'Notifications':
|
||||
def __on_plugin_disabled(self, plugin_name):
|
||||
if plugin_name == 'Notifications':
|
||||
component.get("CorePlugin.Notifications"). \
|
||||
deregister_custom_email_notification("LowDiskSpaceEvent")
|
||||
|
||||
|
||||
@ -134,22 +134,22 @@ class GtkUI(GtkPluginBase):
|
||||
self.glade.get_widget('enabled').set_active(config['enabled'])
|
||||
self.glade.get_widget('percent').set_value(config['percent'])
|
||||
|
||||
def __custom_popup_notification(self, event):
|
||||
def __custom_popup_notification(self, ocupied_percents):
|
||||
title = _("Low Free Space")
|
||||
message = ''
|
||||
for path, percent in event.percents_dict.iteritems():
|
||||
for path, percent in ocupied_percents.iteritems():
|
||||
message += '%s%% %s\n' % (percent, path)
|
||||
message += '\n'
|
||||
return title, message
|
||||
|
||||
def __custom_blink_notification(self, event):
|
||||
def __custom_blink_notification(self, ocupied_percents):
|
||||
return True # Yes, do blink
|
||||
|
||||
def __custom_sound_notification(self, event):
|
||||
def __custom_sound_notification(self, ocupied_percents):
|
||||
return '' # Use default sound
|
||||
|
||||
def __on_plugin_enabled(self, event):
|
||||
if event.plugin_name == 'Notifications':
|
||||
def __on_plugin_enabled(self, plugin_name):
|
||||
if plugin_name == 'Notifications':
|
||||
notifications = component.get("GtkPlugin.Notifications")
|
||||
notifications.register_custom_popup_notification(
|
||||
"LowDiskSpaceEvent", self.__custom_popup_notification
|
||||
@ -161,7 +161,7 @@ class GtkUI(GtkPluginBase):
|
||||
"LowDiskSpaceEvent", self.__custom_sound_notification
|
||||
)
|
||||
|
||||
def __on_plugin_disabled(self, event):
|
||||
def __on_plugin_disabled(self, plugin_name):
|
||||
pass
|
||||
# if plugin_name == 'Notifications':
|
||||
# notifications = component.get("GtkPlugin.Notifications")
|
||||
|
||||
@ -133,20 +133,20 @@ class Core(CorePluginBase):
|
||||
return dict( [(label, 0) for label in self.labels.keys()])
|
||||
|
||||
## Plugin hooks ##
|
||||
def post_torrent_add(self, event):
|
||||
def post_torrent_add(self, torrent_id, from_state):
|
||||
log.debug("post_torrent_add")
|
||||
torrent = self.torrents[event.torrent_id]
|
||||
torrent = self.torrents[torrent_id]
|
||||
|
||||
for label_id, options in self.labels.iteritems():
|
||||
if options["auto_add"]:
|
||||
if self._has_auto_match(torrent, options):
|
||||
self.set_torrent(event.torrent_id, label_id)
|
||||
self.set_torrent(torrent_id, label_id)
|
||||
return
|
||||
|
||||
def post_torrent_remove(self, event):
|
||||
def post_torrent_remove(self, torrent_id):
|
||||
log.debug("post_torrent_remove")
|
||||
if event.torrent_id in self.torrent_labels:
|
||||
del self.torrent_labels[event.torrent_id]
|
||||
if torrent_id in self.torrent_labels:
|
||||
del self.torrent_labels[torrent_id]
|
||||
|
||||
## Utils ##
|
||||
def clean_config(self):
|
||||
|
||||
@ -330,6 +330,7 @@ Deluge.ux.LabelOptionsWindow = Ext.extend(Ext.Window, {
|
||||
|
||||
onOkClick: function() {
|
||||
var values = this.form.getForm().getFieldValues();
|
||||
values['auto_add_trackers'] = values['auto_add_trackers'].split('\n');
|
||||
deluge.client.label.set_options(this.label, values);
|
||||
this.hide();
|
||||
},
|
||||
|
||||
@ -39,15 +39,11 @@ __author_email__ = "mvoncken@gmail.com"
|
||||
__version__ = "0.1"
|
||||
__url__ = "http://deluge-torrent.org"
|
||||
__license__ = "GPLv3"
|
||||
__description__ = "Label plugin."
|
||||
__description__ = "Allows labels to be assigned to torrents"
|
||||
__long_description__ = """
|
||||
Label plugin.
|
||||
|
||||
Offers filters on state,tracker and keyword.
|
||||
adds a tracker column.
|
||||
|
||||
future: Real labels.
|
||||
Allows labels to be assigned to torrents
|
||||
|
||||
Also offers filters on state, tracker and keywords
|
||||
"""
|
||||
__pkg_data__ = {"deluge.plugins."+__plugin_name__.lower(): ["template/*", "data/*"]}
|
||||
|
||||
|
||||
@ -38,6 +38,7 @@
|
||||
#
|
||||
|
||||
import smtplib
|
||||
from email.utils import formatdate
|
||||
from twisted.internet import defer, threads
|
||||
from deluge import component
|
||||
from deluge.event import known_events
|
||||
@ -118,11 +119,14 @@ class CoreNotifications(CustomNotifications):
|
||||
From: %(smtp_from)s
|
||||
To: %(smtp_recipients)s
|
||||
Subject: %(subject)s
|
||||
Date: %(date)s
|
||||
|
||||
|
||||
""" % {'smtp_from': self.config['smtp_from'],
|
||||
'subject': subject,
|
||||
'smtp_recipients': to_addrs}
|
||||
'smtp_recipients': to_addrs,
|
||||
'date': formatdate()
|
||||
}
|
||||
|
||||
message = '\r\n'.join((headers + message).splitlines())
|
||||
|
||||
@ -188,9 +192,9 @@ Subject: %(subject)s
|
||||
return _("Notification email sent.")
|
||||
|
||||
|
||||
def _on_torrent_finished_event(self, event):
|
||||
def _on_torrent_finished_event(self, torrent_id):
|
||||
log.debug("Handler for TorrentFinishedEvent called for CORE")
|
||||
torrent = component.get("TorrentManager")[event.torrent_id]
|
||||
torrent = component.get("TorrentManager")[torrent_id]
|
||||
torrent_status = torrent.get_status({})
|
||||
# Email
|
||||
subject = _("Finished Torrent \"%(name)s\"") % torrent_status
|
||||
|
||||
@ -213,7 +213,7 @@
|
||||
<property name="max_length">65535</property>
|
||||
<property name="invisible_char">●</property>
|
||||
<property name="width_chars">5</property>
|
||||
<property name="adjustment">25 1 100 1 10 0</property>
|
||||
<property name="adjustment">25 1 65535 0 10 0</property>
|
||||
<property name="climb_rate">1</property>
|
||||
<property name="snap_to_ticks">True</property>
|
||||
<property name="numeric">True</property>
|
||||
@ -292,6 +292,7 @@
|
||||
<property name="can_focus">True</property>
|
||||
<property name="hscrollbar_policy">automatic</property>
|
||||
<property name="vscrollbar_policy">automatic</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<widget class="GtkTreeView" id="smtp_recipients">
|
||||
<property name="visible">True</property>
|
||||
|
||||
@ -46,10 +46,13 @@ __version__ = "0.1"
|
||||
__url__ = "http://dev.deluge-torrent.org/"
|
||||
__license__ = "GPLv3"
|
||||
__description__ = "Plugin which provides notifications to Deluge."
|
||||
__long_description__ = __description__ + """\
|
||||
Email, Popup, Blink and Sound notifications are supported.
|
||||
The plugin also allows other plugins to make use of itself for their own custom
|
||||
notifications.
|
||||
__long_description__ = """
|
||||
Plugin which provides notifications to Deluge
|
||||
|
||||
Email, Popup, Blink and Sound notifications
|
||||
|
||||
The plugin also allows other plugins to make
|
||||
use of itself for their own custom notifications
|
||||
"""
|
||||
__pkg_data__ = {"deluge.plugins."+__plugin_name__.lower(): ["template/*", "data/*"]}
|
||||
|
||||
|
||||
@ -66,7 +66,7 @@ STATES = {
|
||||
|
||||
CONTROLLED_SETTINGS = [
|
||||
"max_download_speed",
|
||||
"max_download_speed",
|
||||
"max_upload_speed",
|
||||
"max_active_limit",
|
||||
"max_active_downloading",
|
||||
"max_active_seeding"
|
||||
@ -76,12 +76,11 @@ class SchedulerEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a schedule state changes.
|
||||
"""
|
||||
__slots__ = ('colour',)
|
||||
def __init__(self, colour):
|
||||
"""
|
||||
:param colour: str, the current scheduler state
|
||||
"""
|
||||
self.colour = colour
|
||||
self._args = [colour]
|
||||
|
||||
class Core(CorePluginBase):
|
||||
def enable(self):
|
||||
@ -120,8 +119,8 @@ class Core(CorePluginBase):
|
||||
pass
|
||||
|
||||
|
||||
def on_config_value_changed(self, event):
|
||||
if event.key in CONTROLLED_SETTINGS:
|
||||
def on_config_value_changed(self, key, value):
|
||||
if key in CONTROLLED_SETTINGS:
|
||||
self.do_schedule(False)
|
||||
|
||||
def __apply_set_functions(self):
|
||||
|
||||
@ -203,9 +203,9 @@ class GtkUI(GtkPluginBase):
|
||||
|
||||
client.scheduler.get_config().addCallback(on_get_config)
|
||||
|
||||
def on_scheduler_event(self, event):
|
||||
def on_scheduler_event(self, state):
|
||||
def on_state_deferred(s):
|
||||
self.status_item.set_image_from_file(get_resource(event.colour.lower() + ".png"))
|
||||
self.status_item.set_image_from_file(get_resource(state.lower() + ".png"))
|
||||
|
||||
self.state_deferred.addCallback(on_state_deferred)
|
||||
|
||||
|
||||
@ -63,7 +63,6 @@ __all__ = ['dumps', 'loads']
|
||||
#
|
||||
|
||||
import struct
|
||||
import string
|
||||
from threading import Lock
|
||||
|
||||
# Default number of bits for serialized floats, either 32 or 64 (also a parameter for dumps()).
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import tempfile
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
import deluge.configmanager
|
||||
import deluge.log
|
||||
@ -10,6 +14,9 @@ def set_tmp_config_dir():
|
||||
deluge.configmanager.set_config_dir(config_directory)
|
||||
return config_directory
|
||||
|
||||
def rpath(*args):
|
||||
return os.path.join(os.path.dirname(__file__), *args)
|
||||
|
||||
import gettext
|
||||
import locale
|
||||
import pkg_resources
|
||||
@ -26,3 +33,38 @@ try:
|
||||
gettext.install("deluge", pkg_resources.resource_filename("deluge", "i18n"))
|
||||
except Exception, e:
|
||||
print e
|
||||
|
||||
def start_core():
|
||||
CWD = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
DAEMON_SCRIPT = """
|
||||
import sys
|
||||
import deluge.main
|
||||
|
||||
sys.argv.extend(['-d', '-c', '%s', '-L', 'info'])
|
||||
|
||||
deluge.main.start_daemon()
|
||||
"""
|
||||
config_directory = set_tmp_config_dir()
|
||||
fp = tempfile.TemporaryFile()
|
||||
fp.write(DAEMON_SCRIPT % config_directory)
|
||||
fp.seek(0)
|
||||
|
||||
core = Popen([sys.executable], cwd=CWD, stdin=fp, stdout=PIPE, stderr=PIPE)
|
||||
while True:
|
||||
line = core.stderr.readline()
|
||||
if "Factory starting on 58846" in line:
|
||||
time.sleep(0.3) # Slight pause just incase
|
||||
break
|
||||
elif "Couldn't listen on localhost:58846" in line:
|
||||
raise SystemExit("Could not start deluge test client. %s" % line)
|
||||
elif 'Traceback' in line:
|
||||
raise SystemExit(
|
||||
"Failed to start core daemon. Do \"\"\" %s \"\"\" to see what's "
|
||||
"happening" %
|
||||
"python -c \"import sys; import tempfile; import deluge.main; "
|
||||
"import deluge.configmanager; config_directory = tempfile.mkdtemp(); "
|
||||
"deluge.configmanager.set_config_dir(config_directory); "
|
||||
"sys.argv.extend(['-d', '-c', config_directory, '-L', 'info']); "
|
||||
"deluge.main.start_daemon()\""
|
||||
)
|
||||
return core
|
||||
|
||||
@ -2,7 +2,7 @@ from twisted.trial import unittest
|
||||
|
||||
import common
|
||||
|
||||
from deluge.core.authmanager import AuthManager
|
||||
from deluge.core.authmanager import AuthManager, AUTH_LEVEL_ADMIN
|
||||
|
||||
class AuthManagerTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@ -11,4 +11,7 @@ class AuthManagerTestCase(unittest.TestCase):
|
||||
|
||||
def test_authorize(self):
|
||||
from deluge.ui import common
|
||||
self.assertEquals(self.auth.authorize(*common.get_localhost_auth()), 10)
|
||||
self.assertEquals(
|
||||
self.auth.authorize(*common.get_localhost_auth()),
|
||||
AUTH_LEVEL_ADMIN
|
||||
)
|
||||
|
||||
@ -1,27 +1,152 @@
|
||||
import tempfile
|
||||
import os
|
||||
import signal
|
||||
|
||||
import common
|
||||
|
||||
from twisted.internet import defer
|
||||
from twisted.trial import unittest
|
||||
|
||||
from deluge.ui.client import client
|
||||
from deluge import error
|
||||
from deluge.core.authmanager import AUTH_LEVEL_ADMIN
|
||||
from deluge.ui.client import client, Client, DaemonSSLProxy
|
||||
|
||||
# Start a daemon to test with and wait a couple seconds to make sure it's started
|
||||
config_directory = common.set_tmp_config_dir()
|
||||
client.start_daemon(58847, config_directory)
|
||||
import time
|
||||
time.sleep(2)
|
||||
|
||||
class NoVersionSendingDaemonSSLProxy(DaemonSSLProxy):
|
||||
def authenticate(self, username, password):
|
||||
self.login_deferred = defer.Deferred()
|
||||
d = self.call("daemon.login", username, password)
|
||||
d.addCallback(self.__on_login, username)
|
||||
d.addErrback(self.__on_login_fail)
|
||||
return self.login_deferred
|
||||
|
||||
def __on_login(self, result, username):
|
||||
self.login_deferred.callback(result)
|
||||
|
||||
def __on_login_fail(self, result):
|
||||
self.login_deferred.errback(result)
|
||||
|
||||
class NoVersionSendingClient(Client):
|
||||
|
||||
def connect(self, host="127.0.0.1", port=58846, username="", password="",
|
||||
skip_authentication=False):
|
||||
self._daemon_proxy = NoVersionSendingDaemonSSLProxy()
|
||||
self._daemon_proxy.set_disconnect_callback(self.__on_disconnect)
|
||||
|
||||
d = self._daemon_proxy.connect(host, port)
|
||||
|
||||
def on_connect_fail(reason):
|
||||
self.disconnect()
|
||||
return reason
|
||||
|
||||
def on_authenticate(result, daemon_info):
|
||||
return result
|
||||
|
||||
def on_authenticate_fail(reason):
|
||||
return reason
|
||||
|
||||
def on_connected(daemon_version):
|
||||
return daemon_version
|
||||
|
||||
def authenticate(daemon_version, username, password):
|
||||
d = self._daemon_proxy.authenticate(username, password)
|
||||
d.addCallback(on_authenticate, daemon_version)
|
||||
d.addErrback(on_authenticate_fail)
|
||||
return d
|
||||
|
||||
d.addCallback(on_connected)
|
||||
d.addErrback(on_connect_fail)
|
||||
if not skip_authentication:
|
||||
d.addCallback(authenticate, username, password)
|
||||
return d
|
||||
|
||||
def __on_disconnect(self):
|
||||
if self.disconnect_callback:
|
||||
self.disconnect_callback()
|
||||
|
||||
class ClientTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.core = common.start_core()
|
||||
|
||||
def tearDown(self):
|
||||
self.core.terminate()
|
||||
|
||||
def test_connect_no_credentials(self):
|
||||
d = client.connect("localhost", 58847)
|
||||
d.addCallback(self.assertEquals, 10)
|
||||
|
||||
d = client.connect("localhost", 58846)
|
||||
|
||||
def on_failure(failure):
|
||||
self.assertEqual(
|
||||
failure.trap(error.AuthenticationRequired),
|
||||
error.AuthenticationRequired
|
||||
)
|
||||
self.addCleanup(client.disconnect)
|
||||
|
||||
d.addErrback(on_failure)
|
||||
return d
|
||||
|
||||
def test_connect_localclient(self):
|
||||
from deluge.ui import common
|
||||
username, password = common.get_localhost_auth()
|
||||
d = client.connect(
|
||||
"localhost", 58846, username=username, password=password
|
||||
)
|
||||
|
||||
def on_connect(result):
|
||||
self.assertEqual(client.get_auth_level(), AUTH_LEVEL_ADMIN)
|
||||
self.addCleanup(client.disconnect)
|
||||
return result
|
||||
|
||||
d.addCallback(on_connect)
|
||||
return d
|
||||
|
||||
def test_connect_bad_password(self):
|
||||
from deluge.ui import common
|
||||
username, password = common.get_localhost_auth()
|
||||
d = client.connect(
|
||||
"localhost", 58846, username=username, password=password+'1'
|
||||
)
|
||||
|
||||
def on_failure(failure):
|
||||
self.assertEqual(
|
||||
failure.trap(error.BadLoginError),
|
||||
error.BadLoginError
|
||||
)
|
||||
self.addCleanup(client.disconnect)
|
||||
|
||||
d.addErrback(on_failure)
|
||||
return d
|
||||
|
||||
def test_connect_without_password(self):
|
||||
from deluge.ui import common
|
||||
username, password = common.get_localhost_auth()
|
||||
d = client.connect(
|
||||
"localhost", 58846, username=username
|
||||
)
|
||||
|
||||
def on_failure(failure):
|
||||
self.assertEqual(
|
||||
failure.trap(error.AuthenticationRequired),
|
||||
error.AuthenticationRequired
|
||||
)
|
||||
self.assertEqual(failure.value.username, username)
|
||||
self.addCleanup(client.disconnect)
|
||||
|
||||
d.addErrback(on_failure)
|
||||
return d
|
||||
|
||||
def test_connect_without_sending_client_version_fails(self):
|
||||
from deluge.ui import common
|
||||
username, password = common.get_localhost_auth()
|
||||
no_version_sending_client = NoVersionSendingClient()
|
||||
d = no_version_sending_client.connect(
|
||||
"localhost", 58846, username=username, password=password
|
||||
)
|
||||
|
||||
def on_failure(failure):
|
||||
self.assertEqual(
|
||||
failure.trap(error.IncompatibleClient),
|
||||
error.IncompatibleClient
|
||||
)
|
||||
self.addCleanup(no_version_sending_client.disconnect)
|
||||
|
||||
d.addErrback(on_failure)
|
||||
return d
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
from twisted.trial import unittest
|
||||
from twisted.internet import reactor
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.web.http import FORBIDDEN
|
||||
from twisted.web.resource import Resource
|
||||
from twisted.web.server import Site
|
||||
from twisted.web.static import File
|
||||
|
||||
try:
|
||||
from hashlib import sha1 as sha
|
||||
@ -8,19 +13,65 @@ except ImportError:
|
||||
|
||||
import os
|
||||
import common
|
||||
import warnings
|
||||
rpath = common.rpath
|
||||
|
||||
from deluge.core.rpcserver import RPCServer
|
||||
from deluge.core.core import Core
|
||||
warnings.filterwarnings("ignore", category=RuntimeWarning)
|
||||
from deluge.ui.web.common import compress
|
||||
warnings.resetwarnings()
|
||||
import deluge.component as component
|
||||
import deluge.error
|
||||
|
||||
class TestCookieResource(Resource):
|
||||
|
||||
def render(self, request):
|
||||
if request.getCookie("password") != "deluge":
|
||||
request.setResponseCode(FORBIDDEN)
|
||||
return
|
||||
|
||||
request.setHeader("Content-Type", "application/x-bittorrent")
|
||||
return open(rpath("ubuntu-9.04-desktop-i386.iso.torrent")).read()
|
||||
|
||||
class TestPartialDownload(Resource):
|
||||
|
||||
def render(self, request):
|
||||
data = open(rpath("ubuntu-9.04-desktop-i386.iso.torrent")).read()
|
||||
request.setHeader("Content-Type", len(data))
|
||||
request.setHeader("Content-Type", "application/x-bittorrent")
|
||||
if request.requestHeaders.hasHeader("accept-encoding"):
|
||||
return compress(data, request)
|
||||
return data
|
||||
|
||||
class TestRedirectResource(Resource):
|
||||
|
||||
def render(self, request):
|
||||
request.redirect("/ubuntu-9.04-desktop-i386.iso.torrent")
|
||||
return ""
|
||||
|
||||
class TopLevelResource(Resource):
|
||||
|
||||
addSlash = True
|
||||
|
||||
def __init__(self):
|
||||
Resource.__init__(self)
|
||||
self.putChild("cookie", TestCookieResource())
|
||||
self.putChild("partial", TestPartialDownload())
|
||||
self.putChild("redirect", TestRedirectResource())
|
||||
self.putChild("ubuntu-9.04-desktop-i386.iso.torrent", File(common.rpath("ubuntu-9.04-desktop-i386.iso.torrent")))
|
||||
|
||||
class CoreTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
common.set_tmp_config_dir()
|
||||
self.rpcserver = RPCServer(listen=False)
|
||||
self.core = Core()
|
||||
d = component.start()
|
||||
return d
|
||||
return component.start().addCallback(self.startWebserver)
|
||||
|
||||
def startWebserver(self, result):
|
||||
self.website = Site(TopLevelResource())
|
||||
self.webserver = reactor.listenTCP(51242, self.website)
|
||||
return result
|
||||
|
||||
def tearDown(self):
|
||||
|
||||
@ -28,6 +79,7 @@ class CoreTestCase(unittest.TestCase):
|
||||
component._ComponentRegistry.components = {}
|
||||
del self.rpcserver
|
||||
del self.core
|
||||
return self.webserver.stopListening()
|
||||
|
||||
return component.shutdown().addCallback(on_shutdown)
|
||||
|
||||
@ -44,7 +96,7 @@ class CoreTestCase(unittest.TestCase):
|
||||
self.assertEquals(torrent_id, info_hash)
|
||||
|
||||
def test_add_torrent_url(self):
|
||||
url = "http://deluge-torrent.org/ubuntu-9.04-desktop-i386.iso.torrent"
|
||||
url = "http://localhost:51242/ubuntu-9.04-desktop-i386.iso.torrent"
|
||||
options = {}
|
||||
info_hash = "60d5d82328b4547511fdeac9bf4d0112daa0ce00"
|
||||
|
||||
@ -53,7 +105,7 @@ class CoreTestCase(unittest.TestCase):
|
||||
return d
|
||||
|
||||
def test_add_torrent_url_with_cookie(self):
|
||||
url = "http://deluge-torrent.org/test_torrent.php"
|
||||
url = "http://localhost:51242/cookie"
|
||||
options = {}
|
||||
headers = { "Cookie" : "password=deluge" }
|
||||
info_hash = "60d5d82328b4547511fdeac9bf4d0112daa0ce00"
|
||||
@ -66,6 +118,26 @@ class CoreTestCase(unittest.TestCase):
|
||||
|
||||
return d
|
||||
|
||||
def test_add_torrent_url_with_redirect(self):
|
||||
url = "http://localhost:51242/redirect"
|
||||
options = {}
|
||||
info_hash = "60d5d82328b4547511fdeac9bf4d0112daa0ce00"
|
||||
|
||||
d = self.core.add_torrent_url(url, options)
|
||||
d.addCallback(self.assertEquals, info_hash)
|
||||
|
||||
return d
|
||||
|
||||
def test_add_torrent_url_with_partial_download(self):
|
||||
url = "http://localhost:51242/partial"
|
||||
options = {}
|
||||
info_hash = "60d5d82328b4547511fdeac9bf4d0112daa0ce00"
|
||||
|
||||
d = self.core.add_torrent_url(url, options)
|
||||
d.addCallback(self.assertEquals, info_hash)
|
||||
|
||||
return d
|
||||
|
||||
def test_add_magnet(self):
|
||||
info_hash = "60d5d82328b4547511fdeac9bf4d0112daa0ce00"
|
||||
import deluge.common
|
||||
|
||||
@ -1,17 +1,89 @@
|
||||
import os
|
||||
import warnings
|
||||
|
||||
from twisted.trial import unittest
|
||||
from twisted.internet import reactor
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.web.http import FORBIDDEN, NOT_MODIFIED
|
||||
from twisted.web.resource import Resource, ForbiddenResource
|
||||
from twisted.web.server import Site
|
||||
|
||||
from deluge.httpdownloader import download_file
|
||||
from deluge.log import setupLogger
|
||||
|
||||
warnings.filterwarnings("ignore", category=RuntimeWarning)
|
||||
from deluge.ui.web.common import compress
|
||||
warnings.resetwarnings()
|
||||
|
||||
from email.utils import formatdate
|
||||
|
||||
import common
|
||||
rpath = common.rpath
|
||||
|
||||
class TestRedirectResource(Resource):
|
||||
|
||||
def render(self, request):
|
||||
request.redirect("http://localhost:51242/")
|
||||
|
||||
class TestRenameResource(Resource):
|
||||
|
||||
def render(self, request):
|
||||
filename = request.args.get("filename", ["renamed_file"])[0]
|
||||
request.setHeader("Content-Type", "text/plain")
|
||||
request.setHeader("Content-Disposition", "attachment; filename=" +
|
||||
filename)
|
||||
return "This file should be called " + filename
|
||||
|
||||
class TestCookieResource(Resource):
|
||||
|
||||
def render(self, request):
|
||||
request.setHeader("Content-Type", "text/plain")
|
||||
if request.getCookie("password") is None:
|
||||
return "Password cookie not set!"
|
||||
|
||||
if request.getCookie("password") == "deluge":
|
||||
return "COOKIE MONSTER!"
|
||||
|
||||
return request.getCookie("password")
|
||||
|
||||
class TestGzipResource(Resource):
|
||||
|
||||
def render(self, request):
|
||||
message = request.args.get("msg", ["EFFICIENCY!"])[0]
|
||||
request.setHeader("Content-Type", "text/plain")
|
||||
return compress(message, request)
|
||||
|
||||
class TopLevelResource(Resource):
|
||||
|
||||
addSlash = True
|
||||
|
||||
def __init__(self):
|
||||
Resource.__init__(self)
|
||||
self.putChild("cookie", TestCookieResource())
|
||||
self.putChild("gzip", TestGzipResource())
|
||||
self.putChild("redirect", TestRedirectResource())
|
||||
self.putChild("rename", TestRenameResource())
|
||||
|
||||
def getChild(self, path, request):
|
||||
if path == "":
|
||||
return self
|
||||
else:
|
||||
return Resource.getChild(self, path, request)
|
||||
|
||||
def render(self, request):
|
||||
if request.getHeader("If-Modified-Since"):
|
||||
request.setResponseCode(NOT_MODIFIED)
|
||||
return "<h1>Deluge HTTP Downloader tests webserver here</h1>"
|
||||
|
||||
class DownloadFileTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
setupLogger("warning", "log_file")
|
||||
self.website = Site(TopLevelResource())
|
||||
self.webserver = reactor.listenTCP(51242, self.website)
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
return self.webserver.stopListening()
|
||||
|
||||
def assertContains(self, filename, contents):
|
||||
f = open(filename)
|
||||
@ -34,19 +106,19 @@ class DownloadFileTestCase(unittest.TestCase):
|
||||
return filename
|
||||
|
||||
def test_download(self):
|
||||
d = download_file("http://deluge-torrent.org", "index.html")
|
||||
d = download_file("http://localhost:51242/", "index.html")
|
||||
d.addCallback(self.assertEqual, "index.html")
|
||||
return d
|
||||
|
||||
def test_download_without_required_cookies(self):
|
||||
url = "http://damoxc.net/deluge/httpdownloader.php?test=cookie"
|
||||
url = "http://localhost:51242/cookie"
|
||||
d = download_file(url, "none")
|
||||
d.addCallback(self.fail)
|
||||
d.addErrback(self.assertIsInstance, Failure)
|
||||
return d
|
||||
|
||||
def test_download_with_required_cookies(self):
|
||||
url = "http://damoxc.net/deluge/httpdownloader.php?test=cookie"
|
||||
url = "http://localhost:51242/cookie"
|
||||
cookie = { "cookie" : "password=deluge" }
|
||||
d = download_file(url, "monster", headers=cookie)
|
||||
d.addCallback(self.assertEqual, "monster")
|
||||
@ -54,61 +126,61 @@ class DownloadFileTestCase(unittest.TestCase):
|
||||
return d
|
||||
|
||||
def test_download_with_rename(self):
|
||||
url = "http://damoxc.net/deluge/httpdownloader.php?test=rename&filename=renamed"
|
||||
url = "http://localhost:51242/rename?filename=renamed"
|
||||
d = download_file(url, "original")
|
||||
d.addCallback(self.assertEqual, "renamed")
|
||||
d.addCallback(self.assertContains, "This file should be called renamed")
|
||||
return d
|
||||
|
||||
def test_download_with_rename_fail(self):
|
||||
url = "http://damoxc.net/deluge/httpdownloader.php?test=rename&filename=renamed"
|
||||
url = "http://localhost:51242/rename?filename=renamed"
|
||||
d = download_file(url, "original")
|
||||
d.addCallback(self.assertEqual, "original")
|
||||
d.addCallback(self.assertContains, "This file should be called renamed")
|
||||
return d
|
||||
|
||||
def test_download_with_rename_sanitised(self):
|
||||
url = "http://damoxc.net/deluge/httpdownloader.php?test=rename&filename=/etc/passwd"
|
||||
url = "http://localhost:51242/rename?filename=/etc/passwd"
|
||||
d = download_file(url, "original")
|
||||
d.addCallback(self.assertEqual, "passwd")
|
||||
d.addCallback(self.assertContains, "This file should be called /etc/passwd")
|
||||
return d
|
||||
|
||||
def test_download_with_rename_prevented(self):
|
||||
url = "http://damoxc.net/deluge/httpdownloader.php?test=rename&filename=spam"
|
||||
url = "http://localhost:51242/rename?filename=spam"
|
||||
d = download_file(url, "forced", force_filename=True)
|
||||
d.addCallback(self.assertEqual, "forced")
|
||||
d.addCallback(self.assertContains, "This file should be called spam")
|
||||
return d
|
||||
|
||||
def test_download_with_gzip_encoding(self):
|
||||
url = "http://damoxc.net/deluge/httpdownloader.php?test=gzip&msg=success"
|
||||
url = "http://localhost:51242/gzip?msg=success"
|
||||
d = download_file(url, "gzip_encoded")
|
||||
d.addCallback(self.assertContains, "success")
|
||||
return d
|
||||
|
||||
def test_download_with_gzip_encoding_disabled(self):
|
||||
url = "http://damoxc.net/deluge/httpdownloader.php?test=gzip&msg=fail"
|
||||
url = "http://localhost:51242/gzip?msg=fail"
|
||||
d = download_file(url, "gzip_encoded", allow_compression=False)
|
||||
d.addCallback(self.failIfContains, "fail")
|
||||
return d
|
||||
|
||||
def test_page_redirect(self):
|
||||
url = "http://damoxc.net/deluge/httpdownloader.php?test=redirect"
|
||||
url = 'http://localhost:51242/redirect'
|
||||
d = download_file(url, "none")
|
||||
d.addCallback(self.fail)
|
||||
d.addErrback(self.assertIsInstance, Failure)
|
||||
return d
|
||||
|
||||
def test_page_not_found(self):
|
||||
d = download_file("http://does.not.exist", "none")
|
||||
d = download_file("http://localhost:51242/page/not/found", "none")
|
||||
d.addCallback(self.fail)
|
||||
d.addErrback(self.assertIsInstance, Failure)
|
||||
return d
|
||||
|
||||
def test_page_not_modified(self):
|
||||
headers = { 'If-Modified-Since' : formatdate(usegmt=True) }
|
||||
d = download_file("http://deluge-torrent.org", "index.html", headers=headers)
|
||||
d = download_file("http://localhost:51242/", "index.html", headers=headers)
|
||||
d.addCallback(self.fail)
|
||||
d.addErrback(self.assertIsInstance, Failure)
|
||||
return d
|
||||
|
||||
BIN
deluge/tests/ubuntu-9.04-desktop-i386.iso.torrent
Normal file
@ -2,6 +2,7 @@
|
||||
# client.py
|
||||
#
|
||||
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
@ -44,7 +45,7 @@ except ImportError:
|
||||
import zlib
|
||||
|
||||
import deluge.common
|
||||
import deluge.component as component
|
||||
from deluge import error
|
||||
from deluge.event import known_events
|
||||
|
||||
if deluge.common.windows_check():
|
||||
@ -61,18 +62,6 @@ log = logging.getLogger(__name__)
|
||||
def format_kwargs(kwargs):
|
||||
return ", ".join([key + "=" + str(value) for key, value in kwargs.items()])
|
||||
|
||||
class DelugeRPCError(object):
|
||||
"""
|
||||
This object is passed to errback handlers in the event of a RPCError from the
|
||||
daemon.
|
||||
"""
|
||||
def __init__(self, method, args, kwargs, exception_type, exception_msg, traceback):
|
||||
self.method = method
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.exception_type = exception_type
|
||||
self.exception_msg = exception_msg
|
||||
self.traceback = traceback
|
||||
|
||||
class DelugeRPCRequest(object):
|
||||
"""
|
||||
@ -108,12 +97,15 @@ class DelugeRPCRequest(object):
|
||||
|
||||
:returns: a properly formated RPCRequest
|
||||
"""
|
||||
if self.request_id is None or self.method is None or self.args is None or self.kwargs is None:
|
||||
raise TypeError("You must set the properties of this object before calling format_message!")
|
||||
if self.request_id is None or self.method is None or self.args is None \
|
||||
or self.kwargs is None:
|
||||
raise TypeError("You must set the properties of this object "
|
||||
"before calling format_message!")
|
||||
|
||||
return (self.request_id, self.method, self.args, self.kwargs)
|
||||
|
||||
class DelugeRPCProtocol(Protocol):
|
||||
|
||||
def connectionMade(self):
|
||||
self.__rpc_requests = {}
|
||||
self.__buffer = None
|
||||
@ -160,20 +152,20 @@ class DelugeRPCProtocol(Protocol):
|
||||
log.debug("Received invalid message: type is not tuple")
|
||||
return
|
||||
if len(request) < 3:
|
||||
log.debug("Received invalid message: number of items in response is %s", len(3))
|
||||
log.debug("Received invalid message: number of items in "
|
||||
"response is %s", len(3))
|
||||
return
|
||||
|
||||
message_type = request[0]
|
||||
|
||||
if message_type == RPC_EVENT:
|
||||
event_name = request[1]
|
||||
event = request[1]
|
||||
#log.debug("Received RPCEvent: %s", event)
|
||||
# A RPCEvent was received from the daemon so run any handlers
|
||||
# associated with it.
|
||||
if event_name in self.factory.event_handlers:
|
||||
event = known_events[event_name](*request[2])
|
||||
for handler in self.factory.event_handlers[event_name]:
|
||||
reactor.callLater(0, handler, event.copy())
|
||||
if event in self.factory.event_handlers:
|
||||
for handler in self.factory.event_handlers[event]:
|
||||
reactor.callLater(0, handler, *request[2])
|
||||
continue
|
||||
|
||||
request_id = request[1]
|
||||
@ -186,12 +178,35 @@ class DelugeRPCProtocol(Protocol):
|
||||
# Run the callbacks registered with this Deferred object
|
||||
d.callback(request[2])
|
||||
elif message_type == RPC_ERROR:
|
||||
# Create the DelugeRPCError to pass to the errback
|
||||
r = self.__rpc_requests[request_id]
|
||||
e = DelugeRPCError(r.method, r.args, r.kwargs, request[2][0], request[2][1], request[2][2])
|
||||
# Run the errbacks registered with this Deferred object
|
||||
d.errback(e)
|
||||
# Recreate exception and errback'it
|
||||
exception_cls = getattr(error, request[2])
|
||||
exception = exception_cls(*request[3], **request[4])
|
||||
|
||||
# Ideally we would chain the deferreds instead of instance
|
||||
# checking just to log them. But, that would mean that any
|
||||
# errback on the fist deferred should returns it's failure
|
||||
# so it could pass back to the 2nd deferred on the chain. But,
|
||||
# that does not always happen.
|
||||
# So, just do some instance checking and just log rpc error at
|
||||
# diferent levels.
|
||||
r = self.__rpc_requests[request_id]
|
||||
msg = "RPCError Message Received!"
|
||||
msg += "\n" + "-" * 80
|
||||
msg += "\n" + "RPCRequest: " + r.__repr__()
|
||||
msg += "\n" + "-" * 80
|
||||
msg += "\n" + request[5] + "\n" + request[2] + ": "
|
||||
msg += str(exception)
|
||||
msg += "\n" + "-" * 80
|
||||
|
||||
if not isinstance(exception, error._ClientSideRecreateError):
|
||||
# Let's log these as errors
|
||||
log.error(msg)
|
||||
else:
|
||||
# The rest just get's logged in debug level, just to log
|
||||
# what's happending
|
||||
log.debug(msg)
|
||||
|
||||
d.errback(exception)
|
||||
del self.__rpc_requests[request_id]
|
||||
|
||||
def send_request(self, request):
|
||||
@ -222,14 +237,17 @@ class DelugeRPCClientFactory(ClientFactory):
|
||||
self.bytes_sent = 0
|
||||
|
||||
def startedConnecting(self, connector):
|
||||
log.info("Connecting to daemon at %s:%s..", connector.host, connector.port)
|
||||
log.info("Connecting to daemon at \"%s:%s\"...",
|
||||
connector.host, connector.port)
|
||||
|
||||
def clientConnectionFailed(self, connector, reason):
|
||||
log.warning("Connection to daemon at %s:%s failed: %s", connector.host, connector.port, reason.value)
|
||||
log.warning("Connection to daemon at \"%s:%s\" failed: %s",
|
||||
connector.host, connector.port, reason.value)
|
||||
self.daemon.connect_deferred.errback(reason)
|
||||
|
||||
def clientConnectionLost(self, connector, reason):
|
||||
log.info("Connection lost to daemon at %s:%s reason: %s", connector.host, connector.port, reason.value)
|
||||
log.info("Connection lost to daemon at \"%s:%s\" reason: %s",
|
||||
connector.host, connector.port, reason.value)
|
||||
self.daemon.host = None
|
||||
self.daemon.port = None
|
||||
self.daemon.username = None
|
||||
@ -256,37 +274,43 @@ class DaemonSSLProxy(DaemonProxy):
|
||||
self.host = None
|
||||
self.port = None
|
||||
self.username = None
|
||||
self.authentication_level = 0
|
||||
|
||||
self.connected = False
|
||||
|
||||
self.disconnect_deferred = None
|
||||
self.disconnect_callback = None
|
||||
|
||||
def connect(self, host, port, username, password):
|
||||
self.auth_levels_mapping = None
|
||||
self.auth_levels_mapping_reverse = None
|
||||
|
||||
def connect(self, host, port):
|
||||
"""
|
||||
Connects to a daemon at host:port
|
||||
|
||||
:param host: str, the host to connect to
|
||||
:param port: int, the listening port on the daemon
|
||||
:param username: str, the username to login as
|
||||
:param password: str, the password to login with
|
||||
|
||||
:returns: twisted.Deferred
|
||||
|
||||
"""
|
||||
log.debug("sslproxy.connect()")
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.__connector = reactor.connectSSL(self.host, self.port, self.__factory, ssl.ClientContextFactory())
|
||||
self.__connector = reactor.connectSSL(self.host, self.port,
|
||||
self.__factory,
|
||||
ssl.ClientContextFactory())
|
||||
self.connect_deferred = defer.Deferred()
|
||||
self.login_deferred = defer.Deferred()
|
||||
self.daemon_info_deferred = defer.Deferred()
|
||||
|
||||
# Upon connect we do a 'daemon.login' RPC
|
||||
self.connect_deferred.addCallback(self.__on_connect, username, password)
|
||||
self.connect_deferred.addCallback(self.__on_connect)
|
||||
self.connect_deferred.addErrback(self.__on_connect_fail)
|
||||
|
||||
return self.login_deferred
|
||||
return self.daemon_info_deferred
|
||||
|
||||
def disconnect(self):
|
||||
log.debug("sslproxy.disconnect()")
|
||||
self.disconnect_deferred = defer.Deferred()
|
||||
self.__connector.disconnect()
|
||||
return self.disconnect_deferred
|
||||
@ -315,7 +339,6 @@ class DaemonSSLProxy(DaemonProxy):
|
||||
# Create a Deferred object to return and add a default errback to print
|
||||
# the error.
|
||||
d = defer.Deferred()
|
||||
d.addErrback(self.__rpcError)
|
||||
|
||||
# Store the Deferred until we receive a response from the daemon
|
||||
self.__deferred[self.__request_counter] = d
|
||||
@ -373,50 +396,58 @@ class DaemonSSLProxy(DaemonProxy):
|
||||
if event in self.__factory.event_handlers and handler in self.__factory.event_handlers[event]:
|
||||
self.__factory.event_handlers[event].remove(handler)
|
||||
|
||||
def __rpcError(self, error_data):
|
||||
"""
|
||||
Prints out a RPCError message to the error log. This includes the daemon
|
||||
traceback.
|
||||
def __on_connect(self, result):
|
||||
log.debug("__on_connect called")
|
||||
|
||||
:param error_data: this is passed from the deferred errback with error.value
|
||||
containing a `:class:DelugeRPCError` object.
|
||||
"""
|
||||
# Get the DelugeRPCError object from the error_data
|
||||
error = error_data.value
|
||||
# Create a delugerpcrequest to print out a nice RPCRequest string
|
||||
r = DelugeRPCRequest()
|
||||
r.method = error.method
|
||||
r.args = error.args
|
||||
r.kwargs = error.kwargs
|
||||
msg = "RPCError Message Received!"
|
||||
msg += "\n" + "-" * 80
|
||||
msg += "\n" + "RPCRequest: " + r.__repr__()
|
||||
msg += "\n" + "-" * 80
|
||||
msg += "\n" + error.traceback + "\n" + error.exception_type + ": " + error.exception_msg
|
||||
msg += "\n" + "-" * 80
|
||||
log.error(msg)
|
||||
return error_data
|
||||
def on_info(daemon_info):
|
||||
self.daemon_info = daemon_info
|
||||
log.debug("Got info from daemon: %s", daemon_info)
|
||||
self.daemon_info_deferred.callback(daemon_info)
|
||||
|
||||
def __on_connect(self, result, username, password):
|
||||
self.__login_deferred = self.call("daemon.login", username, password)
|
||||
self.__login_deferred.addCallback(self.__on_login, username)
|
||||
self.__login_deferred.addErrback(self.__on_login_fail)
|
||||
def on_info_fail(reason):
|
||||
log.debug("Failed to get info from daemon")
|
||||
log.exception(reason)
|
||||
self.daemon_info_deferred.errback(reason)
|
||||
|
||||
self.call("daemon.info").addCallback(on_info).addErrback(on_info_fail)
|
||||
return self.daemon_info_deferred
|
||||
|
||||
def __on_connect_fail(self, reason):
|
||||
log.debug("connect_fail: %s", reason)
|
||||
self.login_deferred.errback(reason)
|
||||
self.daemon_info_deferred.errback(reason)
|
||||
|
||||
def authenticate(self, username, password):
|
||||
log.debug("%s.authenticate: %s", self.__class__.__name__, username)
|
||||
self.login_deferred = defer.Deferred()
|
||||
d = self.call("daemon.login", username, password,
|
||||
client_version=deluge.common.get_version())
|
||||
d.addCallback(self.__on_login, username)
|
||||
d.addErrback(self.__on_login_fail)
|
||||
return self.login_deferred
|
||||
|
||||
def __on_login(self, result, username):
|
||||
log.debug("__on_login called: %s %s", username, result)
|
||||
self.username = username
|
||||
self.authentication_level = result
|
||||
# We need to tell the daemon what events we're interested in receiving
|
||||
if self.__factory.event_handlers:
|
||||
self.call("daemon.set_event_interest", self.__factory.event_handlers.keys())
|
||||
self.call("daemon.set_event_interest",
|
||||
self.__factory.event_handlers.keys())
|
||||
|
||||
self.call("core.get_auth_levels_mappings").addCallback(
|
||||
self.__on_auth_levels_mappings
|
||||
)
|
||||
|
||||
self.login_deferred.callback(result)
|
||||
|
||||
def __on_login_fail(self, result):
|
||||
log.debug("_on_login_fail(): %s", result)
|
||||
log.debug("_on_login_fail(): %s", result.value)
|
||||
self.login_deferred.errback(result)
|
||||
|
||||
def __on_auth_levels_mappings(self, result):
|
||||
auth_levels_mapping, auth_levels_mapping_reverse = result
|
||||
self.auth_levels_mapping = auth_levels_mapping
|
||||
self.auth_levels_mapping_reverse = auth_levels_mapping_reverse
|
||||
|
||||
def set_disconnect_callback(self, cb):
|
||||
"""
|
||||
Set a function to be called when the connection to the daemon is lost
|
||||
@ -438,13 +469,21 @@ class DaemonClassicProxy(DaemonProxy):
|
||||
self.connected = True
|
||||
self.host = "localhost"
|
||||
self.port = 58846
|
||||
# Running in classic mode, it's safe to import auth level
|
||||
from deluge.core.authmanager import (AUTH_LEVEL_ADMIN,
|
||||
AUTH_LEVELS_MAPPING,
|
||||
AUTH_LEVELS_MAPPING_REVERSE)
|
||||
self.username = "localclient"
|
||||
self.authentication_level = AUTH_LEVEL_ADMIN
|
||||
self.auth_levels_mapping = AUTH_LEVELS_MAPPING
|
||||
self.auth_levels_mapping_reverse = AUTH_LEVELS_MAPPING_REVERSE
|
||||
# Register the event handlers
|
||||
for event in event_handlers:
|
||||
for handler in event_handlers[event]:
|
||||
self.__daemon.core.eventmanager.register_event_handler(event, handler)
|
||||
|
||||
def disconnect(self):
|
||||
self.connected = False
|
||||
self.__daemon = None
|
||||
|
||||
def call(self, method, *args, **kwargs):
|
||||
@ -458,12 +497,14 @@ class DaemonClassicProxy(DaemonProxy):
|
||||
log.exception(e)
|
||||
return defer.fail(e)
|
||||
else:
|
||||
return defer.maybeDeferred(m, *copy.deepcopy(args), **copy.deepcopy(kwargs))
|
||||
return defer.maybeDeferred(
|
||||
m, *copy.deepcopy(args), **copy.deepcopy(kwargs)
|
||||
)
|
||||
|
||||
def register_event_handler(self, event, handler):
|
||||
"""
|
||||
Registers a handler function to be called when `:param:event` is received
|
||||
from the daemon.
|
||||
Registers a handler function to be called when `:param:event` is
|
||||
received from the daemon.
|
||||
|
||||
:param event: the name of the event to handle
|
||||
:type event: str
|
||||
@ -520,7 +561,8 @@ class Client(object):
|
||||
self.disconnect_callback = None
|
||||
self.__started_in_classic = False
|
||||
|
||||
def connect(self, host="127.0.0.1", port=58846, username="", password=""):
|
||||
def connect(self, host="127.0.0.1", port=58846, username="", password="",
|
||||
skip_authentication=False):
|
||||
"""
|
||||
Connects to a daemon process.
|
||||
|
||||
@ -532,27 +574,50 @@ class Client(object):
|
||||
:returns: a Deferred object that will be called once the connection
|
||||
has been established or fails
|
||||
"""
|
||||
if not username and host in ("127.0.0.1", "localhost"):
|
||||
# No username was provided and it's the localhost, so we can try
|
||||
# to grab the credentials from the auth file.
|
||||
import common
|
||||
username, password = common.get_localhost_auth()
|
||||
|
||||
self._daemon_proxy = DaemonSSLProxy(dict(self.__event_handlers))
|
||||
self._daemon_proxy.set_disconnect_callback(self.__on_disconnect)
|
||||
d = self._daemon_proxy.connect(host, port, username, password)
|
||||
def on_connect_fail(result):
|
||||
log.debug("on_connect_fail: %s", result)
|
||||
|
||||
d = self._daemon_proxy.connect(host, port)
|
||||
|
||||
def on_connect_fail(reason):
|
||||
self.disconnect()
|
||||
return reason
|
||||
|
||||
def on_authenticate(result, daemon_info):
|
||||
log.debug("Authentication sucessfull: %s", result)
|
||||
return result
|
||||
|
||||
def on_authenticate_fail(reason):
|
||||
log.debug("Failed to authenticate: %s", reason.value)
|
||||
return reason
|
||||
|
||||
def on_connected(daemon_version):
|
||||
log.debug("Client.connect.on_connected. Daemon version: %s",
|
||||
daemon_version)
|
||||
return daemon_version
|
||||
|
||||
def authenticate(daemon_version, username, password):
|
||||
d = self._daemon_proxy.authenticate(username, password)
|
||||
d.addCallback(on_authenticate, daemon_version)
|
||||
d.addErrback(on_authenticate_fail)
|
||||
return d
|
||||
|
||||
d.addCallback(on_connected)
|
||||
d.addErrback(on_connect_fail)
|
||||
if not skip_authentication:
|
||||
d.addCallback(authenticate, username, password)
|
||||
return d
|
||||
|
||||
def disconnect(self):
|
||||
"""
|
||||
Disconnects from the daemon.
|
||||
"""
|
||||
if self.is_classicmode():
|
||||
self._daemon_proxy.disconnect()
|
||||
self.stop_classic_mode()
|
||||
return
|
||||
|
||||
if self._daemon_proxy:
|
||||
return self._daemon_proxy.disconnect()
|
||||
|
||||
@ -563,6 +628,13 @@ class Client(object):
|
||||
self._daemon_proxy = DaemonClassicProxy(self.__event_handlers)
|
||||
self.__started_in_classic = True
|
||||
|
||||
def stop_classic_mode(self):
|
||||
"""
|
||||
Stops the daemon process in the client.
|
||||
"""
|
||||
self._daemon_proxy = None
|
||||
self.__started_in_classic = False
|
||||
|
||||
def start_daemon(self, port, config):
|
||||
"""
|
||||
Starts a daemon process.
|
||||
@ -698,5 +770,31 @@ class Client(object):
|
||||
"""
|
||||
return self._daemon_proxy.get_bytes_sent()
|
||||
|
||||
def get_auth_user(self):
|
||||
"""
|
||||
Returns the current authenticated username.
|
||||
|
||||
:returns: the authenticated username
|
||||
:rtype: str
|
||||
"""
|
||||
return self._daemon_proxy.username
|
||||
|
||||
def get_auth_level(self):
|
||||
"""
|
||||
Returns the authentication level the daemon returned upon authentication.
|
||||
|
||||
:returns: the authentication level
|
||||
:rtype: int
|
||||
"""
|
||||
return self._daemon_proxy.authentication_level
|
||||
|
||||
@property
|
||||
def auth_levels_mapping(self):
|
||||
return self._daemon_proxy.auth_levels_mapping
|
||||
|
||||
@property
|
||||
def auth_levels_mapping_reverse(self):
|
||||
return self._daemon_proxy.auth_levels_mapping_reverse
|
||||
|
||||
# This is the object clients will use
|
||||
client = Client()
|
||||
|
||||
@ -407,26 +407,28 @@ def get_localhost_auth():
|
||||
:rtype: tuple
|
||||
"""
|
||||
auth_file = deluge.configmanager.get_config_dir("auth")
|
||||
if os.path.exists(auth_file):
|
||||
for line in open(auth_file):
|
||||
if line.startswith("#"):
|
||||
# This is a comment line
|
||||
continue
|
||||
line = line.strip()
|
||||
try:
|
||||
lsplit = line.split(":")
|
||||
except Exception, e:
|
||||
log.error("Your auth file is malformed: %s", e)
|
||||
continue
|
||||
if not os.path.exists(auth_file):
|
||||
from deluge.common import create_localclient_account
|
||||
create_localclient_account()
|
||||
|
||||
if len(lsplit) == 2:
|
||||
username, password = lsplit
|
||||
elif len(lsplit) == 3:
|
||||
username, password, level = lsplit
|
||||
else:
|
||||
log.error("Your auth file is malformed: Incorrect number of fields!")
|
||||
continue
|
||||
for line in open(auth_file):
|
||||
if line.startswith("#"):
|
||||
# This is a comment line
|
||||
continue
|
||||
line = line.strip()
|
||||
try:
|
||||
lsplit = line.split(":")
|
||||
except Exception, e:
|
||||
log.error("Your auth file is malformed: %s", e)
|
||||
continue
|
||||
|
||||
if username == "localclient":
|
||||
return (username, password)
|
||||
return ("", "")
|
||||
if len(lsplit) == 2:
|
||||
username, password = lsplit
|
||||
elif len(lsplit) == 3:
|
||||
username, password, level = lsplit
|
||||
else:
|
||||
log.error("Your auth file is malformed: Incorrect number of fields!")
|
||||
continue
|
||||
|
||||
if username == "localclient":
|
||||
return (username, password)
|
||||
|
||||
@ -57,11 +57,17 @@ color_pairs = {
|
||||
# Some default color schemes
|
||||
schemes = {
|
||||
"input": ("white", "black"),
|
||||
"normal": ("white","black"),
|
||||
"status": ("yellow", "blue", "bold"),
|
||||
"info": ("white", "black", "bold"),
|
||||
"error": ("red", "black", "bold"),
|
||||
"success": ("green", "black", "bold"),
|
||||
"event": ("magenta", "black", "bold")
|
||||
"event": ("magenta", "black", "bold"),
|
||||
"selected": ("black", "white", "bold"),
|
||||
"marked": ("white","blue","bold"),
|
||||
"selectedmarked": ("blue","white","bold"),
|
||||
"header": ("green","black","bold"),
|
||||
"filterstatus": ("green", "blue", "bold")
|
||||
}
|
||||
|
||||
# Colors for various torrent states
|
||||
@ -94,6 +100,14 @@ def init_colors():
|
||||
curses.init_pair(counter, getattr(curses, fg), getattr(curses, bg))
|
||||
counter += 1
|
||||
|
||||
# try to redefine white/black as it makes underlining work for some terminals
|
||||
# but can also fail on others, so we try/except
|
||||
try:
|
||||
curses.init_pair(counter, curses.COLOR_WHITE, curses.COLOR_BLACK)
|
||||
color_pairs[("white","black")] = counter
|
||||
except:
|
||||
pass
|
||||
|
||||
class BadColorString(Exception):
|
||||
pass
|
||||
|
||||
|
||||
146
deluge/ui/console/commander.py
Normal file
@ -0,0 +1,146 @@
|
||||
#
|
||||
# commander.py
|
||||
#
|
||||
# Copyright (C) 2008-2009 Ido Abramovich <ido.deluge@gmail.com>
|
||||
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
# You may redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License, as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# deluge is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with deluge. If not, write to:
|
||||
# The Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# In addition, as a special exception, the copyright holders give
|
||||
# permission to link the code of portions of this program with the OpenSSL
|
||||
# library.
|
||||
# You must obey the GNU General Public License in all respects for all of
|
||||
# the code used other than OpenSSL. If you modify file(s) with this
|
||||
# exception, you may extend this exception to your version of the file(s),
|
||||
# but you are not obligated to do so. If you do not wish to do so, delete
|
||||
# this exception statement from your version. If you delete this exception
|
||||
# statement from all source files in the program, then also delete it here.
|
||||
#
|
||||
#
|
||||
|
||||
from twisted.internet import defer, reactor
|
||||
import deluge.component as component
|
||||
from deluge.error import DelugeError
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console import UI_PATH
|
||||
from colors import strip_colors
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class Commander:
|
||||
def __init__(self, cmds, interactive=False):
|
||||
self._commands = cmds
|
||||
self.console = component.get("ConsoleUI")
|
||||
self.interactive = interactive
|
||||
|
||||
def write(self,line):
|
||||
print(strip_colors(line))
|
||||
|
||||
def do_command(self, cmd):
|
||||
"""
|
||||
Processes a command.
|
||||
|
||||
:param cmd: str, the command string
|
||||
|
||||
"""
|
||||
if not cmd:
|
||||
return
|
||||
cmd, _, line = cmd.partition(' ')
|
||||
try:
|
||||
parser = self._commands[cmd].create_parser()
|
||||
except KeyError:
|
||||
self.write("{!error!}Unknown command: %s" % cmd)
|
||||
return
|
||||
args = self._commands[cmd].split(line)
|
||||
|
||||
# Do a little hack here to print 'command --help' properly
|
||||
parser._print_help = parser.print_help
|
||||
def print_help(f=None):
|
||||
if self.interactive:
|
||||
self.write(parser.format_help())
|
||||
else:
|
||||
parser._print_help(f)
|
||||
parser.print_help = print_help
|
||||
|
||||
# Only these commands can be run when not connected to a daemon
|
||||
not_connected_cmds = ["help", "connect", "quit"]
|
||||
aliases = []
|
||||
for c in not_connected_cmds:
|
||||
aliases.extend(self._commands[c].aliases)
|
||||
not_connected_cmds.extend(aliases)
|
||||
|
||||
if not client.connected() and cmd not in not_connected_cmds:
|
||||
self.write("{!error!}Not connected to a daemon, please use the connect command first.")
|
||||
return
|
||||
|
||||
try:
|
||||
options, args = parser.parse_args(args)
|
||||
except Exception, e:
|
||||
self.write("{!error!}Error parsing options: %s" % e)
|
||||
return
|
||||
|
||||
if not getattr(options, '_exit', False):
|
||||
try:
|
||||
ret = self._commands[cmd].handle(*args, **options.__dict__)
|
||||
except Exception, e:
|
||||
self.write("{!error!}" + str(e))
|
||||
log.exception(e)
|
||||
import traceback
|
||||
self.write("%s" % traceback.format_exc())
|
||||
return defer.succeed(True)
|
||||
else:
|
||||
return ret
|
||||
|
||||
def exec_args(self,args,host,port,username,password):
|
||||
def on_connect(result):
|
||||
def on_started(result):
|
||||
def on_started(result):
|
||||
deferreds = []
|
||||
# If we have args, lets process them and quit
|
||||
# allow multiple commands split by ";"
|
||||
for arg in args.split(";"):
|
||||
deferreds.append(defer.maybeDeferred(self.do_command, arg.strip()))
|
||||
|
||||
def on_complete(result):
|
||||
self.do_command("quit")
|
||||
|
||||
dl = defer.DeferredList(deferreds).addCallback(on_complete)
|
||||
|
||||
# We need to wait for the rpcs in start() to finish before processing
|
||||
# any of the commands.
|
||||
self.console.started_deferred.addCallback(on_started)
|
||||
component.start().addCallback(on_started)
|
||||
|
||||
def on_connect_fail(reason):
|
||||
if reason.check(DelugeError):
|
||||
rm = reason.value.message
|
||||
else:
|
||||
rm = reason.getErrorMessage()
|
||||
print "Could not connect to: %s:%d\n %s"%(host,port,rm)
|
||||
self.do_command("quit")
|
||||
|
||||
if host:
|
||||
d = client.connect(host,port,username,password)
|
||||
else:
|
||||
d = client.connect()
|
||||
d.addCallback(on_connect)
|
||||
d.addErrback(on_connect_fail)
|
||||
|
||||
@ -39,6 +39,7 @@ from deluge.ui.console.main import BaseCommand
|
||||
import deluge.ui.console.colors as colors
|
||||
from deluge.ui.client import client
|
||||
import deluge.component as component
|
||||
import deluge.common
|
||||
|
||||
from optparse import make_option
|
||||
import os
|
||||
@ -49,36 +50,52 @@ class Command(BaseCommand):
|
||||
option_list = BaseCommand.option_list + (
|
||||
make_option('-p', '--path', dest='path',
|
||||
help='save path for torrent'),
|
||||
make_option('-u', '--urls', action='store_true', default=False, dest='force_url',
|
||||
help='Interpret all given torrent-file arguments as URLs'),
|
||||
make_option('-f', '--files', action='store_true', default=False, dest='force_file',
|
||||
help='Interpret all given torrent-file arguments as files'),
|
||||
)
|
||||
|
||||
usage = "Usage: add [-p <save-location>] <torrent-file> [<torrent-file> ...]"
|
||||
usage = "Usage: add [-p <save-location>] [-u | --urls] [-f | --files] <torrent-file> [<torrent-file> ...]\n"\
|
||||
" <torrent-file> arguments can be file paths, URLs or magnet uris"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.console = component.get("ConsoleUI")
|
||||
|
||||
if options["force_file"] and options["force_url"]:
|
||||
self.console.write("{!error!}Cannot specify --urls and --files at the same time")
|
||||
return
|
||||
|
||||
t_options = {}
|
||||
if options["path"]:
|
||||
t_options["download_location"] = os.path.expanduser(options["path"])
|
||||
|
||||
def on_success(result):
|
||||
self.console.write("{!success!}Torrent added!")
|
||||
def on_fail(result):
|
||||
self.console.write("{!error!}Torrent was not added! %s" % result)
|
||||
|
||||
# Keep a list of deferreds to make a DeferredList
|
||||
deferreds = []
|
||||
for arg in args:
|
||||
if not os.path.exists(arg):
|
||||
self.console.write("{!error!}%s doesn't exist!" % arg)
|
||||
continue
|
||||
if not os.path.isfile(arg):
|
||||
self.console.write("{!error!}This is a directory!")
|
||||
continue
|
||||
self.console.write("{!info!}Attempting to add torrent: %s" % arg)
|
||||
filename = os.path.split(arg)[-1]
|
||||
filedump = base64.encodestring(open(arg).read())
|
||||
if not options["force_file"] and (deluge.common.is_url(arg) or options["force_url"]):
|
||||
self.console.write("{!info!}Attempting to add torrent from url: %s" % arg)
|
||||
deferreds.append(client.core.add_torrent_url(arg, t_options).addCallback(on_success).addErrback(on_fail))
|
||||
elif not options["force_file"] and (deluge.common.is_magnet(arg)):
|
||||
self.console.write("{!info!}Attempting to add torrent from magnet uri: %s" % arg)
|
||||
deferreds.append(client.core.add_torrent_magnet(arg, t_options).addCallback(on_success).addErrback(on_fail))
|
||||
else:
|
||||
if not os.path.exists(arg):
|
||||
self.console.write("{!error!}%s doesn't exist!" % arg)
|
||||
continue
|
||||
if not os.path.isfile(arg):
|
||||
self.console.write("{!error!}This is a directory!")
|
||||
continue
|
||||
self.console.write("{!info!}Attempting to add torrent: %s" % arg)
|
||||
filename = os.path.split(arg)[-1]
|
||||
filedump = base64.encodestring(open(arg, "rb").read())
|
||||
|
||||
def on_success(result):
|
||||
self.console.write("{!success!}Torrent added!")
|
||||
def on_fail(result):
|
||||
self.console.write("{!error!}Torrent was not added! %s" % result)
|
||||
|
||||
deferreds.append(client.core.add_torrent_file(filename, filedump, t_options).addCallback(on_success).addErrback(on_fail))
|
||||
deferreds.append(client.core.add_torrent_file(filename, filedump, t_options).addCallback(on_success).addErrback(on_fail))
|
||||
|
||||
return defer.DeferredList(deferreds)
|
||||
|
||||
|
||||
@ -47,4 +47,6 @@ class Command(BaseCommand):
|
||||
for key, value in status.items():
|
||||
self.console.write("{!info!}%s: {!input!}%s" % (key, value))
|
||||
|
||||
client.core.get_cache_status().addCallback(on_cache_status)
|
||||
d = client.core.get_cache_status()
|
||||
d.addCallback(on_cache_status)
|
||||
return d
|
||||
|
||||
@ -62,7 +62,10 @@ def atom(next, token):
|
||||
return tuple(out)
|
||||
elif token[0] is tokenize.NUMBER or token[1] == "-":
|
||||
try:
|
||||
return int(token[-1], 0)
|
||||
if token[1] == "-":
|
||||
return int(token[-1], 0)
|
||||
else:
|
||||
return int(token[1], 0)
|
||||
except ValueError:
|
||||
return float(token[-1])
|
||||
elif token[1].lower() == 'true':
|
||||
@ -133,7 +136,7 @@ class Command(BaseCommand):
|
||||
deferred = defer.Deferred()
|
||||
config = component.get("CoreConfig")
|
||||
key = options["set"][0]
|
||||
val = simple_eval(options["set"][1] + " " + " ".join(args))
|
||||
val = simple_eval(options["set"][1] + " " .join(args))
|
||||
|
||||
if key not in config.keys():
|
||||
self.console.write("{!error!}The key '%s' is invalid!" % key)
|
||||
|
||||
@ -44,7 +44,7 @@ import deluge.component as component
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Enable and disable debugging"""
|
||||
usage = 'debug [on|off]'
|
||||
usage = 'Usage: debug [on|off]'
|
||||
def handle(self, state='', **options):
|
||||
if state == 'on':
|
||||
deluge.log.setLoggerLevel("debug")
|
||||
|
||||
51
deluge/ui/console/commands/gui.py
Normal file
@ -0,0 +1,51 @@
|
||||
#
|
||||
# status.py
|
||||
#
|
||||
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
# You may redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License, as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# deluge is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with deluge. If not, write to:
|
||||
# The Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# In addition, as a special exception, the copyright holders give
|
||||
# permission to link the code of portions of this program with the OpenSSL
|
||||
# library.
|
||||
# You must obey the GNU General Public License in all respects for all of
|
||||
# the code used other than OpenSSL. If you modify file(s) with this
|
||||
# exception, you may extend this exception to your version of the file(s),
|
||||
# but you are not obligated to do so. If you do not wish to do so, delete
|
||||
# this exception statement from your version. If you delete this exception
|
||||
# statement from all source files in the program, then also delete it here.
|
||||
#
|
||||
|
||||
from deluge.ui.console.main import BaseCommand
|
||||
import deluge.common
|
||||
import deluge.component as component
|
||||
from deluge.ui.console.modes.alltorrents import AllTorrents
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Exit this mode and go into the more 'gui' like mode"""
|
||||
usage = "Usage: gui"
|
||||
interactive_only = True
|
||||
def handle(self, *args, **options):
|
||||
console = component.get("ConsoleUI")
|
||||
try:
|
||||
at = component.get("AllTorrents")
|
||||
except KeyError:
|
||||
at = AllTorrents(console.stdscr,console.encoding)
|
||||
console.set_mode(at)
|
||||
at.resume()
|
||||
@ -39,7 +39,8 @@ from deluge.ui.client import client
|
||||
import deluge.component as component
|
||||
|
||||
class Command(BaseCommand):
|
||||
"Shutdown the deluge server."
|
||||
"Shutdown the deluge server"
|
||||
usage = "Usage: halt"
|
||||
def handle(self, **options):
|
||||
self.console = component.get("ConsoleUI")
|
||||
|
||||
|
||||
@ -70,6 +70,8 @@ status_keys = ["state",
|
||||
"is_finished"
|
||||
]
|
||||
|
||||
states = ["Active", "Downloading", "Seeding", "Paused", "Checking", "Error", "Queued"]
|
||||
|
||||
|
||||
def format_progressbar(progress, width):
|
||||
"""
|
||||
@ -85,7 +87,7 @@ def format_progressbar(progress, width):
|
||||
s = "["
|
||||
p = int(round((progress/100) * w))
|
||||
s += "#" * p
|
||||
s += "~" * (w - p)
|
||||
s += "-" * (w - p)
|
||||
s += "]"
|
||||
return s
|
||||
|
||||
@ -98,11 +100,17 @@ class Command(BaseCommand):
|
||||
help='shows more information per torrent'),
|
||||
make_option('-i', '--id', action='store_true', default=False, dest='tid',
|
||||
help='use internal id instead of torrent name'),
|
||||
make_option('-s', '--state', action='store', dest='state',
|
||||
help="Only retrieve torrents in state STATE. "
|
||||
"Allowable values are: %s "%(", ".join(states))),
|
||||
)
|
||||
|
||||
usage = "Usage: info [<torrent-id> [<torrent-id> ...]]\n"\
|
||||
usage = "Usage: info [-v | -i | -s <state>] [<torrent-id> [<torrent-id> ...]]\n"\
|
||||
" info -s <state> will show only torrents in state <state>\n"\
|
||||
" You can give the first few characters of a torrent-id to identify the torrent."
|
||||
|
||||
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.console = component.get("ConsoleUI")
|
||||
# Compile a list of torrent_ids to request the status of
|
||||
@ -121,7 +129,17 @@ class Command(BaseCommand):
|
||||
def on_torrents_status_fail(reason):
|
||||
self.console.write("{!error!}Error getting torrent info: %s" % reason)
|
||||
|
||||
d = client.core.get_torrents_status({"id": torrent_ids}, status_keys)
|
||||
status_dict = {"id": torrent_ids}
|
||||
|
||||
if options["state"]:
|
||||
if options["state"] not in states:
|
||||
self.console.write("Invalid state: %s"%options["state"])
|
||||
self.console.write("Allowble values are: %s."%(", ".join(states)))
|
||||
return
|
||||
else:
|
||||
status_dict["state"] = options["state"]
|
||||
|
||||
d = client.core.get_torrents_status(status_dict, status_keys)
|
||||
d.addCallback(on_torrents_status)
|
||||
d.addErrback(on_torrents_status_fail)
|
||||
return d
|
||||
@ -189,10 +207,11 @@ class Command(BaseCommand):
|
||||
s += " %s" % (fp)
|
||||
# Check if this is too long for the screen and reduce the path
|
||||
# if necessary
|
||||
cols = self.console.screen.cols
|
||||
slen = colors.get_line_length(s, self.console.screen.encoding)
|
||||
if slen > cols:
|
||||
s = s.replace(f["path"], f["path"][slen - cols + 1:])
|
||||
if hasattr(self.console, "screen"):
|
||||
cols = self.console.screen.cols
|
||||
slen = colors.get_line_length(s, self.console.screen.encoding)
|
||||
if slen > cols:
|
||||
s = s.replace(f["path"], f["path"][slen - cols + 1:])
|
||||
self.console.write(s)
|
||||
|
||||
self.console.write(" {!info!}::Peers")
|
||||
|
||||
111
deluge/ui/console/commands/move.py
Normal file
@ -0,0 +1,111 @@
|
||||
#
|
||||
# move.py
|
||||
#
|
||||
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
# You may redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License, as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# deluge is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with deluge. If not, write to:
|
||||
# The Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# In addition, as a special exception, the copyright holders give
|
||||
# permission to link the code of portions of this program with the OpenSSL
|
||||
# library.
|
||||
# You must obey the GNU General Public License in all respects for all of
|
||||
# the code used other than OpenSSL. If you modify file(s) with this
|
||||
# exception, you may extend this exception to your version of the file(s),
|
||||
# but you are not obligated to do so. If you do not wish to do so, delete
|
||||
# this exception statement from your version. If you delete this exception
|
||||
# statement from all source files in the program, then also delete it here.
|
||||
#
|
||||
#
|
||||
|
||||
from deluge.ui.console.main import BaseCommand
|
||||
from deluge.ui.client import client
|
||||
import deluge.component as component
|
||||
|
||||
import os.path
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Move torrents' storage location"""
|
||||
usage = "Usage: move <torrent-id> [<torrent-id> ...] <path>"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.console = component.get("ConsoleUI")
|
||||
|
||||
if len(args) < 2:
|
||||
self.console.write(self.usage)
|
||||
return
|
||||
|
||||
path = args[-1]
|
||||
|
||||
if os.path.exists(path) and not os.path.isdir(path):
|
||||
self.console.write("{!error!}Cannot Move Storage: %s exists and is not a directory"%path)
|
||||
return
|
||||
|
||||
ids = []
|
||||
for i in args[:-1]:
|
||||
ids.extend(self.console.match_torrent(i))
|
||||
|
||||
names = []
|
||||
for i in ids:
|
||||
names.append(self.console.get_torrent_name(i))
|
||||
namestr = ", ".join(names)
|
||||
|
||||
def on_move(res):
|
||||
self.console.write("Moved \"%s\" to %s"%(namestr,path))
|
||||
|
||||
d = client.core.move_storage(ids,path)
|
||||
d.addCallback(on_move)
|
||||
return d
|
||||
|
||||
def complete(self, line):
|
||||
line = os.path.abspath(os.path.expanduser(line))
|
||||
ret = []
|
||||
if os.path.exists(line):
|
||||
# This is a correct path, check to see if it's a directory
|
||||
if os.path.isdir(line):
|
||||
# Directory, so we need to show contents of directory
|
||||
#ret.extend(os.listdir(line))
|
||||
for f in os.listdir(line):
|
||||
# Skip hidden
|
||||
if f.startswith("."):
|
||||
continue
|
||||
f = os.path.join(line, f)
|
||||
if os.path.isdir(f):
|
||||
f += "/"
|
||||
ret.append(f)
|
||||
else:
|
||||
# This is a file, but we could be looking for another file that
|
||||
# shares a common prefix.
|
||||
for f in os.listdir(os.path.dirname(line)):
|
||||
if f.startswith(os.path.split(line)[1]):
|
||||
ret.append(os.path.join( os.path.dirname(line), f))
|
||||
else:
|
||||
# This path does not exist, so lets do a listdir on it's parent
|
||||
# and find any matches.
|
||||
ret = []
|
||||
if os.path.isdir(os.path.dirname(line)):
|
||||
for f in os.listdir(os.path.dirname(line)):
|
||||
if f.startswith(os.path.split(line)[1]):
|
||||
p = os.path.join(os.path.dirname(line), f)
|
||||
|
||||
if os.path.isdir(p):
|
||||
p += "/"
|
||||
ret.append(p)
|
||||
|
||||
return ret
|
||||
@ -40,6 +40,7 @@ from twisted.internet import reactor
|
||||
class Command(BaseCommand):
|
||||
"""Exit from the client."""
|
||||
aliases = ['exit']
|
||||
interactive_only = True
|
||||
def handle(self, *args, **options):
|
||||
if client.connected():
|
||||
def on_disconnect(result):
|
||||
|
||||
126
deluge/ui/console/commands/status.py
Normal file
@ -0,0 +1,126 @@
|
||||
#
|
||||
# status.py
|
||||
#
|
||||
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
# You may redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License, as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# deluge is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with deluge. If not, write to:
|
||||
# The Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# In addition, as a special exception, the copyright holders give
|
||||
# permission to link the code of portions of this program with the OpenSSL
|
||||
# library.
|
||||
# You must obey the GNU General Public License in all respects for all of
|
||||
# the code used other than OpenSSL. If you modify file(s) with this
|
||||
# exception, you may extend this exception to your version of the file(s),
|
||||
# but you are not obligated to do so. If you do not wish to do so, delete
|
||||
# this exception statement from your version. If you delete this exception
|
||||
# statement from all source files in the program, then also delete it here.
|
||||
#
|
||||
|
||||
from optparse import make_option
|
||||
from twisted.internet import defer
|
||||
from deluge.ui.console.main import BaseCommand
|
||||
from deluge.ui.client import client
|
||||
import deluge.common
|
||||
import deluge.component as component
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Shows a various status information from the daemon."""
|
||||
option_list = BaseCommand.option_list + (
|
||||
make_option('-r', '--raw', action='store_true', default=False, dest='raw',
|
||||
help='Don\'t format upload/download rates in KiB/s (useful for scripts that want to do their own parsing)'),
|
||||
make_option('-n', '--no-torrents', action='store_false', default=True, dest='show_torrents',
|
||||
help='Don\'t show torrent status (this will make the command a bit faster)'),
|
||||
)
|
||||
|
||||
|
||||
usage = "Usage: status [-r] [-n]"
|
||||
def handle(self, *args, **options):
|
||||
self.console = component.get("ConsoleUI")
|
||||
self.status = None
|
||||
self.connections = None
|
||||
if options["show_torrents"]:
|
||||
self.torrents = None
|
||||
else:
|
||||
self.torrents = -2
|
||||
|
||||
self.raw = options["raw"]
|
||||
|
||||
def on_session_status(status):
|
||||
self.status = status
|
||||
if self.status != None and self.connections != None and self.torrents != None:
|
||||
self.print_status()
|
||||
|
||||
def on_num_connections(conns):
|
||||
self.connections = conns
|
||||
if self.status != None and self.connections != None and self.torrents != None:
|
||||
self.print_status()
|
||||
|
||||
def on_torrents_status(status):
|
||||
self.torrents = status
|
||||
if self.status != None and self.connections != None and self.torrents != None:
|
||||
self.print_status()
|
||||
|
||||
def on_torrents_status_fail(reason):
|
||||
self.torrents = -1
|
||||
if self.status != None and self.connections != None and self.torrents != None:
|
||||
self.print_status()
|
||||
|
||||
deferreds = []
|
||||
|
||||
ds = client.core.get_session_status(["payload_upload_rate","payload_download_rate","dht_nodes"])
|
||||
ds.addCallback(on_session_status)
|
||||
deferreds.append(ds)
|
||||
|
||||
dc = client.core.get_num_connections()
|
||||
dc.addCallback(on_num_connections)
|
||||
deferreds.append(dc)
|
||||
|
||||
if options["show_torrents"]:
|
||||
dt = client.core.get_torrents_status({}, ["state"])
|
||||
dt.addCallback(on_torrents_status)
|
||||
dt.addErrback(on_torrents_status_fail)
|
||||
deferreds.append(dt)
|
||||
|
||||
return defer.DeferredList(deferreds)
|
||||
|
||||
def print_status(self):
|
||||
self.console.set_batch_write(True)
|
||||
if self.raw:
|
||||
self.console.write("{!info!}Total upload: %f"%self.status["payload_upload_rate"])
|
||||
self.console.write("{!info!}Total download: %f"%self.status["payload_download_rate"])
|
||||
else:
|
||||
self.console.write("{!info!}Total upload: %s"%deluge.common.fspeed(self.status["payload_upload_rate"]))
|
||||
self.console.write("{!info!}Total download: %s"%deluge.common.fspeed(self.status["payload_download_rate"]))
|
||||
self.console.write("{!info!}DHT Nodes: %i"%self.status["dht_nodes"])
|
||||
self.console.write("{!info!}Total connections: %i"%self.connections)
|
||||
if self.torrents == -1:
|
||||
self.console.write("{!error!}Error getting torrent info")
|
||||
elif self.torrents != -2:
|
||||
self.console.write("{!info!}Total torrents: %i"%len(self.torrents))
|
||||
states = ["Downloading","Seeding","Paused","Checking","Error","Queued"]
|
||||
state_counts = {}
|
||||
for state in states:
|
||||
state_counts[state] = 0
|
||||
for t in self.torrents:
|
||||
s = self.torrents[t]
|
||||
state_counts[s["state"]] += 1
|
||||
for state in states:
|
||||
self.console.write("{!info!} %s: %i"%(state,state_counts[state]))
|
||||
|
||||
self.console.set_batch_write(False)
|
||||
65
deluge/ui/console/commands/update-tracker.py
Normal file
@ -0,0 +1,65 @@
|
||||
#
|
||||
# rm.py
|
||||
#
|
||||
# Copyright (C) 2008-2009 Ido Abramovich <ido.deluge@gmail.com>
|
||||
# Copyright (C) 2009 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.
|
||||
#
|
||||
#
|
||||
from deluge.ui.console.main import BaseCommand
|
||||
import deluge.ui.console.colors as colors
|
||||
from deluge.ui.client import client
|
||||
import deluge.component as component
|
||||
|
||||
from optparse import make_option
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Update tracker for torrent(s)"""
|
||||
usage = "Usage: update-tracker [ * | <torrent-id> [<torrent-id> ...] ]"
|
||||
aliases = ['reannounce']
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.console = component.get("ConsoleUI")
|
||||
if len(args) == 0:
|
||||
self.console.write(self.usage)
|
||||
return
|
||||
if len(args) > 0 and args[0].lower() == '*':
|
||||
args = [""]
|
||||
|
||||
torrent_ids = []
|
||||
for arg in args:
|
||||
torrent_ids.extend(self.console.match_torrent(arg))
|
||||
|
||||
client.core.force_reannounce(torrent_ids)
|
||||
|
||||
def complete(self, line):
|
||||
# We use the ConsoleUI torrent tab complete method
|
||||
return component.get("ConsoleUI").tab_complete_torrent(line)
|
||||
@ -62,53 +62,53 @@ class EventLog(component.Component):
|
||||
client.register_event_handler("PluginEnabledEvent", self.on_plugin_enabled_event)
|
||||
client.register_event_handler("PluginDisabledEvent", self.on_plugin_disabled_event)
|
||||
|
||||
def on_torrent_added_event(self, event):
|
||||
def on_torrent_added_event(self, torrent_id, from_state):
|
||||
def on_torrent_status(status):
|
||||
self.console.write(self.prefix + "TorrentAdded(from_state=%s): {!info!}%s (%s)" % (
|
||||
event.from_state, status["name"], event.torrent_id)
|
||||
from_state, status["name"], torrent_id)
|
||||
)
|
||||
client.core.get_torrent_status(event.torrent_id, ["name"]).addCallback(on_torrent_status)
|
||||
client.core.get_torrent_status(torrent_id, ["name"]).addCallback(on_torrent_status)
|
||||
|
||||
def on_torrent_removed_event(self, event):
|
||||
def on_torrent_removed_event(self, torrent_id):
|
||||
self.console.write(self.prefix + "TorrentRemoved: {!info!}%s (%s)" %
|
||||
(self.console.get_torrent_name(event.torrent_id), event.torrent_id))
|
||||
(self.console.get_torrent_name(torrent_id), torrent_id))
|
||||
|
||||
def on_torrent_state_changed_event(self, event):
|
||||
def on_torrent_state_changed_event(self, torrent_id, state):
|
||||
# Modify the state string color
|
||||
if event.state in colors.state_color:
|
||||
state = colors.state_color[event.state] + event.state
|
||||
if state in colors.state_color:
|
||||
state = colors.state_color[state] + state
|
||||
|
||||
self.console.write(self.prefix + "TorrentStateChanged: %s {!info!}%s (%s)" %
|
||||
(state, self.console.get_torrent_name(event.torrent_id), event.torrent_id))
|
||||
(state, self.console.get_torrent_name(torrent_id), torrent_id))
|
||||
|
||||
def on_torrent_paused_event(self, event):
|
||||
def on_torrent_paused_event(self, torrent_id):
|
||||
self.console.write(self.prefix + "TorrentPaused: {!info!}%s (%s)" %
|
||||
(self.console.get_torrent_name(event.torrent_id), event.torrent_id))
|
||||
(self.console.get_torrent_name(torrent_id), torrent_id))
|
||||
|
||||
def on_torrent_finished_event(self, event):
|
||||
def on_torrent_finished_event(self, torrent_id):
|
||||
self.console.write(self.prefix + "TorrentFinished: {!info!}%s (%s)" %
|
||||
(self.console.get_torrent_name(event.torrent_id), event.torrent_id))
|
||||
(self.console.get_torrent_name(torrent_id), torrent_id))
|
||||
|
||||
def on_new_version_available_event(self, event):
|
||||
def on_new_version_available_event(self, version):
|
||||
self.console.write(self.prefix + "NewVersionAvailable: {!info!}%s" %
|
||||
(event.new_release))
|
||||
(version))
|
||||
|
||||
def on_session_paused_event(self, event):
|
||||
def on_session_paused_event(self):
|
||||
self.console.write(self.prefix + "SessionPaused")
|
||||
|
||||
def on_session_resumed_event(self, event):
|
||||
def on_session_resumed_event(self):
|
||||
self.console.write(self.prefix + "SessionResumed")
|
||||
|
||||
def on_config_value_changed_event(self, event):
|
||||
def on_config_value_changed_event(self, key, value):
|
||||
color = "{!white,black,bold!}"
|
||||
if type(event.value) in colors.type_color:
|
||||
color = colors.type_color[type(event.value)]
|
||||
if type(value) in colors.type_color:
|
||||
color = colors.type_color[type(value)]
|
||||
|
||||
self.console.write(self.prefix + "ConfigValueChanged: {!input!}%s: %s%s" %
|
||||
(event.key, color, event.value))
|
||||
(key, color, value))
|
||||
|
||||
def on_plugin_enabled_event(self, event):
|
||||
self.console.write(self.prefix + "PluginEnabled: {!info!}%s" % event.plugin_name)
|
||||
def on_plugin_enabled_event(self, name):
|
||||
self.console.write(self.prefix + "PluginEnabled: {!info!}%s" % name)
|
||||
|
||||
def on_plugin_disabled_event(self, event):
|
||||
self.console.write(self.prefix + "PluginDisabled: {!info!}%s" % event.plugin_name)
|
||||
def on_plugin_disabled_event(self, name):
|
||||
self.console.write(self.prefix + "PluginDisabled: {!info!}%s" % name)
|
||||
|
||||
@ -43,16 +43,17 @@ import locale
|
||||
|
||||
from twisted.internet import defer, reactor
|
||||
|
||||
from deluge.ui.console import UI_PATH
|
||||
import deluge.component as component
|
||||
from deluge.ui.client import client
|
||||
import deluge.common
|
||||
from deluge.ui.coreconfig import CoreConfig
|
||||
from deluge.ui.sessionproxy import SessionProxy
|
||||
from deluge.ui.console.statusbars import StatusBars
|
||||
from deluge.ui.console.eventlog import EventLog
|
||||
import screen
|
||||
import colors
|
||||
from deluge.ui.ui import _UI
|
||||
from deluge.ui.console import UI_PATH
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -62,16 +63,62 @@ class Console(_UI):
|
||||
|
||||
def __init__(self):
|
||||
super(Console, self).__init__("console")
|
||||
cmds = load_commands(os.path.join(UI_PATH, 'commands'))
|
||||
group = optparse.OptionGroup(self.parser, "Console Options","These options control how "
|
||||
"the console connects to the daemon. These options will be "
|
||||
"used if you pass a command, or if you have autoconnect "
|
||||
"enabled for the console ui.")
|
||||
|
||||
group = optparse.OptionGroup(self.parser, "Console Commands",
|
||||
"\n".join(cmds.keys()))
|
||||
group.add_option("-d","--daemon",dest="daemon_addr",
|
||||
action="store",type="str",default="127.0.0.1",
|
||||
help="Set the address of the daemon to connect to."
|
||||
" [default: %default]")
|
||||
group.add_option("-p","--port",dest="daemon_port",
|
||||
help="Set the port to connect to the daemon on. [default: %default]",
|
||||
action="store",type="int",default=58846)
|
||||
group.add_option("-u","--username",dest="daemon_user",
|
||||
help="Set the username to connect to the daemon with. [default: %default]",
|
||||
action="store",type="string")
|
||||
group.add_option("-P","--password",dest="daemon_pass",
|
||||
help="Set the password to connect to the daemon with. [default: %default]",
|
||||
action="store",type="string")
|
||||
self.parser.add_option_group(group)
|
||||
|
||||
self.cmds = load_commands(os.path.join(UI_PATH, 'commands'))
|
||||
class CommandOptionGroup(optparse.OptionGroup):
|
||||
def __init__(self, parser, title, description=None, cmds = None):
|
||||
optparse.OptionGroup.__init__(self,parser,title,description)
|
||||
self.cmds = cmds
|
||||
|
||||
def format_help(self, formatter):
|
||||
result = formatter.format_heading(self.title)
|
||||
formatter.indent()
|
||||
if self.description:
|
||||
result += "%s\n"%formatter.format_description(self.description)
|
||||
for cname in self.cmds:
|
||||
cmd = self.cmds[cname]
|
||||
if cmd.interactive_only or cname in cmd.aliases: continue
|
||||
allnames = [cname]
|
||||
allnames.extend(cmd.aliases)
|
||||
cname = "/".join(allnames)
|
||||
result += formatter.format_heading(" - ".join([cname,cmd.__doc__]))
|
||||
formatter.indent()
|
||||
result += "%*s%s\n" % (formatter.current_indent, "", cmd.usage)
|
||||
formatter.dedent()
|
||||
formatter.dedent()
|
||||
return result
|
||||
cmd_group = CommandOptionGroup(self.parser, "Console Commands",
|
||||
description="The following commands can be issued at the "
|
||||
"command line. Commands should be quoted, so, for example, "
|
||||
"to pause torrent with id 'abc' you would run: '%s "
|
||||
"\"pause abc\"'"%os.path.basename(sys.argv[0]),
|
||||
cmds=self.cmds)
|
||||
self.parser.add_option_group(cmd_group)
|
||||
|
||||
def start(self):
|
||||
super(Console, self).start()
|
||||
|
||||
ConsoleUI(self.args)
|
||||
ConsoleUI(self.args,self.cmds,(self.options.daemon_addr,
|
||||
self.options.daemon_port,self.options.daemon_user,
|
||||
self.options.daemon_pass))
|
||||
|
||||
def start():
|
||||
Console().start()
|
||||
@ -92,9 +139,11 @@ class OptionParser(optparse.OptionParser):
|
||||
"""
|
||||
raise Exception(msg)
|
||||
|
||||
|
||||
class BaseCommand(object):
|
||||
|
||||
usage = 'usage'
|
||||
interactive_only = False
|
||||
option_list = tuple()
|
||||
aliases = []
|
||||
|
||||
@ -122,6 +171,7 @@ class BaseCommand(object):
|
||||
epilog = self.epilog,
|
||||
option_list = self.option_list)
|
||||
|
||||
|
||||
def load_commands(command_dir, exclude=[]):
|
||||
def get_command(name):
|
||||
return getattr(__import__('deluge.ui.console.commands.%s' % name, {}, {}, ['Command']), 'Command')()
|
||||
@ -142,11 +192,13 @@ def load_commands(command_dir, exclude=[]):
|
||||
except OSError, e:
|
||||
return {}
|
||||
|
||||
|
||||
class ConsoleUI(component.Component):
|
||||
def __init__(self, args=None):
|
||||
def __init__(self, args=None, cmds = None, daemon = None):
|
||||
component.Component.__init__(self, "ConsoleUI", 2)
|
||||
|
||||
self.batch_write = False
|
||||
# keep track of events for the log view
|
||||
self.events = []
|
||||
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, '')
|
||||
@ -155,40 +207,30 @@ class ConsoleUI(component.Component):
|
||||
self.encoding = sys.getdefaultencoding()
|
||||
|
||||
log.debug("Using encoding: %s", self.encoding)
|
||||
# Load all the commands
|
||||
self._commands = load_commands(os.path.join(UI_PATH, 'commands'))
|
||||
|
||||
|
||||
# start up the session proxy
|
||||
self.sessionproxy = SessionProxy()
|
||||
|
||||
client.set_disconnect_callback(self.on_client_disconnect)
|
||||
|
||||
# Set the interactive flag to indicate where we should print the output
|
||||
self.interactive = True
|
||||
self._commands = cmds
|
||||
if args:
|
||||
args = args[0]
|
||||
self.interactive = False
|
||||
|
||||
# Try to connect to the localhost daemon
|
||||
def on_connect(result):
|
||||
def on_started(result):
|
||||
if not self.interactive:
|
||||
def on_started(result):
|
||||
deferreds = []
|
||||
# If we have args, lets process them and quit
|
||||
# allow multiple commands split by ";"
|
||||
for arg in args.split(";"):
|
||||
deferreds.append(defer.maybeDeferred(self.do_command, arg.strip()))
|
||||
|
||||
def on_complete(result):
|
||||
self.do_command("quit")
|
||||
|
||||
dl = defer.DeferredList(deferreds).addCallback(on_complete)
|
||||
|
||||
# We need to wait for the rpcs in start() to finish before processing
|
||||
# any of the commands.
|
||||
self.started_deferred.addCallback(on_started)
|
||||
component.start().addCallback(on_started)
|
||||
|
||||
d = client.connect()
|
||||
d.addCallback(on_connect)
|
||||
if not cmds:
|
||||
print "Sorry, couldn't find any commands"
|
||||
return
|
||||
else:
|
||||
from commander import Commander
|
||||
cmdr = Commander(cmds)
|
||||
if daemon:
|
||||
cmdr.exec_args(args,*daemon)
|
||||
else:
|
||||
cmdr.exec_args(args,None,None,None,None)
|
||||
|
||||
|
||||
self.coreconfig = CoreConfig()
|
||||
if self.interactive and not deluge.common.windows_check():
|
||||
@ -197,8 +239,13 @@ class ConsoleUI(component.Component):
|
||||
import curses.wrapper
|
||||
curses.wrapper(self.run)
|
||||
elif self.interactive and deluge.common.windows_check():
|
||||
print "You cannot run the deluge-console in interactive mode in Windows.\
|
||||
Please use commands from the command line, eg: deluge-console config;help;exit"
|
||||
print """\nDeluge-console does not run in interactive mode on Windows. \n
|
||||
Please use commands from the command line, eg:\n
|
||||
deluge-console.exe help
|
||||
deluge-console.exe info
|
||||
deluge-console.exe "add --help"
|
||||
deluge-console.exe "add -p c:\\mytorrents c:\\new.torrent"
|
||||
"""
|
||||
else:
|
||||
reactor.run()
|
||||
|
||||
@ -213,8 +260,10 @@ class ConsoleUI(component.Component):
|
||||
# We want to do an interactive session, so start up the curses screen and
|
||||
# pass it the function that handles commands
|
||||
colors.init_colors()
|
||||
self.screen = screen.Screen(stdscr, self.do_command, self.tab_completer, self.encoding)
|
||||
self.statusbars = StatusBars()
|
||||
from modes.connectionmanager import ConnectionManager
|
||||
self.stdscr = stdscr
|
||||
self.screen = ConnectionManager(stdscr, self.encoding)
|
||||
self.eventlog = EventLog()
|
||||
|
||||
self.screen.topbar = "{!status!}Deluge " + deluge.common.get_version() + " Console"
|
||||
@ -228,202 +277,22 @@ class ConsoleUI(component.Component):
|
||||
# Start the twisted mainloop
|
||||
reactor.run()
|
||||
|
||||
def start(self):
|
||||
# This gets fired once we have received the torrents list from the core
|
||||
self.started_deferred = defer.Deferred()
|
||||
|
||||
def start(self):
|
||||
# Maintain a list of (torrent_id, name) for use in tab completion
|
||||
self.torrents = []
|
||||
def on_session_state(result):
|
||||
def on_torrents_status(torrents):
|
||||
for torrent_id, status in torrents.items():
|
||||
self.torrents.append((torrent_id, status["name"]))
|
||||
self.started_deferred.callback(True)
|
||||
if not self.interactive:
|
||||
self.started_deferred = defer.Deferred()
|
||||
def on_session_state(result):
|
||||
def on_torrents_status(torrents):
|
||||
for torrent_id, status in torrents.items():
|
||||
self.torrents.append((torrent_id, status["name"]))
|
||||
self.started_deferred.callback(True)
|
||||
|
||||
client.core.get_torrents_status({"id": result}, ["name"]).addCallback(on_torrents_status)
|
||||
client.core.get_session_state().addCallback(on_session_state)
|
||||
|
||||
# Register some event handlers to keep the torrent list up-to-date
|
||||
client.register_event_handler("TorrentAddedEvent", self.on_torrent_added_event)
|
||||
client.register_event_handler("TorrentRemovedEvent", self.on_torrent_removed_event)
|
||||
|
||||
def update(self):
|
||||
pass
|
||||
|
||||
def set_batch_write(self, batch):
|
||||
"""
|
||||
When this is set the screen is not refreshed after a `:meth:write` until
|
||||
this is set to False.
|
||||
|
||||
:param batch: set True to prevent screen refreshes after a `:meth:write`
|
||||
:type batch: bool
|
||||
|
||||
"""
|
||||
self.batch_write = batch
|
||||
if not batch and self.interactive:
|
||||
self.screen.refresh()
|
||||
|
||||
def write(self, line):
|
||||
"""
|
||||
Writes a line out depending on if we're in interactive mode or not.
|
||||
|
||||
:param line: str, the line to print
|
||||
|
||||
"""
|
||||
if self.interactive:
|
||||
self.screen.add_line(line, not self.batch_write)
|
||||
else:
|
||||
print(colors.strip_colors(line))
|
||||
|
||||
def do_command(self, cmd):
|
||||
"""
|
||||
Processes a command.
|
||||
|
||||
:param cmd: str, the command string
|
||||
|
||||
"""
|
||||
if not cmd:
|
||||
return
|
||||
cmd, _, line = cmd.partition(' ')
|
||||
try:
|
||||
parser = self._commands[cmd].create_parser()
|
||||
except KeyError:
|
||||
self.write("{!error!}Unknown command: %s" % cmd)
|
||||
return
|
||||
args = self._commands[cmd].split(line)
|
||||
|
||||
# Do a little hack here to print 'command --help' properly
|
||||
parser._print_help = parser.print_help
|
||||
def print_help(f=None):
|
||||
if self.interactive:
|
||||
self.write(parser.format_help())
|
||||
else:
|
||||
parser._print_help(f)
|
||||
parser.print_help = print_help
|
||||
|
||||
# Only these commands can be run when not connected to a daemon
|
||||
not_connected_cmds = ["help", "connect", "quit"]
|
||||
aliases = []
|
||||
for c in not_connected_cmds:
|
||||
aliases.extend(self._commands[c].aliases)
|
||||
not_connected_cmds.extend(aliases)
|
||||
|
||||
if not client.connected() and cmd not in not_connected_cmds:
|
||||
self.write("{!error!}Not connected to a daemon, please use the connect command first.")
|
||||
return
|
||||
|
||||
try:
|
||||
options, args = parser.parse_args(args)
|
||||
except Exception, e:
|
||||
self.write("{!error!}Error parsing options: %s" % e)
|
||||
return
|
||||
|
||||
if not getattr(options, '_exit', False):
|
||||
try:
|
||||
ret = self._commands[cmd].handle(*args, **options.__dict__)
|
||||
except Exception, e:
|
||||
self.write("{!error!}" + str(e))
|
||||
log.exception(e)
|
||||
import traceback
|
||||
self.write("%s" % traceback.format_exc())
|
||||
return defer.succeed(True)
|
||||
else:
|
||||
return ret
|
||||
|
||||
def tab_completer(self, line, cursor, second_hit):
|
||||
"""
|
||||
Called when the user hits 'tab' and will autocomplete or show options.
|
||||
If a command is already supplied in the line, this function will call the
|
||||
complete method of the command.
|
||||
|
||||
:param line: str, the current input string
|
||||
:param cursor: int, the cursor position in the line
|
||||
:param second_hit: bool, if this is the second time in a row the tab key
|
||||
has been pressed
|
||||
|
||||
:returns: 2-tuple (string, cursor position)
|
||||
|
||||
"""
|
||||
# First check to see if there is no space, this will mean that it's a
|
||||
# command that needs to be completed.
|
||||
if " " not in line:
|
||||
possible_matches = []
|
||||
# Iterate through the commands looking for ones that startwith the
|
||||
# line.
|
||||
for cmd in self._commands:
|
||||
if cmd.startswith(line):
|
||||
possible_matches.append(cmd + " ")
|
||||
|
||||
line_prefix = ""
|
||||
else:
|
||||
cmd = line.split(" ")[0]
|
||||
if cmd in self._commands:
|
||||
# Call the command's complete method to get 'er done
|
||||
possible_matches = self._commands[cmd].complete(line.split(" ")[-1])
|
||||
line_prefix = " ".join(line.split(" ")[:-1]) + " "
|
||||
else:
|
||||
# This is a bogus command
|
||||
return (line, cursor)
|
||||
|
||||
# No matches, so just return what we got passed
|
||||
if len(possible_matches) == 0:
|
||||
return (line, cursor)
|
||||
# If we only have 1 possible match, then just modify the line and
|
||||
# return it, else we need to print out the matches without modifying
|
||||
# the line.
|
||||
elif len(possible_matches) == 1:
|
||||
new_line = line_prefix + possible_matches[0]
|
||||
return (new_line, len(new_line))
|
||||
else:
|
||||
if second_hit:
|
||||
# Only print these out if it's a second_hit
|
||||
self.write(" ")
|
||||
for match in possible_matches:
|
||||
self.write(match)
|
||||
else:
|
||||
p = " ".join(line.split(" ")[:-1])
|
||||
new_line = " ".join([p, os.path.commonprefix(possible_matches)])
|
||||
if len(new_line) > len(line):
|
||||
line = new_line
|
||||
cursor = len(line)
|
||||
return (line, cursor)
|
||||
|
||||
def tab_complete_torrent(self, line):
|
||||
"""
|
||||
Completes torrent_ids or names.
|
||||
|
||||
:param line: str, the string to complete
|
||||
|
||||
:returns: list of matches
|
||||
|
||||
"""
|
||||
|
||||
possible_matches = []
|
||||
|
||||
# Find all possible matches
|
||||
for torrent_id, torrent_name in self.torrents:
|
||||
if torrent_id.startswith(line):
|
||||
possible_matches.append(torrent_id + " ")
|
||||
if torrent_name.startswith(line):
|
||||
possible_matches.append(torrent_name + " ")
|
||||
|
||||
return possible_matches
|
||||
|
||||
def get_torrent_name(self, torrent_id):
|
||||
"""
|
||||
Gets a torrent name from the torrents list.
|
||||
|
||||
:param torrent_id: str, the torrent_id
|
||||
|
||||
:returns: the name of the torrent or None
|
||||
"""
|
||||
|
||||
for tid, name in self.torrents:
|
||||
if torrent_id == tid:
|
||||
return name
|
||||
|
||||
return None
|
||||
client.core.get_torrents_status({"id": result}, ["name"]).addCallback(on_torrents_status)
|
||||
client.core.get_session_state().addCallback(on_session_state)
|
||||
|
||||
|
||||
def match_torrent(self, string):
|
||||
"""
|
||||
Returns a list of torrent_id matches for the string. It will search both
|
||||
@ -435,6 +304,8 @@ class ConsoleUI(component.Component):
|
||||
no matches are found.
|
||||
|
||||
"""
|
||||
if self.interactive and isinstance(self.screen,deluge.ui.console.modes.legacy.Legacy):
|
||||
return self.screen.match_torrent(string)
|
||||
ret = []
|
||||
for tid, name in self.torrents:
|
||||
if tid.startswith(string) or name.startswith(string):
|
||||
@ -442,15 +313,40 @@ class ConsoleUI(component.Component):
|
||||
|
||||
return ret
|
||||
|
||||
def on_torrent_added_event(self, event):
|
||||
def on_torrent_status(status):
|
||||
self.torrents.append((event.torrent_id, status["name"]))
|
||||
client.core.get_torrent_status(event.torrent_id, ["name"]).addCallback(on_torrent_status)
|
||||
|
||||
def on_torrent_removed_event(self, event):
|
||||
for index, (tid, name) in enumerate(self.torrents):
|
||||
if event.torrent_id == tid:
|
||||
del self.torrents[index]
|
||||
def get_torrent_name(self, torrent_id):
|
||||
if self.interactive and hasattr(self.screen,"get_torrent_name"):
|
||||
return self.screen.get_torrent_name(torrent_id)
|
||||
|
||||
for tid, name in self.torrents:
|
||||
if torrent_id == tid:
|
||||
return name
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def set_batch_write(self, batch):
|
||||
if self.interactive and isinstance(self.screen,deluge.ui.console.modes.legacy.Legacy):
|
||||
return self.screen.set_batch_write(batch)
|
||||
|
||||
def tab_complete_torrent(self, line):
|
||||
if self.interactive and isinstance(self.screen,deluge.ui.console.modes.legacy.Legacy):
|
||||
return self.screen.tab_complete_torrent(line)
|
||||
|
||||
def set_mode(self, mode):
|
||||
reactor.removeReader(self.screen)
|
||||
self.screen = mode
|
||||
self.statusbars.screen = self.screen
|
||||
reactor.addReader(self.screen)
|
||||
|
||||
def on_client_disconnect(self):
|
||||
component.stop()
|
||||
|
||||
def write(self, s):
|
||||
if self.interactive:
|
||||
if isinstance(self.screen,deluge.ui.console.modes.legacy.Legacy):
|
||||
self.screen.write(s)
|
||||
else:
|
||||
self.events.append(s)
|
||||
else:
|
||||
print colors.strip_colors(s.encode(self.encoding))
|
||||
|
||||
0
deluge/ui/console/modes/__init__.py
Normal file
106
deluge/ui/console/modes/add_util.py
Normal file
@ -0,0 +1,106 @@
|
||||
#
|
||||
# add_util.py
|
||||
#
|
||||
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||
#
|
||||
# Modified function from commands/add.py:
|
||||
# Copyright (C) 2008-2009 Ido Abramovich <ido.deluge@gmail.com>
|
||||
# Copyright (C) 2009 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.
|
||||
#
|
||||
#
|
||||
from deluge.ui.client import client
|
||||
import deluge.component as component
|
||||
from deluge.ui.common import TorrentInfo
|
||||
import deluge.common
|
||||
|
||||
import os,base64,glob
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def __bracket_fixup(path):
|
||||
if (path.find("[") == -1 and
|
||||
path.find("]") == -1):
|
||||
return path
|
||||
sentinal = 256
|
||||
while (path.find(unichr(sentinal)) != -1):
|
||||
sentinal+=1
|
||||
if sentinal > 65535:
|
||||
log.error("Can't fix brackets in path, path contains all possible sentinal characters")
|
||||
return path
|
||||
newpath = path.replace("]",unichr(sentinal))
|
||||
newpath = newpath.replace("[","[[]")
|
||||
newpath = newpath.replace(unichr(sentinal),"[]]")
|
||||
return newpath
|
||||
|
||||
def add_torrent(t_file, options, success_cb, fail_cb, ress):
|
||||
t_options = {}
|
||||
if options["path"]:
|
||||
t_options["download_location"] = os.path.expanduser(options["path"])
|
||||
t_options["add_paused"] = options["add_paused"]
|
||||
|
||||
is_url = (not (options["path_type"]==1)) and (deluge.common.is_url(t_file) or options["path_type"]==2)
|
||||
is_mag = not(is_url) and (not (options["path_type"]==1)) and deluge.common.is_magnet(t_file)
|
||||
|
||||
if is_url or is_mag:
|
||||
files = [t_file]
|
||||
else:
|
||||
files = glob.glob(__bracket_fixup(t_file))
|
||||
num_files = len(files)
|
||||
ress["total"] = num_files
|
||||
|
||||
if num_files <= 0:
|
||||
fail_cb("Doesn't exist",t_file,ress)
|
||||
|
||||
for f in files:
|
||||
if is_url:
|
||||
client.core.add_torrent_url(f, t_options).addCallback(success_cb,f,ress).addErrback(fail_cb,f,ress)
|
||||
elif is_mag:
|
||||
client.core.add_torrent_magnet(f, t_options).addCallback(success_cb,f,ress).addErrback(fail_cb,f,ress)
|
||||
else:
|
||||
if not os.path.exists(f):
|
||||
fail_cb("Doesn't exist",f,ress)
|
||||
continue
|
||||
if not os.path.isfile(f):
|
||||
fail_cb("Is a directory",f,ress)
|
||||
continue
|
||||
|
||||
try:
|
||||
TorrentInfo(f)
|
||||
except Exception as e:
|
||||
fail_cb(e.message,f,ress)
|
||||
continue
|
||||
|
||||
filename = os.path.split(f)[-1]
|
||||
filedump = base64.encodestring(open(f).read())
|
||||
|
||||
client.core.add_torrent_file(filename, filedump, t_options).addCallback(success_cb,f,ress).addErrback(fail_cb,f,ress)
|
||||
|
||||
861
deluge/ui/console/modes/alltorrents.py
Normal file
@ -0,0 +1,861 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# alltorrents.py
|
||||
#
|
||||
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||
#
|
||||
# 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 deluge.component as component
|
||||
from basemode import BaseMode
|
||||
import deluge.common
|
||||
from deluge.ui.client import client
|
||||
from deluge.configmanager import ConfigManager
|
||||
|
||||
from collections import deque
|
||||
|
||||
from deluge.ui.sessionproxy import SessionProxy
|
||||
|
||||
from popup import Popup,SelectablePopup,MessagePopup
|
||||
from add_util import add_torrent
|
||||
from input_popup import InputPopup
|
||||
from torrentdetail import TorrentDetail
|
||||
from preferences import Preferences
|
||||
from torrent_actions import torrent_actions_popup
|
||||
from eventview import EventView
|
||||
from legacy import Legacy
|
||||
|
||||
import format_utils,column
|
||||
|
||||
try:
|
||||
import curses
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Big help string that gets displayed when the user hits 'h'
|
||||
HELP_STR = """\
|
||||
This screen shows an overview of the current torrents Deluge is managing. \
|
||||
The currently selected torrent is indicated by having a white background. \
|
||||
You can change the selected torrent using the up/down arrows or the \
|
||||
PgUp/Pg keys. Home and End keys go to the first and last torrent \
|
||||
respectively.
|
||||
|
||||
Operations can be performed on multiple torrents by marking them and \
|
||||
then hitting Enter. See below for the keys used to mark torrents.
|
||||
|
||||
You can scroll a popup window that doesn't fit its content (like \
|
||||
this one) using the up/down arrows.
|
||||
|
||||
All popup windows can be closed/canceled by hitting the Esc key \
|
||||
(you might need to wait a second for an Esc to register)
|
||||
|
||||
The actions you can perform and the keys to perform them are as follows:
|
||||
|
||||
{!info!}'h'{!normal!} - Show this help
|
||||
|
||||
{!info!}'a'{!normal!} - Add a torrent
|
||||
|
||||
{!info!}'p'{!normal!} - View/Set preferences
|
||||
|
||||
{!info!}'/'{!normal!} - Search torrent names. Enter to exectue search, ESC to cancel
|
||||
|
||||
{!info!}'n'{!normal!} - Next matching torrent for last search
|
||||
|
||||
{!info!}'f'{!normal!} - Show only torrents in a certain state
|
||||
(Will open a popup where you can select the state you want to see)
|
||||
|
||||
{!info!}'i'{!normal!} - Show more detailed information about the current selected torrent
|
||||
|
||||
{!info!}'e'{!normal!} - Show the event log view ({!info!}'q'{!normal!} to get out of event log)
|
||||
|
||||
{!info!}'l'{!normal!} - Go into 'legacy' mode (the way deluge-console used to work)
|
||||
|
||||
{!info!}'Q'{!normal!} - quit
|
||||
|
||||
{!info!}'m'{!normal!} - Mark a torrent
|
||||
{!info!}'M'{!normal!} - Mark all torrents between currently selected torrent and last marked torrent
|
||||
{!info!}'c'{!normal!} - Un-mark all torrents
|
||||
|
||||
{!info!}Right Arrow{!normal!} - Torrent Detail Mode. This includes more detailed information \
|
||||
about the currently selected torrent, as well as a view of the \
|
||||
files in the torrent and the ability to set file priorities.
|
||||
|
||||
{!info!}Enter{!normal!} - Show torrent actions popup. Here you can do things like \
|
||||
pause/resume, remove, recheck and so on. These actions \
|
||||
apply to all currently marked torrents. The currently \
|
||||
selected torrent is automatically marked when you press enter.
|
||||
|
||||
{!info!}'q'/Esc{!normal!} - Close a popup (Note that Esc can take a moment to register \
|
||||
as having been pressed.
|
||||
"""
|
||||
|
||||
class FILTER:
|
||||
ALL=0
|
||||
ACTIVE=1
|
||||
DOWNLOADING=2
|
||||
SEEDING=3
|
||||
PAUSED=4
|
||||
CHECKING=5
|
||||
ERROR=6
|
||||
QUEUED=7
|
||||
|
||||
DEFAULT_PREFS = {
|
||||
"show_queue":True,
|
||||
"show_name":True,
|
||||
"show_size":True,
|
||||
"show_state":True,
|
||||
"show_progress":True,
|
||||
"show_seeders":True,
|
||||
"show_peers":True,
|
||||
"show_downspeed":True,
|
||||
"show_upspeed":True,
|
||||
"show_eta":False,
|
||||
"show_ratio":False,
|
||||
"show_avail":False,
|
||||
"show_added":False,
|
||||
"show_tracker":False,
|
||||
"show_savepath":False,
|
||||
"show_downloaded":False,
|
||||
"show_uploaded":False,
|
||||
"show_owner":False,
|
||||
"queue_width":5,
|
||||
"name_width":-1,
|
||||
"size_width":15,
|
||||
"state_width":13,
|
||||
"progress_width":10,
|
||||
"seeders_width":10,
|
||||
"peers_width":10,
|
||||
"downspeed_width":15,
|
||||
"upspeed_width":15,
|
||||
"eta_width":10,
|
||||
"ratio_width":10,
|
||||
"avail_width":10,
|
||||
"added_width":25,
|
||||
"tracker_width":15,
|
||||
"savepath_width":15,
|
||||
"downloaded_width":13,
|
||||
"uploaded_width":13,
|
||||
"owner_width":10,
|
||||
}
|
||||
|
||||
column_pref_names = ["queue","name","size","state",
|
||||
"progress","seeders","peers",
|
||||
"downspeed","upspeed","eta",
|
||||
"ratio","avail","added","tracker",
|
||||
"savepath","downloaded","uploaded",
|
||||
"owner"]
|
||||
|
||||
prefs_to_names = {
|
||||
"queue":"#",
|
||||
"name":"Name",
|
||||
"size":"Size",
|
||||
"state":"State",
|
||||
"progress":"Progress",
|
||||
"seeders":"Seeders",
|
||||
"peers":"Peers",
|
||||
"downspeed":"Down Speed",
|
||||
"upspeed":"Up Speed",
|
||||
"eta":"ETA",
|
||||
"ratio":"Ratio",
|
||||
"avail":"Avail",
|
||||
"added":"Added",
|
||||
"tracker":"Tracker",
|
||||
"savepath":"Save Path",
|
||||
"downloaded":"Downloaded",
|
||||
"uploaded":"Uploaded",
|
||||
"owner":"Owner",
|
||||
}
|
||||
|
||||
class AllTorrents(BaseMode, component.Component):
|
||||
def __init__(self, stdscr, encoding=None):
|
||||
self.formatted_rows = None
|
||||
self.torrent_names = None
|
||||
self.cursel = 1
|
||||
self.curoff = 1 # TODO: this should really be 0 indexed
|
||||
self.column_string = ""
|
||||
self.popup = None
|
||||
self.messages = deque()
|
||||
self.marked = []
|
||||
self.last_mark = -1
|
||||
self._sorted_ids = None
|
||||
self._go_top = False
|
||||
|
||||
self._curr_filter = None
|
||||
self.entering_search = False
|
||||
self.search_string = None
|
||||
self.cursor = 0
|
||||
|
||||
self.coreconfig = component.get("ConsoleUI").coreconfig
|
||||
|
||||
self.legacy_mode = None
|
||||
|
||||
self.__status_dict = {}
|
||||
self.__torrent_info_id = None
|
||||
|
||||
BaseMode.__init__(self, stdscr, encoding)
|
||||
component.Component.__init__(self, "AllTorrents", 1, depend=["SessionProxy"])
|
||||
curses.curs_set(0)
|
||||
self.stdscr.notimeout(0)
|
||||
|
||||
self.__split_help()
|
||||
self.update_config()
|
||||
|
||||
component.start(["AllTorrents"])
|
||||
|
||||
self._info_fields = [
|
||||
("Name",None,("name",)),
|
||||
("State", None, ("state",)),
|
||||
("Down Speed", format_utils.format_speed, ("download_payload_rate",)),
|
||||
("Up Speed", format_utils.format_speed, ("upload_payload_rate",)),
|
||||
("Progress", format_utils.format_progress, ("progress",)),
|
||||
("ETA", deluge.common.ftime, ("eta",)),
|
||||
("Path", None, ("save_path",)),
|
||||
("Downloaded",deluge.common.fsize,("all_time_download",)),
|
||||
("Uploaded", deluge.common.fsize,("total_uploaded",)),
|
||||
("Share Ratio", format_utils.format_float, ("ratio",)),
|
||||
("Seeders",format_utils.format_seeds_peers,("num_seeds","total_seeds")),
|
||||
("Peers",format_utils.format_seeds_peers,("num_peers","total_peers")),
|
||||
("Active Time",deluge.common.ftime,("active_time",)),
|
||||
("Seeding Time",deluge.common.ftime,("seeding_time",)),
|
||||
("Date Added",deluge.common.fdate,("time_added",)),
|
||||
("Availability", format_utils.format_float, ("distributed_copies",)),
|
||||
("Pieces", format_utils.format_pieces, ("num_pieces","piece_length")),
|
||||
]
|
||||
|
||||
self.__status_keys = ["name","state","download_payload_rate","upload_payload_rate",
|
||||
"progress","eta","all_time_download","total_uploaded", "ratio",
|
||||
"num_seeds","total_seeds","num_peers","total_peers", "active_time",
|
||||
"seeding_time","time_added","distributed_copies", "num_pieces",
|
||||
"piece_length","save_path"]
|
||||
|
||||
# component start/update
|
||||
def start(self):
|
||||
component.get("SessionProxy").get_torrents_status(self.__status_dict, self.__status_fields).addCallback(self.set_state,False)
|
||||
|
||||
def update(self):
|
||||
component.get("SessionProxy").get_torrents_status(self.__status_dict, self.__status_fields).addCallback(self.set_state,True)
|
||||
if self.__torrent_info_id:
|
||||
component.get("SessionProxy").get_torrent_status(self.__torrent_info_id, self.__status_keys).addCallback(self._on_torrent_status)
|
||||
|
||||
def update_config(self):
|
||||
self.config = ConfigManager("console.conf",DEFAULT_PREFS)
|
||||
self.__cols_to_show = [pref for pref in column_pref_names if self.config["show_%s"%pref]]
|
||||
self.__columns = [prefs_to_names[col] for col in self.__cols_to_show]
|
||||
self.__status_fields = column.get_required_fields(self.__columns)
|
||||
for rf in ["state","name","queue"]: # we always need these, even if we're not displaying them
|
||||
if not rf in self.__status_fields: self.__status_fields.append(rf)
|
||||
self.__update_columns()
|
||||
|
||||
def __split_help(self):
|
||||
self.__help_lines = format_utils.wrap_string(HELP_STR,(self.cols/2)-2)
|
||||
|
||||
def resume(self):
|
||||
self._go_top = True
|
||||
component.start(["AllTorrents"])
|
||||
self.refresh()
|
||||
|
||||
def __update_columns(self):
|
||||
self.column_widths = [self.config["%s_width"%c] for c in self.__cols_to_show]
|
||||
req = sum(filter(lambda x:x >= 0,self.column_widths))
|
||||
if (req > self.cols): # can't satisfy requests, just spread out evenly
|
||||
cw = int(self.cols/len(self.__columns))
|
||||
for i in range(0,len(self.column_widths)):
|
||||
self.column_widths[i] = cw
|
||||
else:
|
||||
rem = self.cols - req
|
||||
var_cols = len(filter(lambda x: x < 0,self.column_widths))
|
||||
if (var_cols > 0):
|
||||
vw = int(rem/var_cols)
|
||||
for i in range(0, len(self.column_widths)):
|
||||
if (self.column_widths[i] < 0):
|
||||
self.column_widths[i] = vw
|
||||
|
||||
self.column_string = "{!header!}%s"%("".join(["%s%s"%(self.__columns[i]," "*(self.column_widths[i]-len(self.__columns[i]))) for i in range(0,len(self.__columns))]))
|
||||
|
||||
|
||||
def set_state(self, state, refresh):
|
||||
self.curstate = state # cache in case we change sort order
|
||||
newnames = []
|
||||
newrows = []
|
||||
self._sorted_ids = self._sort_torrents(self.curstate)
|
||||
for torrent_id in self._sorted_ids:
|
||||
ts = self.curstate[torrent_id]
|
||||
newnames.append(ts["name"])
|
||||
newrows.append((format_utils.format_row([column.get_column_value(name,ts) for name in self.__columns],self.column_widths),ts["state"]))
|
||||
|
||||
self.numtorrents = len(state)
|
||||
self.formatted_rows = newrows
|
||||
self.torrent_names = newnames
|
||||
if refresh:
|
||||
self.refresh()
|
||||
|
||||
def get_torrent_name(self, torrent_id):
|
||||
for p,i in enumerate(self._sorted_ids):
|
||||
if torrent_id == i:
|
||||
return self.torrent_names[p]
|
||||
return None
|
||||
|
||||
def _scroll_up(self, by):
|
||||
prevoff = self.curoff
|
||||
self.cursel = max(self.cursel - by,1)
|
||||
if ((self.cursel - 1) < self.curoff):
|
||||
self.curoff = max(self.cursel - 1,1)
|
||||
return prevoff != self.curoff
|
||||
|
||||
def _scroll_down(self, by):
|
||||
prevoff = self.curoff
|
||||
self.cursel = min(self.cursel + by,self.numtorrents)
|
||||
if ((self.curoff + self.rows - 5) < self.cursel):
|
||||
self.curoff = self.cursel - self.rows + 5
|
||||
return prevoff != self.curoff
|
||||
|
||||
def current_torrent_id(self):
|
||||
if self._sorted_ids:
|
||||
return self._sorted_ids[self.cursel-1]
|
||||
else:
|
||||
return None
|
||||
|
||||
def _selected_torrent_ids(self):
|
||||
ret = []
|
||||
for i in self.marked:
|
||||
ret.append(self._sorted_ids[i-1])
|
||||
return ret
|
||||
|
||||
def _on_torrent_status(self, state):
|
||||
if (self.popup):
|
||||
self.popup.clear()
|
||||
name = state["name"]
|
||||
off = int((self.cols/4)-(len(name)/2))
|
||||
self.popup.set_title(name)
|
||||
for i,f in enumerate(self._info_fields):
|
||||
if f[1] != None:
|
||||
args = []
|
||||
try:
|
||||
for key in f[2]:
|
||||
args.append(state[key])
|
||||
except:
|
||||
log.debug("Could not get info field: %s",e)
|
||||
continue
|
||||
info = f[1](*args)
|
||||
else:
|
||||
info = state[f[2][0]]
|
||||
|
||||
nl = len(f[0])+4
|
||||
if (nl+len(info))>self.popup.width:
|
||||
self.popup.add_line("{!info!}%s: {!input!}%s"%(f[0],info[:(self.popup.width - nl)]))
|
||||
info = info[(self.popup.width - nl):]
|
||||
n = self.popup.width-3
|
||||
chunks = [info[i:i+n] for i in xrange(0, len(info), n)]
|
||||
for c in chunks:
|
||||
self.popup.add_line(" %s"%c)
|
||||
else:
|
||||
self.popup.add_line("{!info!}%s: {!input!}%s"%(f[0],info))
|
||||
self.refresh()
|
||||
else:
|
||||
self.__torrent_info_id = None
|
||||
|
||||
|
||||
def on_resize(self, *args):
|
||||
BaseMode.on_resize_norefresh(self, *args)
|
||||
self.__update_columns()
|
||||
self.__split_help()
|
||||
if self.popup:
|
||||
self.popup.handle_resize()
|
||||
self.refresh()
|
||||
|
||||
def _queue_sort(self, v1, v2):
|
||||
if v1 == v2:
|
||||
return 0
|
||||
if v2 < 0:
|
||||
return -1
|
||||
if v1 < 0:
|
||||
return 1
|
||||
if v1 > v2:
|
||||
return 1
|
||||
if v2 > v1:
|
||||
return -1
|
||||
|
||||
def _sort_torrents(self, state):
|
||||
"sorts by queue #"
|
||||
return sorted(state,cmp=self._queue_sort,key=lambda s:state.get(s)["queue"])
|
||||
|
||||
def _format_queue(self, qnum):
|
||||
if (qnum >= 0):
|
||||
return "%d"%(qnum+1)
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
def show_torrent_details(self,tid):
|
||||
def dodeets(arg):
|
||||
if arg and True in arg[0]:
|
||||
self.stdscr.clear()
|
||||
component.get("ConsoleUI").set_mode(TorrentDetail(self,tid,self.stdscr,self.encoding))
|
||||
else:
|
||||
self.messages.append(("Error","An error occured trying to display torrent details"))
|
||||
component.stop(["AllTorrents"]).addCallback(dodeets)
|
||||
|
||||
def show_preferences(self):
|
||||
def _on_get_config(config):
|
||||
client.core.get_listen_port().addCallback(_on_get_listen_port,config)
|
||||
|
||||
def _on_get_listen_port(port,config):
|
||||
client.core.get_cache_status().addCallback(_on_get_cache_status,port,config)
|
||||
|
||||
def _on_get_cache_status(status,port,config):
|
||||
def doprefs(arg):
|
||||
if arg and True in arg[0]:
|
||||
self.stdscr.clear()
|
||||
component.get("ConsoleUI").set_mode(Preferences(self,config,self.config,port,status,self.stdscr,self.encoding))
|
||||
else:
|
||||
self.messages.append(("Error","An error occured trying to display preferences"))
|
||||
component.stop(["AllTorrents"]).addCallback(doprefs)
|
||||
|
||||
client.core.get_config().addCallback(_on_get_config)
|
||||
|
||||
|
||||
def __show_events(self):
|
||||
def doevents(arg):
|
||||
if arg and True in arg[0]:
|
||||
self.stdscr.clear()
|
||||
component.get("ConsoleUI").set_mode(EventView(self,self.stdscr,self.encoding))
|
||||
else:
|
||||
self.messages.append(("Error","An error occured trying to display events"))
|
||||
component.stop(["AllTorrents"]).addCallback(doevents)
|
||||
|
||||
def __legacy_mode(self):
|
||||
def dolegacy(arg):
|
||||
if arg and True in arg[0]:
|
||||
self.stdscr.clear()
|
||||
if not self.legacy_mode:
|
||||
self.legacy_mode = Legacy(self.stdscr,self.encoding)
|
||||
component.get("ConsoleUI").set_mode(self.legacy_mode)
|
||||
self.legacy_mode.refresh()
|
||||
curses.curs_set(2)
|
||||
else:
|
||||
self.messages.append(("Error","An error occured trying to switch to legacy mode"))
|
||||
component.stop(["AllTorrents"]).addCallback(dolegacy)
|
||||
|
||||
def _torrent_filter(self, idx, data):
|
||||
if data==FILTER.ALL:
|
||||
self.__status_dict = {}
|
||||
self._curr_filter = None
|
||||
elif data==FILTER.ACTIVE:
|
||||
self.__status_dict = {"state":"Active"}
|
||||
self._curr_filter = "Active"
|
||||
elif data==FILTER.DOWNLOADING:
|
||||
self.__status_dict = {"state":"Downloading"}
|
||||
self._curr_filter = "Downloading"
|
||||
elif data==FILTER.SEEDING:
|
||||
self.__status_dict = {"state":"Seeding"}
|
||||
self._curr_filter = "Seeding"
|
||||
elif data==FILTER.PAUSED:
|
||||
self.__status_dict = {"state":"Paused"}
|
||||
self._curr_filter = "Paused"
|
||||
elif data==FILTER.CHECKING:
|
||||
self.__status_dict = {"state":"Checking"}
|
||||
self._curr_filter = "Checking"
|
||||
elif data==FILTER.ERROR:
|
||||
self.__status_dict = {"state":"Error"}
|
||||
self._curr_filter = "Error"
|
||||
elif data==FILTER.QUEUED:
|
||||
self.__status_dict = {"state":"Queued"}
|
||||
self._curr_filter = "Queued"
|
||||
self._go_top = True
|
||||
return True
|
||||
|
||||
def _show_torrent_filter_popup(self):
|
||||
self.popup = SelectablePopup(self,"Filter Torrents",self._torrent_filter)
|
||||
self.popup.add_line("_All",data=FILTER.ALL)
|
||||
self.popup.add_line("Ac_tive",data=FILTER.ACTIVE)
|
||||
self.popup.add_line("_Downloading",data=FILTER.DOWNLOADING,foreground="green")
|
||||
self.popup.add_line("_Seeding",data=FILTER.SEEDING,foreground="cyan")
|
||||
self.popup.add_line("_Paused",data=FILTER.PAUSED)
|
||||
self.popup.add_line("_Error",data=FILTER.ERROR,foreground="red")
|
||||
self.popup.add_line("_Checking",data=FILTER.CHECKING,foreground="blue")
|
||||
self.popup.add_line("Q_ueued",data=FILTER.QUEUED,foreground="yellow")
|
||||
|
||||
def __report_add_status(self, succ_cnt, fail_cnt, fail_msgs):
|
||||
if fail_cnt == 0:
|
||||
self.report_message("Torrents Added","{!success!}Sucessfully added %d torrent(s)"%succ_cnt)
|
||||
else:
|
||||
msg = ("{!error!}Failed to add the following %d torrent(s):\n {!error!}"%fail_cnt)+"\n {!error!}".join(fail_msgs)
|
||||
if succ_cnt != 0:
|
||||
msg += "\n \n{!success!}Sucessfully added %d torrent(s)"%succ_cnt
|
||||
self.report_message("Torrent Add Report",msg)
|
||||
|
||||
def _do_add(self, result):
|
||||
log.debug("Adding Torrent(s): %s (dl path: %s) (paused: %d)",result["file"],result["path"],result["add_paused"])
|
||||
ress = {"succ":0,
|
||||
"fail":0,
|
||||
"fmsg":[]}
|
||||
|
||||
def fail_cb(msg,t_file,ress):
|
||||
log.debug("failed to add torrent: %s: %s"%(t_file,msg))
|
||||
ress["fail"]+=1
|
||||
ress["fmsg"].append("%s: %s"%(t_file,msg))
|
||||
if (ress["succ"]+ress["fail"]) >= ress["total"]:
|
||||
self.__report_add_status(ress["succ"],ress["fail"],ress["fmsg"])
|
||||
def suc_cb(tid,t_file,ress):
|
||||
if tid:
|
||||
log.debug("added torrent: %s (%s)"%(t_file,tid))
|
||||
ress["succ"]+=1
|
||||
if (ress["succ"]+ress["fail"]) >= ress["total"]:
|
||||
self.__report_add_status(ress["succ"],ress["fail"],ress["fmsg"])
|
||||
else:
|
||||
fail_cb("Already in session (probably)",t_file,ress)
|
||||
|
||||
add_torrent(result["file"],result,suc_cb,fail_cb,ress)
|
||||
|
||||
def _show_torrent_add_popup(self):
|
||||
dl = ""
|
||||
ap = 1
|
||||
try:
|
||||
dl = self.coreconfig["download_location"]
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
if self.coreconfig["add_paused"]:
|
||||
ap = 0
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
self.popup = InputPopup(self,"Add Torrent (Esc to cancel)",close_cb=self._do_add)
|
||||
self.popup.add_text_input("Enter path to torrent file:","file")
|
||||
self.popup.add_text_input("Enter save path:","path",dl)
|
||||
self.popup.add_select_input("Add Paused:","add_paused",["Yes","No"],[True,False],ap)
|
||||
self.popup.add_spaces(1)
|
||||
self.popup.add_select_input("Path is:","path_type",["Auto","File","URL"],[0,1,2],0)
|
||||
|
||||
def report_message(self,title,message):
|
||||
self.messages.append((title,message))
|
||||
|
||||
def clear_marks(self):
|
||||
self.marked = []
|
||||
self.last_mark = -1
|
||||
|
||||
def set_popup(self,pu):
|
||||
self.popup = pu
|
||||
self.refresh()
|
||||
|
||||
def refresh(self,lines=None):
|
||||
#log.error("ref")
|
||||
#import traceback
|
||||
#traceback.print_stack()
|
||||
# Something has requested we scroll to the top of the list
|
||||
if self._go_top:
|
||||
self.cursel = 1
|
||||
self.curoff = 1
|
||||
self._go_top = False
|
||||
|
||||
# show a message popup if there's anything queued
|
||||
if self.popup == None and self.messages:
|
||||
title,msg = self.messages.popleft()
|
||||
self.popup = MessagePopup(self,title,msg)
|
||||
|
||||
if not lines:
|
||||
self.stdscr.clear()
|
||||
|
||||
# Update the status bars
|
||||
if self._curr_filter == None:
|
||||
self.add_string(0,self.statusbars.topbar)
|
||||
else:
|
||||
self.add_string(0,"%s {!filterstatus!}Current filter: %s"%(self.statusbars.topbar,self._curr_filter))
|
||||
self.add_string(1,self.column_string)
|
||||
|
||||
if self.entering_search:
|
||||
self.add_string(self.rows - 1,"{!black,white!}Search torrents: %s"%self.search_string)
|
||||
else:
|
||||
hstr = "%sPress [h] for help"%(" "*(self.cols - len(self.statusbars.bottombar) - 10))
|
||||
self.add_string(self.rows - 1, "%s%s"%(self.statusbars.bottombar,hstr))
|
||||
|
||||
# add all the torrents
|
||||
if self.formatted_rows == []:
|
||||
msg = "No torrents match filter".center(self.cols)
|
||||
self.add_string(3, "{!info!}%s"%msg)
|
||||
elif self.formatted_rows:
|
||||
tidx = self.curoff
|
||||
currow = 2
|
||||
|
||||
if lines:
|
||||
todraw = []
|
||||
for l in lines:
|
||||
todraw.append(self.formatted_rows[l])
|
||||
lines.reverse()
|
||||
else:
|
||||
todraw = self.formatted_rows[tidx-1:]
|
||||
|
||||
for row in todraw:
|
||||
# default style
|
||||
fg = "white"
|
||||
bg = "black"
|
||||
attr = None
|
||||
if lines:
|
||||
tidx = lines.pop()+1
|
||||
currow = tidx-self.curoff+2
|
||||
|
||||
if tidx in self.marked:
|
||||
bg = "blue"
|
||||
attr = "bold"
|
||||
|
||||
if tidx == self.cursel:
|
||||
bg = "white"
|
||||
attr = "bold"
|
||||
if tidx in self.marked:
|
||||
fg = "blue"
|
||||
else:
|
||||
fg = "black"
|
||||
|
||||
if row[1] == "Downloading":
|
||||
fg = "green"
|
||||
elif row[1] == "Seeding":
|
||||
fg = "cyan"
|
||||
elif row[1] == "Error":
|
||||
fg = "red"
|
||||
elif row[1] == "Queued":
|
||||
fg = "yellow"
|
||||
elif row[1] == "Checking":
|
||||
fg = "blue"
|
||||
|
||||
if attr:
|
||||
colorstr = "{!%s,%s,%s!}"%(fg,bg,attr)
|
||||
else:
|
||||
colorstr = "{!%s,%s!}"%(fg,bg)
|
||||
|
||||
self.add_string(currow,"%s%s"%(colorstr,row[0]),trim=False)
|
||||
tidx += 1
|
||||
currow += 1
|
||||
if (currow > (self.rows - 2)):
|
||||
break
|
||||
else:
|
||||
self.add_string(1, "Waiting for torrents from core...")
|
||||
|
||||
#self.stdscr.redrawwin()
|
||||
if self.entering_search:
|
||||
curses.curs_set(2)
|
||||
self.stdscr.move(self.rows-1,self.cursor+17)
|
||||
else:
|
||||
curses.curs_set(0)
|
||||
|
||||
self.stdscr.noutrefresh()
|
||||
|
||||
if self.popup:
|
||||
self.popup.refresh()
|
||||
|
||||
curses.doupdate()
|
||||
|
||||
|
||||
def _mark_unmark(self,idx):
|
||||
if idx in self.marked:
|
||||
self.marked.remove(idx)
|
||||
self.last_mark = -1
|
||||
else:
|
||||
self.marked.append(idx)
|
||||
self.last_mark = idx
|
||||
|
||||
def __do_search(self):
|
||||
# search forward for the next torrent matching self.search_string
|
||||
for i,n in enumerate(self.torrent_names[self.cursel:]):
|
||||
if n.find(self.search_string) >= 0:
|
||||
self.cursel += (i+1)
|
||||
if ((self.curoff + self.rows - 5) < self.cursel):
|
||||
self.curoff = self.cursel - self.rows + 5
|
||||
return
|
||||
|
||||
def __update_search(self, c):
|
||||
if c == curses.KEY_BACKSPACE or c == 127:
|
||||
if self.search_string and self.cursor > 0:
|
||||
self.search_string = self.search_string[:self.cursor - 1] + self.search_string[self.cursor:]
|
||||
self.cursor-=1
|
||||
elif c == curses.KEY_DC:
|
||||
if self.search_string and self.cursor < len(self.search_string):
|
||||
self.search_string = self.search_string[:self.cursor] + self.search_string[self.cursor+1:]
|
||||
elif c == curses.KEY_LEFT:
|
||||
self.cursor = max(0,self.cursor-1)
|
||||
elif c == curses.KEY_RIGHT:
|
||||
self.cursor = min(len(self.search_string),self.cursor+1)
|
||||
elif c == curses.KEY_HOME:
|
||||
self.cursor = 0
|
||||
elif c == curses.KEY_END:
|
||||
self.cursor = len(self.search_string)
|
||||
elif c == 27:
|
||||
self.search_string = None
|
||||
self.entering_search = False
|
||||
elif c == 10 or c == curses.KEY_ENTER:
|
||||
self.entering_search = False
|
||||
if self.search_string:
|
||||
self.__do_search()
|
||||
else:
|
||||
self.search_string = None
|
||||
elif c > 31 and c < 256:
|
||||
stroke = chr(c)
|
||||
uchar = ""
|
||||
while not uchar:
|
||||
try:
|
||||
uchar = stroke.decode(self.encoding)
|
||||
except UnicodeDecodeError:
|
||||
c = self.stdscr.getch()
|
||||
stroke += chr(c)
|
||||
if uchar:
|
||||
if self.cursor == len(self.search_string):
|
||||
self.search_string += uchar
|
||||
else:
|
||||
# Insert into string
|
||||
self.search_string = self.search_string[:self.cursor] + uchar + self.search_string[self.cursor:]
|
||||
# Move the cursor forward
|
||||
self.cursor+=1
|
||||
|
||||
def _doRead(self):
|
||||
# Read the character
|
||||
effected_lines = None
|
||||
|
||||
c = self.stdscr.getch()
|
||||
|
||||
if self.popup:
|
||||
if self.popup.handle_read(c):
|
||||
self.popup = None
|
||||
self.refresh()
|
||||
return
|
||||
|
||||
if c > 31 and c < 256:
|
||||
if chr(c) == 'Q':
|
||||
from twisted.internet import reactor
|
||||
if client.connected():
|
||||
def on_disconnect(result):
|
||||
reactor.stop()
|
||||
client.disconnect().addCallback(on_disconnect)
|
||||
else:
|
||||
reactor.stop()
|
||||
return
|
||||
|
||||
if self.formatted_rows==None or self.popup:
|
||||
return
|
||||
|
||||
elif self.entering_search:
|
||||
self.__update_search(c)
|
||||
self.refresh([])
|
||||
return
|
||||
|
||||
#log.error("pressed key: %d\n",c)
|
||||
#if c == 27: # handle escape
|
||||
# log.error("CANCEL")
|
||||
|
||||
# Navigate the torrent list
|
||||
if c == curses.KEY_UP:
|
||||
if self.cursel == 1: return
|
||||
if not self._scroll_up(1):
|
||||
effected_lines = [self.cursel-1,self.cursel]
|
||||
elif c == curses.KEY_PPAGE:
|
||||
self._scroll_up(int(self.rows/2))
|
||||
elif c == curses.KEY_DOWN:
|
||||
if self.cursel >= self.numtorrents: return
|
||||
if not self._scroll_down(1):
|
||||
effected_lines = [self.cursel-2,self.cursel-1]
|
||||
elif c == curses.KEY_NPAGE:
|
||||
self._scroll_down(int(self.rows/2))
|
||||
elif c == curses.KEY_HOME:
|
||||
self._scroll_up(self.cursel)
|
||||
elif c == curses.KEY_END:
|
||||
self._scroll_down(self.numtorrents-self.cursel)
|
||||
|
||||
elif c == curses.KEY_RIGHT:
|
||||
# We enter a new mode for the selected torrent here
|
||||
tid = self.current_torrent_id()
|
||||
if tid:
|
||||
self.show_torrent_details(tid)
|
||||
return
|
||||
|
||||
# Enter Key
|
||||
elif (c == curses.KEY_ENTER or c == 10) and self.numtorrents:
|
||||
if self.cursel not in self.marked:
|
||||
self.marked.append(self.cursel)
|
||||
self.last_mark = self.cursel
|
||||
torrent_actions_popup(self,self._selected_torrent_ids(),details=True)
|
||||
return
|
||||
else:
|
||||
if c > 31 and c < 256:
|
||||
if chr(c) == '/':
|
||||
self.search_string = ""
|
||||
self.cursor = 0
|
||||
self.entering_search = True
|
||||
elif chr(c) == 'n' and self.search_string:
|
||||
self.__do_search()
|
||||
elif chr(c) == 'j':
|
||||
if not self._scroll_up(1):
|
||||
effected_lines = [self.cursel-1,self.cursel]
|
||||
elif chr(c) == 'k':
|
||||
if not self._scroll_down(1):
|
||||
effected_lines = [self.cursel-2,self.cursel-1]
|
||||
elif chr(c) == 'i':
|
||||
cid = self.current_torrent_id()
|
||||
if cid:
|
||||
def cb(): self.__torrent_info_id = None
|
||||
self.popup = Popup(self,"Info",close_cb=cb)
|
||||
self.popup.add_line("Getting torrent info...")
|
||||
self.__torrent_info_id = cid
|
||||
elif chr(c) == 'm':
|
||||
self._mark_unmark(self.cursel)
|
||||
effected_lines = [self.cursel-1]
|
||||
elif chr(c) == 'M':
|
||||
if self.last_mark >= 0:
|
||||
if (self.cursel+1) > self.last_mark:
|
||||
mrange = range(self.last_mark,self.cursel+1)
|
||||
else:
|
||||
mrange = range(self.cursel-1,self.last_mark)
|
||||
self.marked.extend(mrange[1:])
|
||||
effected_lines = mrange
|
||||
else:
|
||||
self._mark_unmark(self.cursel)
|
||||
effected_lines = [self.cursel-1]
|
||||
elif chr(c) == 'c':
|
||||
self.marked = []
|
||||
self.last_mark = -1
|
||||
elif chr(c) == 'a':
|
||||
self._show_torrent_add_popup()
|
||||
elif chr(c) == 'f':
|
||||
self._show_torrent_filter_popup()
|
||||
elif chr(c) == 'h':
|
||||
self.popup = Popup(self,"Help",init_lines=self.__help_lines)
|
||||
elif chr(c) == 'p':
|
||||
self.show_preferences()
|
||||
return
|
||||
elif chr(c) == 'e':
|
||||
self.__show_events()
|
||||
return
|
||||
elif chr(c) == 'l':
|
||||
self.__legacy_mode()
|
||||
return
|
||||
|
||||
self.refresh(effected_lines)
|
||||
240
deluge/ui/console/modes/basemode.py
Normal file
@ -0,0 +1,240 @@
|
||||
#
|
||||
# basemode.py
|
||||
#
|
||||
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||
#
|
||||
# Most code in this file taken from screen.py:
|
||||
# Copyright (C) 2009 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 sys
|
||||
import logging
|
||||
try:
|
||||
import curses
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import deluge.component as component
|
||||
import deluge.ui.console.colors as colors
|
||||
try:
|
||||
import signal
|
||||
from fcntl import ioctl
|
||||
import termios
|
||||
import struct
|
||||
except:
|
||||
pass
|
||||
|
||||
from twisted.internet import reactor
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class CursesStdIO(object):
|
||||
"""fake fd to be registered as a reader with the twisted reactor.
|
||||
Curses classes needing input should extend this"""
|
||||
|
||||
def fileno(self):
|
||||
""" We want to select on FD 0 """
|
||||
return 0
|
||||
|
||||
def doRead(self):
|
||||
"""called when input is ready"""
|
||||
pass
|
||||
def logPrefix(self): return 'CursesClient'
|
||||
|
||||
|
||||
class BaseMode(CursesStdIO):
|
||||
def __init__(self, stdscr, encoding=None, do_refresh=True):
|
||||
"""
|
||||
A mode that provides a curses screen designed to run as a reader in a twisted reactor.
|
||||
This mode doesn't do much, just shows status bars and "Base Mode" on the screen
|
||||
|
||||
Modes should subclass this and provide overrides for:
|
||||
|
||||
_doRead(self) - Handle user input
|
||||
refresh(self) - draw the mode to the screen
|
||||
add_string(self, row, string) - add a string of text to be displayed.
|
||||
see method for detailed info
|
||||
|
||||
The init method of a subclass *must* call BaseMode.__init__
|
||||
|
||||
Useful fields after calling BaseMode.__init__:
|
||||
self.stdscr - the curses screen
|
||||
self.rows - # of rows on the curses screen
|
||||
self.cols - # of cols on the curses screen
|
||||
self.topbar - top statusbar
|
||||
self.bottombar - bottom statusbar
|
||||
"""
|
||||
log.debug("BaseMode init!")
|
||||
self.stdscr = stdscr
|
||||
# Make the input calls non-blocking
|
||||
self.stdscr.nodelay(1)
|
||||
|
||||
# Strings for the 2 status bars
|
||||
self.statusbars = component.get("StatusBars")
|
||||
|
||||
# Keep track of the screen size
|
||||
self.rows, self.cols = self.stdscr.getmaxyx()
|
||||
try:
|
||||
signal.signal(signal.SIGWINCH, self.on_resize)
|
||||
except Exception, e:
|
||||
log.debug("Unable to catch SIGWINCH signal!")
|
||||
|
||||
if not encoding:
|
||||
self.encoding = sys.getdefaultencoding()
|
||||
else:
|
||||
self.encoding = encoding
|
||||
|
||||
colors.init_colors()
|
||||
|
||||
# Do a refresh right away to draw the screen
|
||||
if do_refresh:
|
||||
self.refresh()
|
||||
|
||||
def on_resize_norefresh(self, *args):
|
||||
log.debug("on_resize_from_signal")
|
||||
# Get the new rows and cols value
|
||||
self.rows, self.cols = struct.unpack("hhhh", ioctl(0, termios.TIOCGWINSZ ,"\000"*8))[0:2]
|
||||
curses.resizeterm(self.rows, self.cols)
|
||||
|
||||
def on_resize(self, *args):
|
||||
self.on_resize_norefresh(args)
|
||||
self.refresh()
|
||||
|
||||
def connectionLost(self, reason):
|
||||
self.close()
|
||||
|
||||
def add_string(self, row, string, scr=None, col = 0, pad=True, trim=True):
|
||||
"""
|
||||
Adds a string to the desired `:param:row`.
|
||||
|
||||
:param row: int, the row number to write the string
|
||||
:param string: string, the string of text to add
|
||||
:param scr: curses.window, optional window to add string to instead of self.stdscr
|
||||
:param col: int, optional starting column offset
|
||||
:param pad: bool, optional bool if the string should be padded out to the width of the screen
|
||||
:param trim: bool, optional bool if the string should be trimmed if it is too wide for the screen
|
||||
|
||||
The text can be formatted with color using the following format:
|
||||
|
||||
"{!fg, bg, attributes, ...!}"
|
||||
|
||||
See: http://docs.python.org/library/curses.html#constants for attributes.
|
||||
|
||||
Alternatively, it can use some built-in scheme for coloring.
|
||||
See colors.py for built-in schemes.
|
||||
|
||||
"{!scheme!}"
|
||||
|
||||
Examples:
|
||||
|
||||
"{!blue, black, bold!}My Text is {!white, black!}cool"
|
||||
"{!info!}I am some info text!"
|
||||
"{!error!}Uh oh!"
|
||||
|
||||
|
||||
"""
|
||||
if scr:
|
||||
screen = scr
|
||||
else:
|
||||
screen = self.stdscr
|
||||
try:
|
||||
parsed = colors.parse_color_string(string, self.encoding)
|
||||
except colors.BadColorString, e:
|
||||
log.error("Cannot add bad color string %s: %s", string, e)
|
||||
return
|
||||
|
||||
for index, (color, s) in enumerate(parsed):
|
||||
if index + 1 == len(parsed) and pad:
|
||||
# This is the last string so lets append some " " to it
|
||||
s += " " * (self.cols - (col + len(s)) - 1)
|
||||
if trim:
|
||||
y,x = screen.getmaxyx()
|
||||
if (col+len(s)) > x:
|
||||
s = "%s..."%s[0:x-4-col]
|
||||
screen.addstr(row, col, s, color)
|
||||
col += len(s)
|
||||
|
||||
def draw_statusbars(self):
|
||||
self.add_string(0, self.statusbars.topbar)
|
||||
self.add_string(self.rows - 1, self.statusbars.bottombar)
|
||||
|
||||
# This mode doesn't report errors
|
||||
def report_message(self):
|
||||
pass
|
||||
|
||||
# This mode doesn't do anything with popups
|
||||
def set_popup(self,popup):
|
||||
pass
|
||||
|
||||
# This mode doesn't support marking
|
||||
def clear_marks(self):
|
||||
pass
|
||||
|
||||
def refresh(self):
|
||||
"""
|
||||
Refreshes the screen.
|
||||
Updates the lines based on the`:attr:lines` based on the `:attr:display_lines_offset`
|
||||
attribute and the status bars.
|
||||
"""
|
||||
self.stdscr.clear()
|
||||
self.draw_statusbars()
|
||||
# Update the status bars
|
||||
|
||||
self.add_string(1,"{!info!}Base Mode (or subclass hasn't overridden refresh)")
|
||||
|
||||
self.stdscr.redrawwin()
|
||||
self.stdscr.refresh()
|
||||
|
||||
def doRead(self):
|
||||
"""
|
||||
Called when there is data to be read, ie, input from the keyboard.
|
||||
"""
|
||||
# We wrap this function to catch exceptions and shutdown the mainloop
|
||||
try:
|
||||
self._doRead()
|
||||
except Exception, e:
|
||||
log.exception(e)
|
||||
reactor.stop()
|
||||
|
||||
def _doRead(self):
|
||||
# Read the character
|
||||
c = self.stdscr.getch()
|
||||
self.stdscr.refresh()
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Clean up the curses stuff on exit.
|
||||
"""
|
||||
curses.nocbreak()
|
||||
self.stdscr.keypad(0)
|
||||
curses.echo()
|
||||
curses.endwin()
|
||||
99
deluge/ui/console/modes/column.py
Normal file
@ -0,0 +1,99 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# column.py
|
||||
#
|
||||
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||
#
|
||||
# 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 deluge.common
|
||||
import format_utils
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def format_queue(qnum):
|
||||
if (qnum >= 0):
|
||||
return "%d"%(qnum+1)
|
||||
else:
|
||||
return ""
|
||||
|
||||
columns = {
|
||||
"#":(("queue",),format_queue),
|
||||
"Name":(("name",),None),
|
||||
"Size":(("total_wanted",),deluge.common.fsize),
|
||||
"State":(("state",),None),
|
||||
"Progress":(("progress",),format_utils.format_progress),
|
||||
"Seeders":(("num_seeds","total_seeds"),format_utils.format_seeds_peers),
|
||||
"Peers":(("num_peers","total_peers"),format_utils.format_seeds_peers),
|
||||
"Down Speed":(("download_payload_rate",),format_utils.format_speed),
|
||||
"Up Speed":(("upload_payload_rate",),format_utils.format_speed),
|
||||
"ETA":(("eta",), format_utils.format_time),
|
||||
"Ratio":(("ratio",), format_utils.format_float),
|
||||
"Avail":(("distributed_copies",), format_utils.format_float),
|
||||
"Added":(("time_added",), deluge.common.fdate),
|
||||
"Tracker":(("tracker_host",), None),
|
||||
"Save Path":(("save_path",), None),
|
||||
"Downloaded":(("all_time_download",), deluge.common.fsize),
|
||||
"Uploaded":(("total_uploaded",), deluge.common.fsize),
|
||||
"Owner":(("owner",),None)
|
||||
}
|
||||
|
||||
def get_column_value(name,state):
|
||||
try:
|
||||
col = columns[name]
|
||||
except KeyError:
|
||||
log.error("No such column: %s",name)
|
||||
return None
|
||||
|
||||
if col[1] != None:
|
||||
args = []
|
||||
try:
|
||||
for key in col[0]:
|
||||
args.append(state[key])
|
||||
except:
|
||||
log.error("Could not get column field: %s",col[0])
|
||||
return None
|
||||
colval = col[1](*args)
|
||||
else:
|
||||
colval = state[col[0][0]]
|
||||
return colval
|
||||
|
||||
|
||||
def get_required_fields(cols):
|
||||
fields = []
|
||||
for col in cols:
|
||||
fields.extend(columns.get(col)[0])
|
||||
return fields
|
||||
|
||||
|
||||
|
||||
219
deluge/ui/console/modes/connectionmanager.py
Normal file
@ -0,0 +1,219 @@
|
||||
#
|
||||
# connectionmanager.py
|
||||
#
|
||||
# Copyright (C) 2007-2009 Nick Lanham <nick@afternight.org>
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
#
|
||||
|
||||
# a mode that show's a popup to select which host to connect to
|
||||
|
||||
import hashlib,time
|
||||
|
||||
from collections import deque
|
||||
|
||||
import deluge.ui.client
|
||||
from deluge.ui.client import client
|
||||
from deluge.configmanager import ConfigManager
|
||||
from deluge.ui.coreconfig import CoreConfig
|
||||
import deluge.component as component
|
||||
|
||||
from alltorrents import AllTorrents
|
||||
from basemode import BaseMode
|
||||
from popup import SelectablePopup,MessagePopup
|
||||
from input_popup import InputPopup
|
||||
|
||||
|
||||
try:
|
||||
import curses
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_HOST = "127.0.0.1"
|
||||
DEFAULT_PORT = 58846
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"hosts": [(hashlib.sha1(str(time.time())).hexdigest(), DEFAULT_HOST, DEFAULT_PORT, "", "")]
|
||||
}
|
||||
|
||||
|
||||
class ConnectionManager(BaseMode):
|
||||
def __init__(self, stdscr, encoding=None):
|
||||
self.popup = None
|
||||
self.statuses = {}
|
||||
self.messages = deque()
|
||||
self.config = ConfigManager("hostlist.conf.1.2", DEFAULT_CONFIG)
|
||||
BaseMode.__init__(self, stdscr, encoding)
|
||||
self.__update_statuses()
|
||||
self.__update_popup()
|
||||
|
||||
def __update_popup(self):
|
||||
self.popup = SelectablePopup(self,"Select Host",self.__host_selected)
|
||||
self.popup.add_line("{!white,black,bold!}'Q'=quit, 'r'=refresh, 'a'=add new host, 'D'=delete host",selectable=False)
|
||||
for host in self.config["hosts"]:
|
||||
if host[0] in self.statuses:
|
||||
self.popup.add_line("%s:%d [Online] (%s)"%(host[1],host[2],self.statuses[host[0]]),data=host[0],foreground="green")
|
||||
else:
|
||||
self.popup.add_line("%s:%d [Offline]"%(host[1],host[2]),data=host[0],foreground="red")
|
||||
self.inlist = True
|
||||
self.refresh()
|
||||
|
||||
def __update_statuses(self):
|
||||
"""Updates the host status"""
|
||||
def on_connect(result, c, host_id):
|
||||
def on_info(info, c):
|
||||
self.statuses[host_id] = info
|
||||
self.__update_popup()
|
||||
c.disconnect()
|
||||
|
||||
def on_info_fail(reason, c):
|
||||
if host_id in self.statuses:
|
||||
del self.statuses[host_id]
|
||||
c.disconnect()
|
||||
|
||||
d = c.daemon.info()
|
||||
d.addCallback(on_info, c)
|
||||
d.addErrback(on_info_fail, c)
|
||||
|
||||
def on_connect_failed(reason, host_id):
|
||||
if host_id in self.statuses:
|
||||
del self.statuses[host_id]
|
||||
|
||||
for host in self.config["hosts"]:
|
||||
c = deluge.ui.client.Client()
|
||||
hadr = host[1]
|
||||
port = host[2]
|
||||
user = host[3]
|
||||
password = host[4]
|
||||
d = c.connect(hadr, port, user, password)
|
||||
d.addCallback(on_connect, c, host[0])
|
||||
d.addErrback(on_connect_failed, host[0])
|
||||
|
||||
def __on_connected(self,result):
|
||||
component.start()
|
||||
self.stdscr.clear()
|
||||
at = AllTorrents(self.stdscr, self.encoding)
|
||||
component.get("ConsoleUI").set_mode(at)
|
||||
at.resume()
|
||||
|
||||
def __host_selected(self, idx, data):
|
||||
for host in self.config["hosts"]:
|
||||
if host[0] == data and host[0] in self.statuses:
|
||||
client.connect(host[1], host[2], host[3], host[4]).addCallback(self.__on_connected)
|
||||
return False
|
||||
|
||||
def __do_add(self,result):
|
||||
hostname = result["hostname"]
|
||||
try:
|
||||
port = int(result["port"])
|
||||
except ValueError:
|
||||
self.report_message("Can't add host","Invalid port. Must be an integer")
|
||||
return
|
||||
username = result["username"]
|
||||
password = result["password"]
|
||||
for host in self.config["hosts"]:
|
||||
if (host[1],host[2],host[3]) == (hostname, port, username):
|
||||
self.report_message("Can't add host","Host already in list")
|
||||
return
|
||||
newid = hashlib.sha1(str(time.time())).hexdigest()
|
||||
self.config["hosts"].append((newid, hostname, port, username, password))
|
||||
self.config.save()
|
||||
self.__update_popup()
|
||||
|
||||
def __add_popup(self):
|
||||
self.inlist = False
|
||||
self.popup = InputPopup(self,"Add Host (esc to cancel)",close_cb=self.__do_add)
|
||||
self.popup.add_text_input("Hostname:","hostname")
|
||||
self.popup.add_text_input("Port:","port")
|
||||
self.popup.add_text_input("Username:","username")
|
||||
self.popup.add_text_input("Password:","password")
|
||||
self.refresh()
|
||||
|
||||
def __delete_current_host(self):
|
||||
idx,data = self.popup.current_selection()
|
||||
log.debug("deleting host: %s",data)
|
||||
for host in self.config["hosts"]:
|
||||
if host[0] == data:
|
||||
self.config["hosts"].remove(host)
|
||||
break
|
||||
self.config.save()
|
||||
|
||||
def report_message(self,title,message):
|
||||
self.messages.append((title,message))
|
||||
|
||||
def refresh(self):
|
||||
self.stdscr.clear()
|
||||
self.draw_statusbars()
|
||||
self.stdscr.noutrefresh()
|
||||
|
||||
if self.popup == None and self.messages:
|
||||
title,msg = self.messages.popleft()
|
||||
self.popup = MessagePopup(self,title,msg)
|
||||
|
||||
if not self.popup:
|
||||
self.__update_popup()
|
||||
|
||||
self.popup.refresh()
|
||||
curses.doupdate()
|
||||
|
||||
|
||||
def _doRead(self):
|
||||
# Read the character
|
||||
c = self.stdscr.getch()
|
||||
|
||||
if c > 31 and c < 256:
|
||||
if chr(c) == 'q' and self.inlist: return
|
||||
if chr(c) == 'Q':
|
||||
from twisted.internet import reactor
|
||||
if client.connected():
|
||||
def on_disconnect(result):
|
||||
reactor.stop()
|
||||
client.disconnect().addCallback(on_disconnect)
|
||||
else:
|
||||
reactor.stop()
|
||||
return
|
||||
if chr(c) == 'D' and self.inlist:
|
||||
self.__delete_current_host()
|
||||
self.__update_popup()
|
||||
return
|
||||
if chr(c) == 'r' and self.inlist:
|
||||
self.__update_statuses()
|
||||
if chr(c) == 'a' and self.inlist:
|
||||
self.__add_popup()
|
||||
return
|
||||
|
||||
if self.popup:
|
||||
if self.popup.handle_read(c):
|
||||
self.popup = None
|
||||
self.refresh()
|
||||
return
|
||||
105
deluge/ui/console/modes/eventview.py
Normal file
@ -0,0 +1,105 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# eventview.py
|
||||
#
|
||||
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||
#
|
||||
# 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 deluge.component as component
|
||||
from basemode import BaseMode
|
||||
try:
|
||||
import curses
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class EventView(BaseMode):
|
||||
def __init__(self, parent_mode, stdscr, encoding=None):
|
||||
self.parent_mode = parent_mode
|
||||
BaseMode.__init__(self, stdscr, encoding)
|
||||
|
||||
def refresh(self):
|
||||
"This method just shows each line of the event log"
|
||||
events = component.get("ConsoleUI").events
|
||||
|
||||
self.add_string(0,self.statusbars.topbar)
|
||||
hstr = "%sPress [h] for help"%(" "*(self.cols - len(self.statusbars.bottombar) - 10))
|
||||
self.add_string(self.rows - 1, "%s%s"%(self.statusbars.bottombar,hstr))
|
||||
|
||||
if events:
|
||||
for i,event in enumerate(events):
|
||||
self.add_string(i+1,event)
|
||||
else:
|
||||
self.add_string(1,"{!white,black,bold!}No events to show yet")
|
||||
|
||||
self.stdscr.noutrefresh()
|
||||
curses.doupdate()
|
||||
|
||||
def back_to_overview(self):
|
||||
self.stdscr.clear()
|
||||
component.get("ConsoleUI").set_mode(self.parent_mode)
|
||||
self.parent_mode.resume()
|
||||
|
||||
def _doRead(self):
|
||||
c = self.stdscr.getch()
|
||||
|
||||
if c > 31 and c < 256:
|
||||
if chr(c) == 'Q':
|
||||
from twisted.internet import reactor
|
||||
if client.connected():
|
||||
def on_disconnect(result):
|
||||
reactor.stop()
|
||||
client.disconnect().addCallback(on_disconnect)
|
||||
else:
|
||||
reactor.stop()
|
||||
return
|
||||
elif chr(c) == 'q':
|
||||
self.back_to_overview()
|
||||
return
|
||||
|
||||
if c == 27:
|
||||
self.back_to_overview()
|
||||
return
|
||||
|
||||
# TODO: Scroll event list
|
||||
if c == curses.KEY_UP:
|
||||
pass
|
||||
elif c == curses.KEY_PPAGE:
|
||||
pass
|
||||
elif c == curses.KEY_DOWN:
|
||||
pass
|
||||
elif c == curses.KEY_NPAGE:
|
||||
pass
|
||||
|
||||
#self.refresh()
|
||||
199
deluge/ui/console/modes/format_utils.py
Normal file
@ -0,0 +1,199 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# format_utils.py
|
||||
#
|
||||
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||
#
|
||||
# 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 deluge.common
|
||||
try:
|
||||
import unicodedata
|
||||
haveud = True
|
||||
except:
|
||||
haveud = False
|
||||
|
||||
def format_speed(speed):
|
||||
if (speed > 0):
|
||||
return deluge.common.fspeed(speed)
|
||||
else:
|
||||
return "-"
|
||||
|
||||
def format_time(time):
|
||||
if (time > 0):
|
||||
return deluge.common.ftime(time)
|
||||
else:
|
||||
return "-"
|
||||
|
||||
def format_float(x):
|
||||
if x < 0:
|
||||
return "∞"
|
||||
else:
|
||||
return "%.3f"%x
|
||||
|
||||
def format_seeds_peers(num, total):
|
||||
return "%d (%d)"%(num,total)
|
||||
|
||||
def format_progress(perc):
|
||||
if perc < 100:
|
||||
return "%.2f%%"%perc
|
||||
else:
|
||||
return "100%"
|
||||
|
||||
def format_pieces(num, size):
|
||||
return "%d (%s)"%(num,deluge.common.fsize(size))
|
||||
|
||||
def format_priority(prio):
|
||||
if prio == -2: return "[Mixed]"
|
||||
if prio < 0: return "-"
|
||||
pstring = deluge.common.FILE_PRIORITY[prio]
|
||||
if prio > 0:
|
||||
return pstring[:pstring.index("Priority")-1]
|
||||
else:
|
||||
return pstring
|
||||
|
||||
def trim_string(string, w, have_dbls):
|
||||
if w <= 0:
|
||||
return ""
|
||||
elif w == 1:
|
||||
return "…"
|
||||
elif have_dbls:
|
||||
# have to do this the slow way
|
||||
chrs = []
|
||||
width = 4
|
||||
idx = 0
|
||||
while width < w:
|
||||
chrs.append(string[idx])
|
||||
if unicodedata.east_asian_width(string[idx]) in ['W','F']:
|
||||
width += 2
|
||||
else:
|
||||
width += 1
|
||||
idx += 1
|
||||
if width != w:
|
||||
chrs.pop()
|
||||
chrs.append('.')
|
||||
return "%s… "%("".join(chrs))
|
||||
else:
|
||||
return "%s… "%(string[0:w-2])
|
||||
|
||||
def format_column(col, lim):
|
||||
dbls = 0
|
||||
if haveud and isinstance(col,unicode):
|
||||
# might have some double width chars
|
||||
col = unicodedata.normalize("NFC",col)
|
||||
for c in col:
|
||||
if unicodedata.east_asian_width(c) in ['W','F']:
|
||||
# found a wide/full char
|
||||
dbls += 1
|
||||
size = len(col)+dbls
|
||||
if (size >= lim - 1):
|
||||
return trim_string(col,lim,dbls>0)
|
||||
else:
|
||||
return "%s%s"%(col," "*(lim-size))
|
||||
|
||||
def format_row(row,column_widths):
|
||||
return "".join([format_column(row[i],column_widths[i]) for i in range(0,len(row))])
|
||||
|
||||
import re
|
||||
from collections import deque
|
||||
_strip_re = re.compile("\{!.*?!\}")
|
||||
def wrap_string(string,width,min_lines=0,strip_colors=True):
|
||||
"""
|
||||
Wrap a string to fit in a particular width. Returns a list of output lines.
|
||||
|
||||
:param string: str, the string to wrap
|
||||
:param width: int, the maximum width of a line of text
|
||||
:param min_lines: int, extra lines will be added so the output tuple contains at least min_lines lines
|
||||
:param strip_colors: boolean, if True, text in {!!} blocks will not be considered as adding to the
|
||||
width of the line. They will still be present in the output.
|
||||
"""
|
||||
ret = []
|
||||
s1 = string.split("\n")
|
||||
|
||||
def insert_clr(s,offset,mtchs,clrs):
|
||||
end_pos = offset+len(s)
|
||||
while mtchs and (mtchs[0] <= end_pos) and (mtchs[0] >= offset):
|
||||
mtc = mtchs.popleft()-offset
|
||||
clr = clrs.popleft()
|
||||
end_pos += len(clr)
|
||||
s = "%s%s%s"%(s[:mtc],clr,s[mtc:])
|
||||
return s
|
||||
|
||||
for s in s1:
|
||||
cur_pos = offset = 0
|
||||
if strip_colors:
|
||||
mtchs = deque()
|
||||
clrs = deque()
|
||||
for m in _strip_re.finditer(s):
|
||||
mtchs.append(m.start())
|
||||
clrs.append(m.group())
|
||||
cstr = _strip_re.sub('',s)
|
||||
else:
|
||||
cstr = s
|
||||
while len(cstr) > width:
|
||||
sidx = cstr.rfind(" ",0,width-1)
|
||||
sidx += 1
|
||||
if sidx > 0:
|
||||
if strip_colors:
|
||||
to_app = cstr[0:sidx]
|
||||
to_app = insert_clr(to_app,offset,mtchs,clrs)
|
||||
ret.append(to_app)
|
||||
offset += len(to_app)
|
||||
else:
|
||||
ret.append(cstr[0:sidx])
|
||||
cstr = cstr[sidx:]
|
||||
if not cstr:
|
||||
cstr = None
|
||||
break
|
||||
else:
|
||||
# can't find a reasonable split, just split at width
|
||||
if strip_colors:
|
||||
to_app = cstr[0:width]
|
||||
to_app = insert_clr(to_app,offset,mtchs,clrs)
|
||||
ret.append(to_app)
|
||||
offset += len(to_app)
|
||||
else:
|
||||
ret.append(cstr[0:width])
|
||||
cstr = cstr[width:]
|
||||
if not cstr:
|
||||
cstr = None
|
||||
break
|
||||
if cstr != None:
|
||||
if strip_colors:
|
||||
ret.append(insert_clr(cstr,offset,mtchs,clrs))
|
||||
else:
|
||||
ret.append(cstr)
|
||||
|
||||
if min_lines>0:
|
||||
for i in range(len(ret),min_lines):
|
||||
ret.append(" ")
|
||||
|
||||
return ret
|
||||
660
deluge/ui/console/modes/input_popup.py
Normal file
@ -0,0 +1,660 @@
|
||||
#
|
||||
# input_popup.py
|
||||
#
|
||||
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||
#
|
||||
# Complete function from commands/add.py:
|
||||
# Copyright (C) 2008-2009 Ido Abramovich <ido.deluge@gmail.com>
|
||||
# Copyright (C) 2009 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.
|
||||
#
|
||||
#
|
||||
|
||||
try:
|
||||
import curses
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import logging,os,os.path
|
||||
|
||||
from popup import Popup
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class InputField:
|
||||
depend = None
|
||||
# render the input. return number of rows taken up
|
||||
def render(self,screen,row,width,selected,col=1):
|
||||
return 0
|
||||
def handle_read(self, c):
|
||||
if c in [curses.KEY_ENTER, 10, 127, 113]:
|
||||
return True
|
||||
return False
|
||||
def get_value(self):
|
||||
return None
|
||||
def set_value(self, value):
|
||||
pass
|
||||
|
||||
def set_depend(self,i,inverse=False):
|
||||
if not isinstance(i,CheckedInput):
|
||||
raise Exception("Can only depend on CheckedInputs")
|
||||
self.depend = i
|
||||
self.inverse = inverse
|
||||
|
||||
def depend_skip(self):
|
||||
if not self.depend:
|
||||
return False
|
||||
if self.inverse:
|
||||
return self.depend.checked
|
||||
else:
|
||||
return not self.depend.checked
|
||||
|
||||
class CheckedInput(InputField):
|
||||
def __init__(self, parent, message, name, checked=False):
|
||||
self.parent = parent
|
||||
self.chkd_inact = "[X] %s"%message
|
||||
self.unchkd_inact = "[ ] %s"%message
|
||||
self.chkd_act = "[{!black,white,bold!}X{!white,black!}] %s"%message
|
||||
self.unchkd_act = "[{!black,white,bold!} {!white,black!}] %s"%message
|
||||
self.name = name
|
||||
self.checked = checked
|
||||
|
||||
def render(self, screen, row, width, active, col=1):
|
||||
if self.checked and active:
|
||||
self.parent.add_string(row,self.chkd_act,screen,col,False,True)
|
||||
elif self.checked:
|
||||
self.parent.add_string(row,self.chkd_inact,screen,col,False,True)
|
||||
elif active:
|
||||
self.parent.add_string(row,self.unchkd_act,screen,col,False,True)
|
||||
else:
|
||||
self.parent.add_string(row,self.unchkd_inact,screen,col,False,True)
|
||||
return 1
|
||||
|
||||
def handle_read(self, c):
|
||||
if c == 32:
|
||||
self.checked = not self.checked
|
||||
|
||||
def get_value(self):
|
||||
return self.checked
|
||||
|
||||
def set_value(self, c):
|
||||
self.checked = c
|
||||
|
||||
|
||||
class CheckedPlusInput(InputField):
|
||||
def __init__(self, parent, message, name, child,checked=False):
|
||||
self.parent = parent
|
||||
self.chkd_inact = "[X] %s"%message
|
||||
self.unchkd_inact = "[ ] %s"%message
|
||||
self.chkd_act = "[{!black,white,bold!}X{!white,black!}] %s"%message
|
||||
self.unchkd_act = "[{!black,white,bold!} {!white,black!}] %s"%message
|
||||
self.name = name
|
||||
self.checked = checked
|
||||
self.msglen = len(self.chkd_inact)+1
|
||||
self.child = child
|
||||
self.child_active = False
|
||||
|
||||
def render(self, screen, row, width, active, col=1):
|
||||
isact = active and not self.child_active
|
||||
if self.checked and isact:
|
||||
self.parent.add_string(row,self.chkd_act,screen,col,False,True)
|
||||
elif self.checked:
|
||||
self.parent.add_string(row,self.chkd_inact,screen,col,False,True)
|
||||
elif isact:
|
||||
self.parent.add_string(row,self.unchkd_act,screen,col,False,True)
|
||||
else:
|
||||
self.parent.add_string(row,self.unchkd_inact,screen,col,False,True)
|
||||
|
||||
if active and self.checked and self.child_active:
|
||||
self.parent.add_string(row+1,"(esc to leave)",screen,col,False,True)
|
||||
elif active and self.checked:
|
||||
self.parent.add_string(row+1,"(right arrow to edit)",screen,col,False,True)
|
||||
rows = 2
|
||||
# show child
|
||||
if self.checked:
|
||||
if isinstance(self.child,(TextInput,IntSpinInput,FloatSpinInput)):
|
||||
crows = self.child.render(screen,row,width-self.msglen,self.child_active and active,col+self.msglen,self.msglen)
|
||||
else:
|
||||
crows = self.child.render(screen,row,width-self.msglen,self.child_active and active,col+self.msglen)
|
||||
rows = max(rows,crows)
|
||||
else:
|
||||
self.parent.add_string(row,"(enable to view/edit value)",screen,col+self.msglen,False,True)
|
||||
return rows
|
||||
|
||||
def handle_read(self, c):
|
||||
if self.child_active:
|
||||
if c == 27: # leave child on esc
|
||||
self.child_active = False
|
||||
return
|
||||
# pass keys through to child
|
||||
self.child.handle_read(c)
|
||||
else:
|
||||
if c == 32:
|
||||
self.checked = not self.checked
|
||||
if c == curses.KEY_RIGHT:
|
||||
self.child_active = True
|
||||
|
||||
def get_value(self):
|
||||
return self.checked
|
||||
|
||||
def set_value(self, c):
|
||||
self.checked = c
|
||||
|
||||
def get_child(self):
|
||||
return self.child
|
||||
|
||||
|
||||
|
||||
class IntSpinInput(InputField):
|
||||
def __init__(self, parent, message, name, move_func, value, min_val, max_val):
|
||||
self.parent = parent
|
||||
self.message = message
|
||||
self.name = name
|
||||
self.value = int(value)
|
||||
self.initvalue = self.value
|
||||
self.valstr = "%d"%self.value
|
||||
self.cursor = len(self.valstr)
|
||||
self.cursoff = len(self.message)+4 # + 4 for the " [ " in the rendered string
|
||||
self.move_func = move_func
|
||||
self.min_val = min_val
|
||||
self.max_val = max_val
|
||||
self.need_update = False
|
||||
|
||||
def render(self, screen, row, width, active, col=1, cursor_offset=0):
|
||||
if not active and self.need_update:
|
||||
if not self.valstr or self.valstr == '-':
|
||||
self.value = self.initvalue
|
||||
else:
|
||||
self.value = int(self.valstr)
|
||||
if self.value < self.min_val:
|
||||
self.value = self.min_val
|
||||
if self.value > self.max_val:
|
||||
self.value = self.max_val
|
||||
self.valstr = "%d"%self.value
|
||||
self.cursor = len(self.valstr)
|
||||
self.need_update = False
|
||||
if not self.valstr:
|
||||
self.parent.add_string(row,"%s [ ]"%self.message,screen,col,False,True)
|
||||
elif active:
|
||||
self.parent.add_string(row,"%s [ {!black,white,bold!}%s{!white,black!} ]"%(self.message,self.valstr),screen,col,False,True)
|
||||
else:
|
||||
self.parent.add_string(row,"%s [ %s ]"%(self.message,self.valstr),screen,col,False,True)
|
||||
|
||||
if active:
|
||||
self.move_func(row,self.cursor+self.cursoff+cursor_offset)
|
||||
|
||||
return 1
|
||||
|
||||
def handle_read(self, c):
|
||||
if c == curses.KEY_PPAGE and self.value < self.max_val:
|
||||
self.value+=1
|
||||
self.valstr = "%d"%self.value
|
||||
self.cursor = len(self.valstr)
|
||||
elif c == curses.KEY_NPAGE and self.value > self.min_val:
|
||||
self.value-=1
|
||||
self.valstr = "%d"%self.value
|
||||
self.cursor = len(self.valstr)
|
||||
elif c == curses.KEY_LEFT:
|
||||
self.cursor = max(0,self.cursor-1)
|
||||
elif c == curses.KEY_RIGHT:
|
||||
self.cursor = min(len(self.valstr),self.cursor+1)
|
||||
elif c == curses.KEY_HOME:
|
||||
self.cursor = 0
|
||||
elif c == curses.KEY_END:
|
||||
self.cursor = len(self.valstr)
|
||||
elif c == curses.KEY_BACKSPACE or c == 127:
|
||||
if self.valstr and self.cursor > 0:
|
||||
self.valstr = self.valstr[:self.cursor - 1] + self.valstr[self.cursor:]
|
||||
self.cursor-=1
|
||||
self.need_update = True
|
||||
elif c == curses.KEY_DC:
|
||||
if self.valstr and self.cursor < len(self.valstr):
|
||||
self.valstr = self.valstr[:self.cursor] + self.valstr[self.cursor+1:]
|
||||
self.need_update = True
|
||||
elif c == 45 and self.cursor == 0 and self.min_val < 0:
|
||||
minus_place = self.valstr.find('-')
|
||||
if minus_place >= 0: return
|
||||
self.valstr = chr(c)+self.valstr
|
||||
self.cursor += 1
|
||||
self.need_update = True
|
||||
elif c > 47 and c < 58:
|
||||
if c == 48 and self.cursor == 0: return
|
||||
minus_place = self.valstr.find('-')
|
||||
if self.cursor <= minus_place: return
|
||||
if self.cursor == len(self.valstr):
|
||||
self.valstr += chr(c)
|
||||
else:
|
||||
# Insert into string
|
||||
self.valstr = self.valstr[:self.cursor] + chr(c) + self.valstr[self.cursor:]
|
||||
self.need_update = True
|
||||
# Move the cursor forward
|
||||
self.cursor+=1
|
||||
|
||||
|
||||
def get_value(self):
|
||||
return self.value
|
||||
|
||||
def set_value(self, val):
|
||||
self.value = int(val)
|
||||
self.valstr = "%d"%self.value
|
||||
self.cursor = len(self.valstr)
|
||||
|
||||
|
||||
class FloatSpinInput(InputField):
|
||||
def __init__(self, parent, message, name, move_func, value, inc_amt, precision, min_val, max_val):
|
||||
self.parent = parent
|
||||
self.message = message
|
||||
self.name = name
|
||||
self.precision = precision
|
||||
self.inc_amt = inc_amt
|
||||
self.value = round(float(value),self.precision)
|
||||
self.initvalue = self.value
|
||||
self.fmt = "%%.%df"%precision
|
||||
self.valstr = self.fmt%self.value
|
||||
self.cursor = len(self.valstr)
|
||||
self.cursoff = len(self.message)+4 # + 4 for the " [ " in the rendered string
|
||||
self.move_func = move_func
|
||||
self.min_val = min_val
|
||||
self.max_val = max_val
|
||||
self.need_update = False
|
||||
|
||||
def __limit_value(self):
|
||||
if self.value < self.min_val:
|
||||
self.value = self.min_val
|
||||
if self.value > self.max_val:
|
||||
self.value = self.max_val
|
||||
|
||||
def render(self, screen, row, width, active, col=1, cursor_offset=0):
|
||||
if not active and self.need_update:
|
||||
try:
|
||||
self.value = round(float(self.valstr),self.precision)
|
||||
self.__limit_value()
|
||||
except ValueError:
|
||||
self.value = self.initvalue
|
||||
self.valstr = self.fmt%self.value
|
||||
self.cursor = len(self.valstr)
|
||||
self.need_update = False
|
||||
if not self.valstr:
|
||||
self.parent.add_string(row,"%s [ ]"%self.message,screen,col,False,True)
|
||||
elif active:
|
||||
self.parent.add_string(row,"%s [ {!black,white,bold!}%s{!white,black!} ]"%(self.message,self.valstr),screen,col,False,True)
|
||||
else:
|
||||
self.parent.add_string(row,"%s [ %s ]"%(self.message,self.valstr),screen,col,False,True)
|
||||
if active:
|
||||
self.move_func(row,self.cursor+self.cursoff+cursor_offset)
|
||||
|
||||
return 1
|
||||
|
||||
def handle_read(self, c):
|
||||
if c == curses.KEY_PPAGE:
|
||||
self.value+=self.inc_amt
|
||||
self.__limit_value()
|
||||
self.valstr = self.fmt%self.value
|
||||
self.cursor = len(self.valstr)
|
||||
elif c == curses.KEY_NPAGE:
|
||||
self.value-=self.inc_amt
|
||||
self.__limit_value()
|
||||
self.valstr = self.fmt%self.value
|
||||
self.cursor = len(self.valstr)
|
||||
elif c == curses.KEY_LEFT:
|
||||
self.cursor = max(0,self.cursor-1)
|
||||
elif c == curses.KEY_RIGHT:
|
||||
self.cursor = min(len(self.valstr),self.cursor+1)
|
||||
elif c == curses.KEY_HOME:
|
||||
self.cursor = 0
|
||||
elif c == curses.KEY_END:
|
||||
self.cursor = len(self.valstr)
|
||||
elif c == curses.KEY_BACKSPACE or c == 127:
|
||||
if self.valstr and self.cursor > 0:
|
||||
self.valstr = self.valstr[:self.cursor - 1] + self.valstr[self.cursor:]
|
||||
self.cursor-=1
|
||||
self.need_update = True
|
||||
elif c == curses.KEY_DC:
|
||||
if self.valstr and self.cursor < len(self.valstr):
|
||||
self.valstr = self.valstr[:self.cursor] + self.valstr[self.cursor+1:]
|
||||
self.need_update = True
|
||||
elif c == 45 and self.cursor == 0 and self.min_val < 0:
|
||||
minus_place = self.valstr.find('-')
|
||||
if minus_place >= 0: return
|
||||
self.valstr = chr(c)+self.valstr
|
||||
self.cursor += 1
|
||||
self.need_update = True
|
||||
elif c == 46:
|
||||
minus_place = self.valstr.find('-')
|
||||
if self.cursor <= minus_place: return
|
||||
point_place = self.valstr.find('.')
|
||||
if point_place >= 0: return
|
||||
if self.cursor == len(self.valstr):
|
||||
self.valstr += chr(c)
|
||||
else:
|
||||
# Insert into string
|
||||
self.valstr = self.valstr[:self.cursor] + chr(c) + self.valstr[self.cursor:]
|
||||
self.need_update = True
|
||||
# Move the cursor forward
|
||||
self.cursor+=1
|
||||
elif (c > 47 and c < 58):
|
||||
minus_place = self.valstr.find('-')
|
||||
if self.cursor <= minus_place: return
|
||||
if self.cursor == len(self.valstr):
|
||||
self.valstr += chr(c)
|
||||
else:
|
||||
# Insert into string
|
||||
self.valstr = self.valstr[:self.cursor] + chr(c) + self.valstr[self.cursor:]
|
||||
self.need_update = True
|
||||
# Move the cursor forward
|
||||
self.cursor+=1
|
||||
|
||||
|
||||
def get_value(self):
|
||||
return self.value
|
||||
|
||||
def set_value(self, val):
|
||||
self.value = round(float(val),self.precision)
|
||||
self.valstr = self.fmt%self.value
|
||||
self.cursor = len(self.valstr)
|
||||
|
||||
|
||||
class SelectInput(InputField):
|
||||
def __init__(self, parent, message, name, opts, vals, selidx):
|
||||
self.parent = parent
|
||||
self.message = message
|
||||
self.name = name
|
||||
self.opts = opts
|
||||
self.vals = vals
|
||||
self.selidx = selidx
|
||||
|
||||
def render(self, screen, row, width, selected, col=1):
|
||||
if self.message:
|
||||
self.parent.add_string(row,self.message,screen,col,False,True)
|
||||
row += 1
|
||||
off = col+1
|
||||
for i,opt in enumerate(self.opts):
|
||||
if selected and i == self.selidx:
|
||||
self.parent.add_string(row,"{!black,white,bold!}[%s]"%opt,screen,off,False,True)
|
||||
elif i == self.selidx:
|
||||
self.parent.add_string(row,"[{!white,black,underline!}%s{!white,black!}]"%opt,screen,off,False,True)
|
||||
else:
|
||||
self.parent.add_string(row,"[%s]"%opt,screen,off,False,True)
|
||||
off += len(opt)+3
|
||||
if self.message:
|
||||
return 2
|
||||
else:
|
||||
return 1
|
||||
|
||||
def handle_read(self, c):
|
||||
if c == curses.KEY_LEFT:
|
||||
self.selidx = max(0,self.selidx-1)
|
||||
if c == curses.KEY_RIGHT:
|
||||
self.selidx = min(len(self.opts)-1,self.selidx+1)
|
||||
|
||||
def get_value(self):
|
||||
return self.vals[self.selidx]
|
||||
|
||||
def set_value(self, nv):
|
||||
for i,val in enumerate(self.vals):
|
||||
if nv == val:
|
||||
self.selidx = i
|
||||
return
|
||||
raise Exception("Invalid value for SelectInput")
|
||||
|
||||
class TextInput(InputField):
|
||||
def __init__(self, parent, move_func, width, message, name, value, docmp):
|
||||
self.parent = parent
|
||||
self.move_func = move_func
|
||||
self.width = width
|
||||
|
||||
self.message = message
|
||||
self.name = name
|
||||
self.value = value
|
||||
self.docmp = docmp
|
||||
|
||||
self.tab_count = 0
|
||||
self.cursor = len(self.value)
|
||||
self.opts = None
|
||||
self.opt_off = 0
|
||||
|
||||
def render(self,screen,row,width,selected,col=1,cursor_offset=0):
|
||||
if self.message:
|
||||
self.parent.add_string(row,self.message,screen,col,False,True)
|
||||
row += 1
|
||||
if selected:
|
||||
if self.opts:
|
||||
self.parent.add_string(row+1,self.opts[self.opt_off:],screen,col,False,True)
|
||||
if self.cursor > (width-3):
|
||||
self.move_func(row,width-2)
|
||||
else:
|
||||
self.move_func(row,self.cursor+1+cursor_offset)
|
||||
slen = len(self.value)+3
|
||||
if slen > width:
|
||||
vstr = self.value[(slen-width):]
|
||||
else:
|
||||
vstr = self.value.ljust(width-2)
|
||||
self.parent.add_string(row,"{!black,white,bold!}%s"%vstr,screen,col,False,False)
|
||||
|
||||
if self.message:
|
||||
return 3
|
||||
else:
|
||||
return 2
|
||||
|
||||
def get_value(self):
|
||||
return self.value
|
||||
|
||||
def set_value(self,val):
|
||||
self.value = val
|
||||
self.cursor = len(self.value)
|
||||
|
||||
# most of the cursor,input stuff here taken from ui/console/screen.py
|
||||
def handle_read(self,c):
|
||||
if c == 9 and self.docmp:
|
||||
# Keep track of tab hit count to know when it's double-hit
|
||||
self.tab_count += 1
|
||||
if self.tab_count > 1:
|
||||
second_hit = True
|
||||
self.tab_count = 0
|
||||
else:
|
||||
second_hit = False
|
||||
|
||||
# We only call the tab completer function if we're at the end of
|
||||
# the input string on the cursor is on a space
|
||||
if self.cursor == len(self.value) or self.value[self.cursor] == " ":
|
||||
if self.opts:
|
||||
prev = self.opt_off
|
||||
self.opt_off += self.width-3
|
||||
# now find previous double space, best guess at a split point
|
||||
# in future could keep opts unjoined to get this really right
|
||||
self.opt_off = self.opts.rfind(" ",0,self.opt_off)+2
|
||||
if second_hit and self.opt_off == prev: # double tap and we're at the end
|
||||
self.opt_off = 0
|
||||
else:
|
||||
opts = self.complete(self.value)
|
||||
if len(opts) == 1: # only one option, just complete it
|
||||
self.value = opts[0]
|
||||
self.cursor = len(opts[0])
|
||||
self.tab_count = 0
|
||||
elif len(opts) > 1:
|
||||
prefix = os.path.commonprefix(opts)
|
||||
if prefix:
|
||||
self.value = prefix
|
||||
self.cursor = len(prefix)
|
||||
|
||||
if len(opts) > 1 and second_hit: # display multiple options on second tab hit
|
||||
sp = self.value.rfind(os.sep)+1
|
||||
self.opts = " ".join([o[sp:] for o in opts])
|
||||
|
||||
# Cursor movement
|
||||
elif c == curses.KEY_LEFT:
|
||||
self.cursor = max(0,self.cursor-1)
|
||||
elif c == curses.KEY_RIGHT:
|
||||
self.cursor = min(len(self.value),self.cursor+1)
|
||||
elif c == curses.KEY_HOME:
|
||||
self.cursor = 0
|
||||
elif c == curses.KEY_END:
|
||||
self.cursor = len(self.value)
|
||||
|
||||
if c != 9:
|
||||
self.opts = None
|
||||
self.opt_off = 0
|
||||
self.tab_count = 0
|
||||
|
||||
# Delete a character in the input string based on cursor position
|
||||
if c == curses.KEY_BACKSPACE or c == 127:
|
||||
if self.value and self.cursor > 0:
|
||||
self.value = self.value[:self.cursor - 1] + self.value[self.cursor:]
|
||||
self.cursor-=1
|
||||
elif c == curses.KEY_DC:
|
||||
if self.value and self.cursor < len(self.value):
|
||||
self.value = self.value[:self.cursor] + self.value[self.cursor+1:]
|
||||
elif c > 31 and c < 256:
|
||||
# Emulate getwch
|
||||
stroke = chr(c)
|
||||
uchar = ""
|
||||
while not uchar:
|
||||
try:
|
||||
uchar = stroke.decode(self.parent.encoding)
|
||||
except UnicodeDecodeError:
|
||||
c = self.parent.stdscr.getch()
|
||||
stroke += chr(c)
|
||||
if uchar:
|
||||
if self.cursor == len(self.value):
|
||||
self.value += uchar
|
||||
else:
|
||||
# Insert into string
|
||||
self.value = self.value[:self.cursor] + uchar + self.value[self.cursor:]
|
||||
# Move the cursor forward
|
||||
self.cursor+=1
|
||||
|
||||
|
||||
def complete(self,line):
|
||||
line = os.path.abspath(os.path.expanduser(line))
|
||||
ret = []
|
||||
if os.path.exists(line):
|
||||
# This is a correct path, check to see if it's a directory
|
||||
if os.path.isdir(line):
|
||||
# Directory, so we need to show contents of directory
|
||||
#ret.extend(os.listdir(line))
|
||||
for f in os.listdir(line):
|
||||
# Skip hidden
|
||||
if f.startswith("."):
|
||||
continue
|
||||
f = os.path.join(line, f)
|
||||
if os.path.isdir(f):
|
||||
f += os.sep
|
||||
ret.append(f)
|
||||
else:
|
||||
# This is a file, but we could be looking for another file that
|
||||
# shares a common prefix.
|
||||
for f in os.listdir(os.path.dirname(line)):
|
||||
if f.startswith(os.path.split(line)[1]):
|
||||
ret.append(os.path.join( os.path.dirname(line), f))
|
||||
else:
|
||||
# This path does not exist, so lets do a listdir on it's parent
|
||||
# and find any matches.
|
||||
ret = []
|
||||
if os.path.isdir(os.path.dirname(line)):
|
||||
for f in os.listdir(os.path.dirname(line)):
|
||||
if f.startswith(os.path.split(line)[1]):
|
||||
p = os.path.join(os.path.dirname(line), f)
|
||||
|
||||
if os.path.isdir(p):
|
||||
p += os.sep
|
||||
ret.append(p)
|
||||
return ret
|
||||
|
||||
|
||||
class InputPopup(Popup):
|
||||
def __init__(self,parent_mode,title,width_req=-1,height_req=-1,close_cb=None):
|
||||
Popup.__init__(self,parent_mode,title,width_req,height_req,close_cb)
|
||||
self.inputs = []
|
||||
self.spaces = []
|
||||
self.current_input = 0
|
||||
|
||||
def move(self,r,c):
|
||||
self._cursor_row = r
|
||||
self._cursor_col = c
|
||||
|
||||
def add_text_input(self, message, name, value="", complete=True):
|
||||
"""
|
||||
Add a text input field to the popup.
|
||||
|
||||
:param message: string to display above the input field
|
||||
:param name: name of the field, for the return callback
|
||||
:param value: initial value of the field
|
||||
:param complete: should completion be run when tab is hit and this field is active
|
||||
"""
|
||||
self.inputs.append(TextInput(self.parent, self.move, self.width, message,
|
||||
name, value, complete))
|
||||
|
||||
def add_spaces(self, num):
|
||||
self.spaces.append((len(self.inputs)-1,num))
|
||||
|
||||
def add_select_input(self, message, name, opts, vals, default_index=0):
|
||||
self.inputs.append(SelectInput(self.parent, message, name, opts, vals, default_index))
|
||||
|
||||
def add_checked_input(self, message, name, checked=False):
|
||||
self.inputs.append(CheckedInput(self.parent,message,name,checked))
|
||||
|
||||
def _refresh_lines(self):
|
||||
self._cursor_row = -1
|
||||
self._cursor_col = -1
|
||||
curses.curs_set(0)
|
||||
crow = 1
|
||||
spos = 0
|
||||
for i,ipt in enumerate(self.inputs):
|
||||
crow += ipt.render(self.screen,crow,self.width,i==self.current_input)
|
||||
if self.spaces and (spos < len(self.spaces)) and (i == self.spaces[spos][0]):
|
||||
crow += self.spaces[spos][1]
|
||||
spos += 1
|
||||
|
||||
# need to do this last as adding things moves the cursor
|
||||
if self._cursor_row >= 0:
|
||||
curses.curs_set(2)
|
||||
self.screen.move(self._cursor_row,self._cursor_col)
|
||||
|
||||
def handle_read(self, c):
|
||||
if c == curses.KEY_UP:
|
||||
self.current_input = max(0,self.current_input-1)
|
||||
elif c == curses.KEY_DOWN:
|
||||
self.current_input = min(len(self.inputs)-1,self.current_input+1)
|
||||
elif c == curses.KEY_ENTER or c == 10:
|
||||
if self.close_cb:
|
||||
vals = {}
|
||||
for ipt in self.inputs:
|
||||
vals[ipt.name] = ipt.get_value()
|
||||
curses.curs_set(0)
|
||||
self.close_cb(vals)
|
||||
return True # close the popup
|
||||
elif c == 27: # close on esc, no action
|
||||
return True
|
||||
elif self.inputs:
|
||||
self.inputs[self.current_input].handle_read(c)
|
||||
|
||||
self.refresh()
|
||||
return False
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#
|
||||
# screen.py
|
||||
# legacy.py
|
||||
#
|
||||
# Copyright (C) 2008-2009 Ido Abramovich <ido.deluge@gmail.com>
|
||||
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||
#
|
||||
# Deluge is free software.
|
||||
@ -33,61 +34,35 @@
|
||||
#
|
||||
#
|
||||
|
||||
import sys
|
||||
import logging
|
||||
try:
|
||||
import curses
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import colors
|
||||
try:
|
||||
import signal
|
||||
from fcntl import ioctl
|
||||
import termios
|
||||
import struct
|
||||
except:
|
||||
pass
|
||||
|
||||
from twisted.internet import reactor
|
||||
from basemode import BaseMode
|
||||
import deluge.ui.console.colors as colors
|
||||
from twisted.internet import defer, reactor
|
||||
from deluge.ui.client import client
|
||||
import deluge.component as component
|
||||
|
||||
import logging,os
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class CursesStdIO(object):
|
||||
"""fake fd to be registered as a reader with the twisted reactor.
|
||||
Curses classes needing input should extend this"""
|
||||
|
||||
def fileno(self):
|
||||
""" We want to select on FD 0 """
|
||||
return 0
|
||||
|
||||
def doRead(self):
|
||||
"""called when input is ready"""
|
||||
pass
|
||||
def logPrefix(self): return 'CursesClient'
|
||||
|
||||
LINES_BUFFER_SIZE = 5000
|
||||
INPUT_HISTORY_SIZE = 500
|
||||
|
||||
class Screen(CursesStdIO):
|
||||
def __init__(self, stdscr, command_parser, tab_completer=None, encoding=None):
|
||||
"""
|
||||
A curses screen designed to run as a reader in a twisted reactor.
|
||||
class Legacy(BaseMode):
|
||||
def __init__(self, stdscr, encoding=None):
|
||||
|
||||
:param command_parser: a function that will be passed a string when the
|
||||
user hits enter
|
||||
:param tab_completer: a function that is sent the `:prop:input` string when
|
||||
the user hits tab. It's intended purpose is to modify the input string.
|
||||
It should return a 2-tuple (input string, input cursor).
|
||||
self.batch_write = False
|
||||
self.lines = []
|
||||
|
||||
"""
|
||||
log.debug("Screen init!")
|
||||
# Function to be called with commands
|
||||
self.command_parser = command_parser
|
||||
self.tab_completer = tab_completer
|
||||
self.stdscr = stdscr
|
||||
# Make the input calls non-blocking
|
||||
self.stdscr.nodelay(1)
|
||||
# A list of strings to be displayed based on the offset (scroll)
|
||||
self.lines = []
|
||||
# The offset to display lines
|
||||
self.display_lines_offset = 0
|
||||
|
||||
# Holds the user input and is cleared on 'enter'
|
||||
self.input = ""
|
||||
@ -101,206 +76,34 @@ class Screen(CursesStdIO):
|
||||
# Keep track of double-tabs
|
||||
self.tab_count = 0
|
||||
|
||||
# Strings for the 2 status bars
|
||||
self.topbar = ""
|
||||
self.bottombar = ""
|
||||
# Get a handle to the main console
|
||||
self.console = component.get("ConsoleUI")
|
||||
|
||||
# A list of strings to be displayed based on the offset (scroll)
|
||||
self.lines = []
|
||||
# The offset to display lines
|
||||
self.display_lines_offset = 0
|
||||
# show the cursor
|
||||
curses.curs_set(2)
|
||||
|
||||
# Keep track of the screen size
|
||||
self.rows, self.cols = self.stdscr.getmaxyx()
|
||||
try:
|
||||
signal.signal(signal.SIGWINCH, self.on_resize)
|
||||
except Exception, e:
|
||||
log.debug("Unable to catch SIGWINCH signal!")
|
||||
BaseMode.__init__(self, stdscr, encoding)
|
||||
|
||||
if not encoding:
|
||||
self.encoding = sys.getdefaultencoding()
|
||||
else:
|
||||
self.encoding = encoding
|
||||
# This gets fired once we have received the torrents list from the core
|
||||
self.started_deferred = defer.Deferred()
|
||||
|
||||
# Do a refresh right away to draw the screen
|
||||
self.refresh()
|
||||
# Maintain a list of (torrent_id, name) for use in tab completion
|
||||
self.torrents = []
|
||||
def on_session_state(result):
|
||||
def on_torrents_status(torrents):
|
||||
for torrent_id, status in torrents.items():
|
||||
self.torrents.append((torrent_id, status["name"]))
|
||||
self.started_deferred.callback(True)
|
||||
|
||||
def on_resize(self, *args):
|
||||
log.debug("on_resize_from_signal")
|
||||
# Get the new rows and cols value
|
||||
self.rows, self.cols = struct.unpack("hhhh", ioctl(0, termios.TIOCGWINSZ ,"\000"*8))[0:2]
|
||||
curses.resizeterm(self.rows, self.cols)
|
||||
self.refresh()
|
||||
client.core.get_torrents_status({"id": result}, ["name"]).addCallback(on_torrents_status)
|
||||
client.core.get_session_state().addCallback(on_session_state)
|
||||
|
||||
def connectionLost(self, reason):
|
||||
self.close()
|
||||
# Register some event handlers to keep the torrent list up-to-date
|
||||
client.register_event_handler("TorrentAddedEvent", self.on_torrent_added_event)
|
||||
client.register_event_handler("TorrentRemovedEvent", self.on_torrent_removed_event)
|
||||
|
||||
def add_line(self, text, refresh=True):
|
||||
"""
|
||||
Add a line to the screen. This will be showed between the two bars.
|
||||
The text can be formatted with color using the following format:
|
||||
|
||||
"{!fg, bg, attributes, ...!}"
|
||||
|
||||
See: http://docs.python.org/library/curses.html#constants for attributes.
|
||||
|
||||
Alternatively, it can use some built-in scheme for coloring.
|
||||
See colors.py for built-in schemes.
|
||||
|
||||
"{!scheme!}"
|
||||
|
||||
Examples:
|
||||
|
||||
"{!blue, black, bold!}My Text is {!white, black!}cool"
|
||||
"{!info!}I am some info text!"
|
||||
"{!error!}Uh oh!"
|
||||
|
||||
:param text: the text to show
|
||||
:type text: string
|
||||
:param refresh: if True, the screen will refresh after the line is added
|
||||
:type refresh: bool
|
||||
|
||||
"""
|
||||
|
||||
def get_line_chunks(line):
|
||||
"""
|
||||
Returns a list of 2-tuples (color string, text)
|
||||
|
||||
"""
|
||||
chunks = []
|
||||
num_chunks = line.count("{!")
|
||||
for i in range(num_chunks):
|
||||
# Find the beginning and end of the color tag
|
||||
beg = line.find("{!")
|
||||
end = line.find("!}") + 2
|
||||
color = line[beg:end]
|
||||
line = line[end:]
|
||||
|
||||
# Check to see if this is the last chunk
|
||||
if i + 1 == num_chunks:
|
||||
text = line
|
||||
else:
|
||||
# Not the last chunk so get the text up to the next tag
|
||||
# and remove the text from line
|
||||
text = line[:line.find("{!")]
|
||||
line = line[line.find("{!"):]
|
||||
|
||||
chunks.append((color, text))
|
||||
|
||||
return chunks
|
||||
|
||||
for line in text.splitlines():
|
||||
# We need to check for line lengths here and split as necessary
|
||||
try:
|
||||
line_length = colors.get_line_length(line)
|
||||
except colors.BadColorString:
|
||||
log.error("Passed a bad colored string..")
|
||||
line_length = len(line)
|
||||
|
||||
if line_length >= (self.cols - 1):
|
||||
s = ""
|
||||
# The length of the text without the color tags
|
||||
s_len = 0
|
||||
# We need to split this over multiple lines
|
||||
for chunk in get_line_chunks(line):
|
||||
if (len(chunk[1]) + s_len) < (self.cols - 1):
|
||||
# This chunk plus the current string in 's' isn't over
|
||||
# the maximum width, so just append the color tag and text
|
||||
s += chunk[0] + chunk[1]
|
||||
s_len += len(chunk[1])
|
||||
else:
|
||||
# The chunk plus the current string in 's' is too long.
|
||||
# We need to take as much of the chunk and put it into 's'
|
||||
# with the color tag.
|
||||
remain = (self.cols - 1) - s_len
|
||||
s += chunk[0] + chunk[1][:remain]
|
||||
# We append the line since it's full
|
||||
self.lines.append(s)
|
||||
# Start a new 's' with the remainder chunk
|
||||
s = chunk[0] + chunk[1][remain:]
|
||||
s_len = len(chunk[1][remain:])
|
||||
# Append the final string which may or may not be the full width
|
||||
if s:
|
||||
self.lines.append(s)
|
||||
else:
|
||||
self.lines.append(line)
|
||||
|
||||
while len(self.lines) > LINES_BUFFER_SIZE:
|
||||
# Remove the oldest line if the max buffer size has been reached
|
||||
del self.lines[0]
|
||||
|
||||
if refresh:
|
||||
self.refresh()
|
||||
|
||||
def add_string(self, row, string):
|
||||
"""
|
||||
Adds a string to the desired `:param:row`.
|
||||
|
||||
:param row: int, the row number to write the string
|
||||
|
||||
"""
|
||||
col = 0
|
||||
try:
|
||||
parsed = colors.parse_color_string(string, self.encoding)
|
||||
except colors.BadColorString, e:
|
||||
log.error("Cannot add bad color string %s: %s", string, e)
|
||||
return
|
||||
|
||||
for index, (color, s) in enumerate(parsed):
|
||||
if index + 1 == len(parsed):
|
||||
# This is the last string so lets append some " " to it
|
||||
s += " " * (self.cols - (col + len(s)) - 1)
|
||||
self.stdscr.addstr(row, col, s, color)
|
||||
col += len(s)
|
||||
|
||||
def refresh(self):
|
||||
"""
|
||||
Refreshes the screen.
|
||||
Updates the lines based on the`:attr:lines` based on the `:attr:display_lines_offset`
|
||||
attribute and the status bars.
|
||||
"""
|
||||
self.stdscr.clear()
|
||||
|
||||
# Update the status bars
|
||||
self.add_string(0, self.topbar)
|
||||
self.add_string(self.rows - 2, self.bottombar)
|
||||
|
||||
# The number of rows minus the status bars and the input line
|
||||
available_lines = self.rows - 3
|
||||
# If the amount of lines exceeds the number of rows, we need to figure out
|
||||
# which ones to display based on the offset
|
||||
if len(self.lines) > available_lines:
|
||||
# Get the lines to display based on the offset
|
||||
offset = len(self.lines) - self.display_lines_offset
|
||||
lines = self.lines[-(available_lines - offset):offset]
|
||||
elif len(self.lines) == available_lines:
|
||||
lines = self.lines
|
||||
else:
|
||||
lines = [""] * (available_lines - len(self.lines))
|
||||
lines.extend(self.lines)
|
||||
|
||||
# Add the lines to the screen
|
||||
for index, line in enumerate(lines):
|
||||
self.add_string(index + 1, line)
|
||||
|
||||
# Add the input string
|
||||
self.add_string(self.rows - 1, self.input)
|
||||
|
||||
# Move the cursor
|
||||
self.stdscr.move(self.rows - 1, self.input_cursor)
|
||||
self.stdscr.redrawwin()
|
||||
self.stdscr.refresh()
|
||||
|
||||
def doRead(self):
|
||||
"""
|
||||
Called when there is data to be read, ie, input from the keyboard.
|
||||
"""
|
||||
# We wrap this function to catch exceptions and shutdown the mainloop
|
||||
try:
|
||||
self._doRead()
|
||||
except Exception, e:
|
||||
log.exception(e)
|
||||
reactor.stop()
|
||||
def update(self):
|
||||
pass
|
||||
|
||||
def _doRead(self):
|
||||
# Read the character
|
||||
@ -310,7 +113,7 @@ class Screen(CursesStdIO):
|
||||
if c == curses.KEY_ENTER or c == 10:
|
||||
if self.input:
|
||||
self.add_line(">>> " + self.input)
|
||||
self.command_parser(self.input.encode(self.encoding))
|
||||
self.do_command(self.input.encode(self.encoding))
|
||||
if len(self.input_history) == INPUT_HISTORY_SIZE:
|
||||
# Remove the oldest input history if the max history size
|
||||
# is reached.
|
||||
@ -426,14 +229,375 @@ class Screen(CursesStdIO):
|
||||
|
||||
# Update the input string on the screen
|
||||
self.add_string(self.rows - 1, self.input)
|
||||
self.stdscr.move(self.rows - 1, self.input_cursor)
|
||||
try:
|
||||
self.stdscr.move(self.rows - 1, self.input_cursor)
|
||||
except curses.error:
|
||||
pass
|
||||
self.stdscr.refresh()
|
||||
|
||||
def close(self):
|
||||
|
||||
|
||||
def refresh(self):
|
||||
"""
|
||||
Clean up the curses stuff on exit.
|
||||
Refreshes the screen.
|
||||
Updates the lines based on the`:attr:lines` based on the `:attr:display_lines_offset`
|
||||
attribute and the status bars.
|
||||
"""
|
||||
curses.nocbreak()
|
||||
self.stdscr.keypad(0)
|
||||
curses.echo()
|
||||
curses.endwin()
|
||||
self.stdscr.clear()
|
||||
|
||||
# Update the status bars
|
||||
self.add_string(0, self.statusbars.topbar)
|
||||
self.add_string(self.rows - 2, self.statusbars.bottombar)
|
||||
|
||||
# The number of rows minus the status bars and the input line
|
||||
available_lines = self.rows - 3
|
||||
# If the amount of lines exceeds the number of rows, we need to figure out
|
||||
# which ones to display based on the offset
|
||||
if len(self.lines) > available_lines:
|
||||
# Get the lines to display based on the offset
|
||||
offset = len(self.lines) - self.display_lines_offset
|
||||
lines = self.lines[-(available_lines - offset):offset]
|
||||
elif len(self.lines) == available_lines:
|
||||
lines = self.lines
|
||||
else:
|
||||
lines = [""] * (available_lines - len(self.lines))
|
||||
lines.extend(self.lines)
|
||||
|
||||
# Add the lines to the screen
|
||||
for index, line in enumerate(lines):
|
||||
self.add_string(index + 1, line)
|
||||
|
||||
# Add the input string
|
||||
self.add_string(self.rows - 1, self.input)
|
||||
|
||||
# Move the cursor
|
||||
try:
|
||||
self.stdscr.move(self.rows - 1, self.input_cursor)
|
||||
except curses.error:
|
||||
pass
|
||||
self.stdscr.redrawwin()
|
||||
self.stdscr.refresh()
|
||||
|
||||
|
||||
def add_line(self, text, refresh=True):
|
||||
"""
|
||||
Add a line to the screen. This will be showed between the two bars.
|
||||
The text can be formatted with color using the following format:
|
||||
|
||||
"{!fg, bg, attributes, ...!}"
|
||||
|
||||
See: http://docs.python.org/library/curses.html#constants for attributes.
|
||||
|
||||
Alternatively, it can use some built-in scheme for coloring.
|
||||
See colors.py for built-in schemes.
|
||||
|
||||
"{!scheme!}"
|
||||
|
||||
Examples:
|
||||
|
||||
"{!blue, black, bold!}My Text is {!white, black!}cool"
|
||||
"{!info!}I am some info text!"
|
||||
"{!error!}Uh oh!"
|
||||
|
||||
:param text: the text to show
|
||||
:type text: string
|
||||
:param refresh: if True, the screen will refresh after the line is added
|
||||
:type refresh: bool
|
||||
|
||||
"""
|
||||
|
||||
def get_line_chunks(line):
|
||||
"""
|
||||
Returns a list of 2-tuples (color string, text)
|
||||
|
||||
"""
|
||||
chunks = []
|
||||
num_chunks = line.count("{!")
|
||||
for i in range(num_chunks):
|
||||
# Find the beginning and end of the color tag
|
||||
beg = line.find("{!")
|
||||
end = line.find("!}") + 2
|
||||
color = line[beg:end]
|
||||
line = line[end:]
|
||||
|
||||
# Check to see if this is the last chunk
|
||||
if i + 1 == num_chunks:
|
||||
text = line
|
||||
else:
|
||||
# Not the last chunk so get the text up to the next tag
|
||||
# and remove the text from line
|
||||
text = line[:line.find("{!")]
|
||||
line = line[line.find("{!"):]
|
||||
|
||||
chunks.append((color, text))
|
||||
|
||||
return chunks
|
||||
|
||||
for line in text.splitlines():
|
||||
# We need to check for line lengths here and split as necessary
|
||||
try:
|
||||
line_length = colors.get_line_length(line)
|
||||
except colors.BadColorString:
|
||||
log.error("Passed a bad colored string..")
|
||||
line_length = len(line)
|
||||
|
||||
if line_length >= (self.cols - 1):
|
||||
s = ""
|
||||
# The length of the text without the color tags
|
||||
s_len = 0
|
||||
# We need to split this over multiple lines
|
||||
for chunk in get_line_chunks(line):
|
||||
if (len(chunk[1]) + s_len) < (self.cols - 1):
|
||||
# This chunk plus the current string in 's' isn't over
|
||||
# the maximum width, so just append the color tag and text
|
||||
s += chunk[0] + chunk[1]
|
||||
s_len += len(chunk[1])
|
||||
else:
|
||||
# The chunk plus the current string in 's' is too long.
|
||||
# We need to take as much of the chunk and put it into 's'
|
||||
# with the color tag.
|
||||
remain = (self.cols - 1) - s_len
|
||||
s += chunk[0] + chunk[1][:remain]
|
||||
# We append the line since it's full
|
||||
self.lines.append(s)
|
||||
# Start a new 's' with the remainder chunk
|
||||
s = chunk[0] + chunk[1][remain:]
|
||||
s_len = len(chunk[1][remain:])
|
||||
# Append the final string which may or may not be the full width
|
||||
if s:
|
||||
self.lines.append(s)
|
||||
else:
|
||||
self.lines.append(line)
|
||||
|
||||
while len(self.lines) > LINES_BUFFER_SIZE:
|
||||
# Remove the oldest line if the max buffer size has been reached
|
||||
del self.lines[0]
|
||||
|
||||
if refresh:
|
||||
self.refresh()
|
||||
|
||||
|
||||
def add_string(self, row, string):
|
||||
"""
|
||||
Adds a string to the desired `:param:row`.
|
||||
|
||||
:param row: int, the row number to write the string
|
||||
|
||||
"""
|
||||
col = 0
|
||||
try:
|
||||
parsed = colors.parse_color_string(string, self.encoding)
|
||||
except colors.BadColorString, e:
|
||||
log.error("Cannot add bad color string %s: %s", string, e)
|
||||
return
|
||||
|
||||
for index, (color, s) in enumerate(parsed):
|
||||
if index + 1 == len(parsed):
|
||||
# This is the last string so lets append some " " to it
|
||||
s += " " * (self.cols - (col + len(s)) - 1)
|
||||
try:
|
||||
self.stdscr.addstr(row, col, s, color)
|
||||
except curses.error:
|
||||
pass
|
||||
|
||||
col += len(s)
|
||||
|
||||
|
||||
def do_command(self, cmd):
|
||||
"""
|
||||
Processes a command.
|
||||
|
||||
:param cmd: str, the command string
|
||||
|
||||
"""
|
||||
if not cmd:
|
||||
return
|
||||
cmd, _, line = cmd.partition(' ')
|
||||
try:
|
||||
parser = self.console._commands[cmd].create_parser()
|
||||
except KeyError:
|
||||
self.write("{!error!}Unknown command: %s" % cmd)
|
||||
return
|
||||
args = self.console._commands[cmd].split(line)
|
||||
|
||||
# Do a little hack here to print 'command --help' properly
|
||||
parser._print_help = parser.print_help
|
||||
def print_help(f=None):
|
||||
parser._print_help(f)
|
||||
parser.print_help = print_help
|
||||
|
||||
# Only these commands can be run when not connected to a daemon
|
||||
not_connected_cmds = ["help", "connect", "quit"]
|
||||
aliases = []
|
||||
for c in not_connected_cmds:
|
||||
aliases.extend(self.console._commands[c].aliases)
|
||||
not_connected_cmds.extend(aliases)
|
||||
|
||||
if not client.connected() and cmd not in not_connected_cmds:
|
||||
self.write("{!error!}Not connected to a daemon, please use the connect command first.")
|
||||
return
|
||||
|
||||
try:
|
||||
options, args = parser.parse_args(args)
|
||||
except Exception, e:
|
||||
self.write("{!error!}Error parsing options: %s" % e)
|
||||
return
|
||||
|
||||
if not getattr(options, '_exit', False):
|
||||
try:
|
||||
ret = self.console._commands[cmd].handle(*args, **options.__dict__)
|
||||
except Exception, e:
|
||||
self.write("{!error!}" + str(e))
|
||||
log.exception(e)
|
||||
import traceback
|
||||
self.write("%s" % traceback.format_exc())
|
||||
return defer.succeed(True)
|
||||
else:
|
||||
return ret
|
||||
|
||||
|
||||
|
||||
def set_batch_write(self, batch):
|
||||
"""
|
||||
When this is set the screen is not refreshed after a `:meth:write` until
|
||||
this is set to False.
|
||||
|
||||
:param batch: set True to prevent screen refreshes after a `:meth:write`
|
||||
:type batch: bool
|
||||
|
||||
"""
|
||||
self.batch_write = batch
|
||||
if not batch:
|
||||
self.refresh()
|
||||
|
||||
def write(self, line):
|
||||
"""
|
||||
Writes a line out
|
||||
|
||||
:param line: str, the line to print
|
||||
|
||||
"""
|
||||
self.add_line(line, not self.batch_write)
|
||||
|
||||
|
||||
def tab_completer(self, line, cursor, second_hit):
|
||||
"""
|
||||
Called when the user hits 'tab' and will autocomplete or show options.
|
||||
If a command is already supplied in the line, this function will call the
|
||||
complete method of the command.
|
||||
|
||||
:param line: str, the current input string
|
||||
:param cursor: int, the cursor position in the line
|
||||
:param second_hit: bool, if this is the second time in a row the tab key
|
||||
has been pressed
|
||||
|
||||
:returns: 2-tuple (string, cursor position)
|
||||
|
||||
"""
|
||||
# First check to see if there is no space, this will mean that it's a
|
||||
# command that needs to be completed.
|
||||
if " " not in line:
|
||||
possible_matches = []
|
||||
# Iterate through the commands looking for ones that startwith the
|
||||
# line.
|
||||
for cmd in self.console._commands:
|
||||
if cmd.startswith(line):
|
||||
possible_matches.append(cmd + " ")
|
||||
|
||||
line_prefix = ""
|
||||
else:
|
||||
cmd = line.split(" ")[0]
|
||||
if cmd in self.console._commands:
|
||||
# Call the command's complete method to get 'er done
|
||||
possible_matches = self.console._commands[cmd].complete(line.split(" ")[-1])
|
||||
line_prefix = " ".join(line.split(" ")[:-1]) + " "
|
||||
else:
|
||||
# This is a bogus command
|
||||
return (line, cursor)
|
||||
|
||||
# No matches, so just return what we got passed
|
||||
if len(possible_matches) == 0:
|
||||
return (line, cursor)
|
||||
# If we only have 1 possible match, then just modify the line and
|
||||
# return it, else we need to print out the matches without modifying
|
||||
# the line.
|
||||
elif len(possible_matches) == 1:
|
||||
new_line = line_prefix + possible_matches[0]
|
||||
return (new_line, len(new_line))
|
||||
else:
|
||||
if second_hit:
|
||||
# Only print these out if it's a second_hit
|
||||
self.write(" ")
|
||||
for match in possible_matches:
|
||||
self.write(match)
|
||||
else:
|
||||
p = " ".join(line.split(" ")[:-1])
|
||||
new_line = " ".join([p, os.path.commonprefix(possible_matches)])
|
||||
if len(new_line) > len(line):
|
||||
line = new_line
|
||||
cursor = len(line)
|
||||
return (line, cursor)
|
||||
|
||||
|
||||
def tab_complete_torrent(self, line):
|
||||
"""
|
||||
Completes torrent_ids or names.
|
||||
|
||||
:param line: str, the string to complete
|
||||
|
||||
:returns: list of matches
|
||||
|
||||
"""
|
||||
possible_matches = []
|
||||
|
||||
# Find all possible matches
|
||||
for torrent_id, torrent_name in self.torrents:
|
||||
if torrent_id.startswith(line):
|
||||
possible_matches.append(torrent_id + " ")
|
||||
if torrent_name.startswith(line):
|
||||
possible_matches.append(torrent_name + " ")
|
||||
|
||||
return possible_matches
|
||||
|
||||
def get_torrent_name(self, torrent_id):
|
||||
"""
|
||||
Gets a torrent name from the torrents list.
|
||||
|
||||
:param torrent_id: str, the torrent_id
|
||||
|
||||
:returns: the name of the torrent or None
|
||||
"""
|
||||
|
||||
for tid, name in self.torrents:
|
||||
if torrent_id == tid:
|
||||
return name
|
||||
|
||||
return None
|
||||
|
||||
def match_torrent(self, string):
|
||||
"""
|
||||
Returns a list of torrent_id matches for the string. It will search both
|
||||
torrent_ids and torrent names, but will only return torrent_ids.
|
||||
|
||||
:param string: str, the string to match on
|
||||
|
||||
:returns: list of matching torrent_ids. Will return an empty list if
|
||||
no matches are found.
|
||||
|
||||
"""
|
||||
ret = []
|
||||
for tid, name in self.torrents:
|
||||
if tid.startswith(string) or name.startswith(string):
|
||||
ret.append(tid)
|
||||
|
||||
return ret
|
||||
|
||||
def on_torrent_added_event(self, event):
|
||||
def on_torrent_status(status):
|
||||
self.torrents.append((event.torrent_id, status["name"]))
|
||||
client.core.get_torrent_status(event.torrent_id, ["name"]).addCallback(on_torrent_status)
|
||||
|
||||
def on_torrent_removed_event(self, event):
|
||||
for index, (tid, name) in enumerate(self.torrents):
|
||||
if event.torrent_id == tid:
|
||||
del self.torrents[index]
|
||||
275
deluge/ui/console/modes/popup.py
Normal file
@ -0,0 +1,275 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# popup.py
|
||||
#
|
||||
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
#
|
||||
|
||||
try:
|
||||
import curses
|
||||
import signal
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import format_utils
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class Popup:
|
||||
def __init__(self,parent_mode,title,width_req=-1,height_req=-1,close_cb=None,init_lines=None):
|
||||
"""
|
||||
Init a new popup. The default constructor will handle sizing and borders and the like.
|
||||
|
||||
NB: The parent mode is responsible for calling refresh on any popups it wants to show.
|
||||
This should be called as the last thing in the parents refresh method.
|
||||
|
||||
The parent *must* also call _doRead on the popup instead of/in addition to
|
||||
running its own _doRead code if it wants to have the popup handle user input.
|
||||
|
||||
:param parent_mode: must be a basemode (or subclass) which the popup will be drawn over
|
||||
:parem title: string, the title of the popup window
|
||||
|
||||
Popups have two methods that must be implemented:
|
||||
|
||||
refresh(self) - draw the popup window to screen. this default mode simply draws a bordered window
|
||||
with the supplied title to the screen
|
||||
|
||||
add_string(self, row, string) - add string at row. handles triming/ignoring if the string won't fit in the popup
|
||||
|
||||
_doRead(self) - handle user input to the popup.
|
||||
"""
|
||||
self.parent = parent_mode
|
||||
|
||||
if (height_req <= 0):
|
||||
height_req = int(self.parent.rows/2)
|
||||
if (width_req <= 0):
|
||||
width_req = int(self.parent.cols/2)
|
||||
by = (self.parent.rows/2)-(height_req/2)
|
||||
bx = (self.parent.cols/2)-(width_req/2)
|
||||
self.screen = curses.newwin(height_req,width_req,by,bx)
|
||||
|
||||
self.title = title
|
||||
self.close_cb = close_cb
|
||||
self.height,self.width = self.screen.getmaxyx()
|
||||
self.divider = None
|
||||
self.lineoff = 0
|
||||
if init_lines:
|
||||
self._lines = init_lines
|
||||
else:
|
||||
self._lines = []
|
||||
|
||||
def _refresh_lines(self):
|
||||
crow = 1
|
||||
for line in self._lines[self.lineoff:]:
|
||||
if (crow >= self.height-1):
|
||||
break
|
||||
self.parent.add_string(crow,line,self.screen,1,False,True)
|
||||
crow+=1
|
||||
|
||||
def handle_resize(self):
|
||||
log.debug("Resizing popup window (actually, just creating a new one)")
|
||||
self.screen = curses.newwin((self.parent.rows/2),(self.parent.cols/2),(self.parent.rows/4),(self.parent.cols/4))
|
||||
self.height,self.width = self.screen.getmaxyx()
|
||||
|
||||
|
||||
def refresh(self):
|
||||
self.screen.clear()
|
||||
self.screen.border(0,0,0,0)
|
||||
toff = max(1,int((self.parent.cols/4)-(len(self.title)/2)))
|
||||
self.parent.add_string(0,"{!white,black,bold!}%s"%self.title,self.screen,toff,False,True)
|
||||
|
||||
self._refresh_lines()
|
||||
if (len(self._lines) > (self.height-2)):
|
||||
lts = len(self._lines)-(self.height-3)
|
||||
perc_sc = float(self.lineoff)/lts
|
||||
sb_pos = int((self.height-2)*perc_sc)+1
|
||||
if (sb_pos == 1) and (self.lineoff != 0):
|
||||
sb_pos += 1
|
||||
self.parent.add_string(sb_pos, "{!white,black,bold!}|",self.screen,col=(self.width-1),pad=False,trim=False)
|
||||
|
||||
self.screen.redrawwin()
|
||||
self.screen.noutrefresh()
|
||||
|
||||
def clear(self):
|
||||
self._lines = []
|
||||
|
||||
def handle_read(self, c):
|
||||
if c == curses.KEY_UP:
|
||||
self.lineoff = max(0,self.lineoff -1)
|
||||
elif c == curses.KEY_DOWN:
|
||||
if len(self._lines)-self.lineoff > (self.height-2):
|
||||
self.lineoff += 1
|
||||
|
||||
elif c == curses.KEY_ENTER or c == 10 or c == 27: # close on enter/esc
|
||||
if self.close_cb:
|
||||
self.close_cb()
|
||||
return True # close the popup
|
||||
|
||||
if c > 31 and c < 256 and chr(c) == 'q':
|
||||
if self.close_cb:
|
||||
self.close_cb()
|
||||
return True # close the popup
|
||||
|
||||
self.refresh()
|
||||
|
||||
return False
|
||||
|
||||
def set_title(self, title):
|
||||
self.title = title
|
||||
|
||||
def add_line(self, string):
|
||||
self._lines.append(string)
|
||||
|
||||
def add_divider(self):
|
||||
if not self.divider:
|
||||
self.divider = "-"*(self.width-2)
|
||||
self._lines.append(self.divider)
|
||||
|
||||
|
||||
class SelectablePopup(Popup):
|
||||
"""
|
||||
A popup which will let the user select from some of the lines that
|
||||
are added.
|
||||
"""
|
||||
def __init__(self,parent_mode,title,selection_callback,*args):
|
||||
Popup.__init__(self,parent_mode,title)
|
||||
self._selection_callback = selection_callback
|
||||
self._selection_args = args
|
||||
self._selectable_lines = []
|
||||
self._select_data = []
|
||||
self._line_foregrounds = []
|
||||
self._udxs = {}
|
||||
self._hotkeys = {}
|
||||
self._selected = -1
|
||||
|
||||
def add_line(self, string, selectable=True, use_underline=True, data=None, foreground=None):
|
||||
if use_underline:
|
||||
udx = string.find('_')
|
||||
if udx >= 0:
|
||||
string = string[:udx]+string[udx+1:]
|
||||
self._udxs[len(self._lines)+1] = udx
|
||||
c = string[udx].lower()
|
||||
self._hotkeys[c] = len(self._lines)
|
||||
Popup.add_line(self,string)
|
||||
self._line_foregrounds.append(foreground)
|
||||
if selectable:
|
||||
self._selectable_lines.append(len(self._lines)-1)
|
||||
self._select_data.append(data)
|
||||
if self._selected < 0:
|
||||
self._selected = (len(self._lines)-1)
|
||||
|
||||
def _refresh_lines(self):
|
||||
crow = 1
|
||||
for row,line in enumerate(self._lines):
|
||||
if (crow >= self.height-1):
|
||||
break
|
||||
if (row < self.lineoff):
|
||||
continue
|
||||
fg = self._line_foregrounds[row]
|
||||
udx = self._udxs.get(crow)
|
||||
if row == self._selected:
|
||||
if fg == None: fg = "black"
|
||||
colorstr = "{!%s,white,bold!}"%fg
|
||||
if udx >= 0:
|
||||
ustr = "{!%s,white,bold,underline!}"%fg
|
||||
else:
|
||||
if fg == None: fg = "white"
|
||||
colorstr = "{!%s,black!}"%fg
|
||||
if udx >= 0:
|
||||
ustr = "{!%s,black,underline!}"%fg
|
||||
if udx == 0:
|
||||
self.parent.add_string(crow,"- %s%c%s%s"%(ustr,line[0],colorstr,line[1:]),self.screen,1,False,True)
|
||||
elif udx > 0:
|
||||
# well, this is a litte gross
|
||||
self.parent.add_string(crow,"- %s%s%s%c%s%s"%(colorstr,line[:udx],ustr,line[udx],colorstr,line[udx+1:]),self.screen,1,False,True)
|
||||
else:
|
||||
self.parent.add_string(crow,"- %s%s"%(colorstr,line),self.screen,1,False,True)
|
||||
crow+=1
|
||||
|
||||
def current_selection(self):
|
||||
"Returns a tuple of (selected index, selected data)"
|
||||
idx = self._selectable_lines.index(self._selected)
|
||||
return (idx,self._select_data[idx])
|
||||
|
||||
def add_divider(self,color="white"):
|
||||
if not self.divider:
|
||||
self.divider = "-"*(self.width-6)+" -"
|
||||
self._lines.append(self.divider)
|
||||
self._line_foregrounds.append(color)
|
||||
|
||||
def handle_read(self, c):
|
||||
if c == curses.KEY_UP:
|
||||
#self.lineoff = max(0,self.lineoff -1)
|
||||
if (self._selected != self._selectable_lines[0] and
|
||||
len(self._selectable_lines) > 1):
|
||||
idx = self._selectable_lines.index(self._selected)
|
||||
self._selected = self._selectable_lines[idx-1]
|
||||
elif c == curses.KEY_DOWN:
|
||||
#if len(self._lines)-self.lineoff > (self.height-2):
|
||||
# self.lineoff += 1
|
||||
idx = self._selectable_lines.index(self._selected)
|
||||
if (idx < len(self._selectable_lines)-1):
|
||||
self._selected = self._selectable_lines[idx+1]
|
||||
elif c == 27: # close on esc, no action
|
||||
return True
|
||||
elif c == curses.KEY_ENTER or c == 10:
|
||||
idx = self._selectable_lines.index(self._selected)
|
||||
return self._selection_callback(idx,self._select_data[idx],*self._selection_args)
|
||||
if c > 31 and c < 256:
|
||||
if chr(c) == 'q':
|
||||
return True # close the popup
|
||||
uc = chr(c).lower()
|
||||
if uc in self._hotkeys:
|
||||
# exec hotkey action
|
||||
idx = self._selectable_lines.index(self._hotkeys[uc])
|
||||
return self._selection_callback(idx,self._select_data[idx],*self._selection_args)
|
||||
self.refresh()
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class MessagePopup(Popup):
|
||||
"""
|
||||
Popup that just displays a message
|
||||
"""
|
||||
def __init__(self, parent_mode, title, message):
|
||||
self.message = message
|
||||
self.width= int(parent_mode.cols/2)
|
||||
lns = format_utils.wrap_string(self.message,self.width-2,3,True)
|
||||
hr = min(len(lns)+2,int(parent_mode.rows/2))
|
||||
Popup.__init__(self,parent_mode,title,height_req=hr)
|
||||
self._lines = lns
|
||||
|
||||
def handle_resize(self):
|
||||
Popup.handle_resize(self)
|
||||
self.clear()
|
||||
self._lines = self._split_message()
|
||||
396
deluge/ui/console/modes/preference_panes.py
Normal file
@ -0,0 +1,396 @@
|
||||
#
|
||||
# preference_panes.py
|
||||
#
|
||||
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
# You may redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License, as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# deluge is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with deluge. If not, write to:
|
||||
# The Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# In addition, as a special exception, the copyright holders give
|
||||
# permission to link the code of portions of this program with the OpenSSL
|
||||
# library.
|
||||
# You must obey the GNU General Public License in all respects for all of
|
||||
# the code used other than OpenSSL. If you modify file(s) with this
|
||||
# exception, you may extend this exception to your version of the file(s),
|
||||
# but you are not obligated to do so. If you do not wish to do so, delete
|
||||
# this exception statement from your version. If you delete this exception
|
||||
# statement from all source files in the program, then also delete it here.
|
||||
#
|
||||
#
|
||||
|
||||
from deluge.ui.console.modes.input_popup import TextInput,SelectInput,CheckedInput,IntSpinInput,FloatSpinInput,CheckedPlusInput
|
||||
import deluge.ui.console.modes.alltorrents
|
||||
|
||||
try:
|
||||
import curses
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NoInput:
|
||||
def depend_skip(self):
|
||||
return False
|
||||
|
||||
class Header(NoInput):
|
||||
def __init__(self, parent, header, space_above, space_below):
|
||||
self.parent = parent
|
||||
self.header = "{!white,black,bold!}%s"%header
|
||||
self.space_above = space_above
|
||||
self.space_below = space_below
|
||||
self.name = header
|
||||
|
||||
def render(self, screen, row, width, active, offset):
|
||||
rows = 1
|
||||
if self.space_above:
|
||||
row += 1
|
||||
rows += 1
|
||||
self.parent.add_string(row,self.header,screen,offset-1,False,True)
|
||||
if self.space_below: rows += 1
|
||||
return rows
|
||||
|
||||
class InfoField(NoInput):
|
||||
def __init__(self,parent,label,value,name):
|
||||
self.parent = parent
|
||||
self.label = label
|
||||
self.value = value
|
||||
self.txt = "%s %s"%(label,value)
|
||||
self.name = name
|
||||
|
||||
def render(self, screen, row, width, active, offset):
|
||||
self.parent.add_string(row,self.txt,screen,offset-1,False,True)
|
||||
return 1
|
||||
|
||||
def set_value(self, v):
|
||||
self.value = v
|
||||
if type(v) == float:
|
||||
self.txt = "%s %.2f"%(self.label,self.value)
|
||||
else:
|
||||
self.txt = "%s %s"%(self.label,self.value)
|
||||
|
||||
class BasePane:
|
||||
def __init__(self, offset, parent, width):
|
||||
self.offset = offset+1
|
||||
self.parent = parent
|
||||
self.width = width
|
||||
self.inputs = []
|
||||
self.active_input = -1
|
||||
|
||||
# have we scrolled down in the list
|
||||
self.input_offset = 0
|
||||
|
||||
def move(self,r,c):
|
||||
self._cursor_row = r
|
||||
self._cursor_col = c
|
||||
|
||||
def add_config_values(self,conf_dict):
|
||||
for ipt in self.inputs:
|
||||
if not isinstance(ipt,NoInput):
|
||||
# gross, have to special case in/out ports since they are tuples
|
||||
if ipt.name in ("listen_ports_to","listen_ports_from",
|
||||
"out_ports_from","out_ports_to"):
|
||||
if ipt.name == "listen_ports_to":
|
||||
conf_dict["listen_ports"] = (self.infrom.get_value(),self.into.get_value())
|
||||
if ipt.name == "out_ports_to":
|
||||
conf_dict["outgoing_ports"] = (self.outfrom.get_value(),self.outto.get_value())
|
||||
else:
|
||||
conf_dict[ipt.name] = ipt.get_value()
|
||||
if hasattr(ipt,"get_child"):
|
||||
c = ipt.get_child()
|
||||
conf_dict[c.name] = c.get_value()
|
||||
|
||||
def update_values(self, conf_dict):
|
||||
for ipt in self.inputs:
|
||||
if not isinstance(ipt,NoInput):
|
||||
try:
|
||||
ipt.set_value(conf_dict[ipt.name])
|
||||
except KeyError: # just ignore if it's not in dict
|
||||
pass
|
||||
if hasattr(ipt,"get_child"):
|
||||
try:
|
||||
c = ipt.get_child()
|
||||
c.set_value(conf_dict[c.name])
|
||||
except KeyError: # just ignore if it's not in dict
|
||||
pass
|
||||
|
||||
def render(self, mode, screen, width, active):
|
||||
self._cursor_row = -1
|
||||
if self.active_input < 0:
|
||||
for i,ipt in enumerate(self.inputs):
|
||||
if not isinstance(ipt,NoInput):
|
||||
self.active_input = i
|
||||
break
|
||||
drew_act = not active
|
||||
crow = 1
|
||||
for i,ipt in enumerate(self.inputs):
|
||||
if ipt.depend_skip() or i<self.input_offset:
|
||||
if active and i==self.active_input:
|
||||
self.input_offset-=1
|
||||
mode.refresh()
|
||||
return 0
|
||||
continue
|
||||
act = active and i==self.active_input
|
||||
if act: drew_act = True
|
||||
crow += ipt.render(screen,crow,width, act, self.offset)
|
||||
if crow >= (mode.prefs_height):
|
||||
break
|
||||
|
||||
if not drew_act:
|
||||
self.input_offset+=1
|
||||
mode.refresh()
|
||||
return 0
|
||||
|
||||
if active and self._cursor_row >= 0:
|
||||
curses.curs_set(2)
|
||||
screen.move(self._cursor_row,self._cursor_col+self.offset-1)
|
||||
else:
|
||||
curses.curs_set(0)
|
||||
|
||||
return crow
|
||||
|
||||
# just handles setting the active input
|
||||
def handle_read(self,c):
|
||||
if not self.inputs: # no inputs added yet
|
||||
return
|
||||
|
||||
if c == curses.KEY_UP:
|
||||
nc = max(0,self.active_input-1)
|
||||
while isinstance(self.inputs[nc], NoInput) or self.inputs[nc].depend_skip():
|
||||
nc-=1
|
||||
if nc <= 0: break
|
||||
if not isinstance(self.inputs[nc], NoInput) or self.inputs[nc].depend_skip():
|
||||
self.active_input = nc
|
||||
elif c == curses.KEY_DOWN:
|
||||
ilen = len(self.inputs)
|
||||
nc = min(self.active_input+1,ilen-1)
|
||||
while isinstance(self.inputs[nc], NoInput) or self.inputs[nc].depend_skip():
|
||||
nc+=1
|
||||
if nc >= ilen:
|
||||
nc-=1
|
||||
break
|
||||
if not isinstance(self.inputs[nc], NoInput) or self.inputs[nc].depend_skip():
|
||||
self.active_input = nc
|
||||
else:
|
||||
self.inputs[self.active_input].handle_read(c)
|
||||
|
||||
|
||||
def add_header(self, header, space_above=False, space_below=False):
|
||||
self.inputs.append(Header(self.parent, header, space_above, space_below))
|
||||
|
||||
def add_info_field(self, label, value, name):
|
||||
self.inputs.append(InfoField(self.parent, label, value, name))
|
||||
|
||||
def add_text_input(self, name, msg, dflt_val):
|
||||
self.inputs.append(TextInput(self.parent,self.move,self.width,msg,name,dflt_val,False))
|
||||
|
||||
def add_select_input(self, name, msg, opts, vals, selidx):
|
||||
self.inputs.append(SelectInput(self.parent,msg,name,opts,vals,selidx))
|
||||
|
||||
def add_checked_input(self, name, message, checked):
|
||||
self.inputs.append(CheckedInput(self.parent,message,name,checked))
|
||||
|
||||
def add_checkedplus_input(self, name, message, child, checked):
|
||||
self.inputs.append(CheckedPlusInput(self.parent,message,name,child,checked))
|
||||
|
||||
def add_int_spin_input(self, name, message, value, min_val, max_val):
|
||||
self.inputs.append(IntSpinInput(self.parent,message,name,self.move,value,min_val,max_val))
|
||||
|
||||
def add_float_spin_input(self, name, message, value, inc_amt, precision, min_val, max_val):
|
||||
self.inputs.append(FloatSpinInput(self.parent,message,name,self.move,value,inc_amt,precision,min_val,max_val))
|
||||
|
||||
|
||||
class DownloadsPane(BasePane):
|
||||
def __init__(self, offset, parent, width):
|
||||
BasePane.__init__(self,offset,parent,width)
|
||||
|
||||
self.add_header("Folders")
|
||||
self.add_text_input("download_location","Download To:",parent.core_config["download_location"])
|
||||
cmptxt = TextInput(self.parent,self.move,self.width,None,"move_completed_path",parent.core_config["move_completed_path"],False)
|
||||
self.add_checkedplus_input("move_completed","Move completed to:",cmptxt,parent.core_config["move_completed"])
|
||||
autotxt = TextInput(self.parent,self.move,self.width,None,"autoadd_location",parent.core_config["autoadd_location"],False)
|
||||
self.add_checkedplus_input("autoadd_enable","Auto add .torrents from:",autotxt,parent.core_config["autoadd_enable"])
|
||||
copytxt = TextInput(self.parent,self.move,self.width,None,"torrentfiles_location",parent.core_config["torrentfiles_location"],False)
|
||||
self.add_checkedplus_input("copy_torrent_file","Copy of .torrent files to:",copytxt,parent.core_config["copy_torrent_file"])
|
||||
self.add_checked_input("del_copy_torrent_file","Delete copy of torrent file on remove",parent.core_config["del_copy_torrent_file"])
|
||||
|
||||
self.add_header("Allocation",True)
|
||||
|
||||
if parent.core_config["compact_allocation"]:
|
||||
alloc_idx = 1
|
||||
else:
|
||||
alloc_idx = 0
|
||||
self.add_select_input("compact_allocation",None,["Use Full Allocation","Use Compact Allocation"],[False,True],alloc_idx)
|
||||
self.add_header("Options",True)
|
||||
self.add_checked_input("prioritize_first_last_pieces","Prioritize first and last pieces of torrent",parent.core_config["prioritize_first_last_pieces"])
|
||||
self.add_checked_input("add_paused","Add torrents in paused state",parent.core_config["add_paused"])
|
||||
|
||||
|
||||
class NetworkPane(BasePane):
|
||||
def __init__(self, offset, parent, width):
|
||||
BasePane.__init__(self,offset,parent,width)
|
||||
self.add_header("Incomming Ports")
|
||||
inrand = CheckedInput(parent,"Use Random Ports Active Port: %d"%parent.active_port,"random_port",parent.core_config["random_port"])
|
||||
self.inputs.append(inrand)
|
||||
listen_ports = parent.core_config["listen_ports"]
|
||||
self.infrom = IntSpinInput(self.parent," From:","listen_ports_from",self.move,listen_ports[0],0,65535)
|
||||
self.infrom.set_depend(inrand,True)
|
||||
self.into = IntSpinInput(self.parent," To: ","listen_ports_to",self.move,listen_ports[1],0,65535)
|
||||
self.into.set_depend(inrand,True)
|
||||
self.inputs.append(self.infrom)
|
||||
self.inputs.append(self.into)
|
||||
|
||||
|
||||
self.add_header("Outgoing Ports",True)
|
||||
outrand = CheckedInput(parent,"Use Random Ports","random_outgoing_ports",parent.core_config["random_outgoing_ports"])
|
||||
self.inputs.append(outrand)
|
||||
out_ports = parent.core_config["outgoing_ports"]
|
||||
self.outfrom = IntSpinInput(self.parent," From:","out_ports_from",self.move,out_ports[0],0,65535)
|
||||
self.outfrom.set_depend(outrand,True)
|
||||
self.outto = IntSpinInput(self.parent," To: ","out_ports_to",self.move,out_ports[1],0,65535)
|
||||
self.outto.set_depend(outrand,True)
|
||||
self.inputs.append(self.outfrom)
|
||||
self.inputs.append(self.outto)
|
||||
|
||||
|
||||
self.add_header("Interface",True)
|
||||
self.add_text_input("listen_interface","IP address of the interface to listen on (leave empty for default):",parent.core_config["listen_interface"])
|
||||
|
||||
self.add_header("TOS",True)
|
||||
self.add_text_input("peer_tos","Peer TOS Byte:",parent.core_config["peer_tos"])
|
||||
|
||||
self.add_header("Network Extras")
|
||||
self.add_checked_input("upnp","UPnP",parent.core_config["upnp"])
|
||||
self.add_checked_input("natpmp","NAT-PMP",parent.core_config["natpmp"])
|
||||
self.add_checked_input("utpex","Peer Exchange",parent.core_config["utpex"])
|
||||
self.add_checked_input("lsd","LSD",parent.core_config["lsd"])
|
||||
self.add_checked_input("dht","DHT",parent.core_config["dht"])
|
||||
|
||||
self.add_header("Encryption",True)
|
||||
self.add_select_input("enc_in_policy","Inbound:",["Forced","Enabled","Disabled"],[0,1,2],parent.core_config["enc_in_policy"])
|
||||
self.add_select_input("enc_out_policy","Outbound:",["Forced","Enabled","Disabled"],[0,1,2],parent.core_config["enc_out_policy"])
|
||||
self.add_select_input("enc_level","Level:",["Handshake","Full Stream","Either"],[0,1,2],parent.core_config["enc_level"])
|
||||
self.add_checked_input("enc_prefer_rc4","Encrypt Entire Stream",parent.core_config["enc_prefer_rc4"])
|
||||
|
||||
|
||||
class BandwidthPane(BasePane):
|
||||
def __init__(self, offset, parent, width):
|
||||
BasePane.__init__(self,offset,parent,width)
|
||||
self.add_header("Global Bandwidth Usage")
|
||||
self.add_int_spin_input("max_connections_global","Maximum Connections:",parent.core_config["max_connections_global"],-1,9000)
|
||||
self.add_int_spin_input("max_upload_slots_global","Maximum Upload Slots:",parent.core_config["max_upload_slots_global"],-1,9000)
|
||||
self.add_float_spin_input("max_download_speed","Maximum Download Speed (KiB/s):",parent.core_config["max_download_speed"],1.0,1,-1.0,60000.0)
|
||||
self.add_float_spin_input("max_upload_speed","Maximum Upload Speed (KiB/s):",parent.core_config["max_upload_speed"],1.0,1,-1.0,60000.0)
|
||||
self.add_int_spin_input("max_half_open_connections","Maximum Half-Open Connections:",parent.core_config["max_half_open_connections"],-1,9999)
|
||||
self.add_int_spin_input("max_connections_per_second","Maximum Connection Attempts per Second:",parent.core_config["max_connections_per_second"],-1,9999)
|
||||
self.add_checked_input("ignore_limits_on_local_network","Ignore limits on local network",parent.core_config["ignore_limits_on_local_network"])
|
||||
self.add_checked_input("rate_limit_ip_overhead","Rate Limit IP Overhead",parent.core_config["rate_limit_ip_overhead"])
|
||||
self.add_header("Per Torrent Bandwidth Usage",True)
|
||||
self.add_int_spin_input("max_connections_per_torrent","Maximum Connections:",parent.core_config["max_connections_per_torrent"],-1,9000)
|
||||
self.add_int_spin_input("max_upload_slots_per_torrent","Maximum Upload Slots:",parent.core_config["max_upload_slots_per_torrent"],-1,9000)
|
||||
self.add_float_spin_input("max_download_speed_per_torrent","Maximum Download Speed (KiB/s):",parent.core_config["max_download_speed_per_torrent"],1.0,1,-1.0,60000.0)
|
||||
self.add_float_spin_input("max_upload_speed_per_torrent","Maximum Upload Speed (KiB/s):",parent.core_config["max_upload_speed_per_torrent"],1.0,1,-1.0,60000.0)
|
||||
|
||||
class InterfacePane(BasePane):
|
||||
def __init__(self, offset, parent, width):
|
||||
BasePane.__init__(self,offset,parent,width)
|
||||
self.add_header("Columns To Display")
|
||||
for cpn in deluge.ui.console.modes.alltorrents.column_pref_names:
|
||||
pn = "show_%s"%cpn
|
||||
self.add_checked_input(pn,
|
||||
deluge.ui.console.modes.alltorrents.prefs_to_names[cpn],
|
||||
parent.console_config[pn])
|
||||
self.add_header("Column Widths (-1 = expand)",True)
|
||||
for cpn in deluge.ui.console.modes.alltorrents.column_pref_names:
|
||||
pn = "%s_width"%cpn
|
||||
self.add_int_spin_input(pn,
|
||||
deluge.ui.console.modes.alltorrents.prefs_to_names[cpn],
|
||||
parent.console_config[pn],-1,100)
|
||||
|
||||
class OtherPane(BasePane):
|
||||
def __init__(self, offset, parent, width):
|
||||
BasePane.__init__(self,offset,parent,width)
|
||||
self.add_header("System Information")
|
||||
self.add_info_field(" Help us improve Deluge by sending us your","","")
|
||||
self.add_info_field(" Python version, PyGTK version, OS and processor","","")
|
||||
self.add_info_field(" types. Absolutely no other information is sent.","","")
|
||||
self.add_checked_input("send_info","Yes, please send anonymous statistics.",parent.core_config["send_info"])
|
||||
self.add_header("GeoIP Database",True)
|
||||
self.add_text_input("geoip_db_location","Location:",parent.core_config["geoip_db_location"])
|
||||
|
||||
class DaemonPane(BasePane):
|
||||
def __init__(self, offset, parent, width):
|
||||
BasePane.__init__(self,offset,parent,width)
|
||||
self.add_header("Port")
|
||||
self.add_int_spin_input("daemon_port","Daemon Port:",parent.core_config["daemon_port"],0,65535)
|
||||
self.add_header("Connections",True)
|
||||
self.add_checked_input("allow_remote","Allow remote connections",parent.core_config["allow_remote"])
|
||||
self.add_header("Other",True)
|
||||
self.add_checked_input("new_release_check","Periodically check the website for new releases",parent.core_config["new_release_check"])
|
||||
|
||||
class QueuePane(BasePane):
|
||||
def __init__(self, offset, parent, width):
|
||||
BasePane.__init__(self,offset,parent,width)
|
||||
self.add_header("General")
|
||||
self.add_checked_input("queue_new_to_top","Queue new torrents to top",parent.core_config["queue_new_to_top"])
|
||||
self.add_header("Active Torrents",True)
|
||||
self.add_int_spin_input("max_active_limit","Total active:",parent.core_config["max_active_limit"],-1,9999)
|
||||
self.add_int_spin_input("max_active_downloading","Total active downloading:",parent.core_config["max_active_downloading"],-1,9999)
|
||||
self.add_int_spin_input("max_active_seeding","Total active seeding:",parent.core_config["max_active_seeding"],-1,9999)
|
||||
self.add_checked_input("dont_count_slow_torrents","Do not count slow torrents",parent.core_config["dont_count_slow_torrents"])
|
||||
self.add_header("Seeding",True)
|
||||
self.add_float_spin_input("share_ratio_limit","Share Ratio Limit:",parent.core_config["share_ratio_limit"],1.0,2,-1.0,100.0)
|
||||
self.add_float_spin_input("seed_time_ratio_limit","Share Time Ratio:",parent.core_config["seed_time_ratio_limit"],1.0,2,-1.0,100.0)
|
||||
self.add_int_spin_input("seed_time_limit","Seed time (m):",parent.core_config["seed_time_limit"],-1,10000)
|
||||
seedratio = FloatSpinInput(self.parent,"","stop_seed_ratio",self.move,parent.core_config["stop_seed_ratio"],0.1,2,0.5,100.0)
|
||||
self.add_checkedplus_input("stop_seed_at_ratio","Stop seeding when share ratio reaches:",seedratio,parent.core_config["stop_seed_at_ratio"])
|
||||
self.add_checked_input("remove_seed_at_ratio","Remove torrent when share ratio reached",parent.core_config["remove_seed_at_ratio"])
|
||||
|
||||
class ProxyPane(BasePane):
|
||||
def __init__(self, offset, parent, width):
|
||||
BasePane.__init__(self,offset,parent,width)
|
||||
self.add_header("Proxy Settings Comming Soon")
|
||||
|
||||
class CachePane(BasePane):
|
||||
def __init__(self, offset, parent, width):
|
||||
BasePane.__init__(self,offset,parent,width)
|
||||
self.add_header("Settings")
|
||||
self.add_int_spin_input("cache_size","Cache Size (16 KiB blocks):",parent.core_config["cache_size"],0,99999)
|
||||
self.add_int_spin_input("cache_expiry","Cache Expiry (seconds):",parent.core_config["cache_expiry"],1,32000)
|
||||
self.add_header("Status (press 'r' to refresh status)",True)
|
||||
self.add_header(" Write")
|
||||
self.add_info_field(" Blocks Written:",self.parent.status["blocks_written"],"blocks_written")
|
||||
self.add_info_field(" Writes:",self.parent.status["writes"],"writes")
|
||||
self.add_info_field(" Write Cache Hit Ratio:","%.2f"%self.parent.status["write_hit_ratio"],"write_hit_ratio")
|
||||
self.add_header(" Read")
|
||||
self.add_info_field(" Blocks Read:",self.parent.status["blocks_read"],"blocks_read")
|
||||
self.add_info_field(" Blocks Read hit:",self.parent.status["blocks_read_hit"],"blocks_read_hit")
|
||||
self.add_info_field(" Reads:",self.parent.status["reads"],"reads")
|
||||
self.add_info_field(" Read Cache Hit Ratio:","%.2f"%self.parent.status["read_hit_ratio"],"read_hit_ratio")
|
||||
self.add_header(" Size")
|
||||
self.add_info_field(" Cache Size:",self.parent.status["cache_size"],"cache_size")
|
||||
self.add_info_field(" Read Cache Size:",self.parent.status["read_cache_size"],"read_cache_size")
|
||||
|
||||
def update_cache_status(self, status):
|
||||
for ipt in self.inputs:
|
||||
if isinstance(ipt,InfoField):
|
||||
try:
|
||||
ipt.set_value(status[ipt.name])
|
||||
except KeyError:
|
||||
pass
|
||||
310
deluge/ui/console/modes/preferences.py
Normal file
@ -0,0 +1,310 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# preferences.py
|
||||
#
|
||||
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||
#
|
||||
# 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 deluge.component as component
|
||||
from deluge.ui.client import client
|
||||
from basemode import BaseMode
|
||||
from input_popup import Popup,SelectInput
|
||||
|
||||
from preference_panes import DownloadsPane,NetworkPane,BandwidthPane,InterfacePane
|
||||
from preference_panes import OtherPane,DaemonPane,QueuePane,ProxyPane,CachePane
|
||||
|
||||
from collections import deque
|
||||
|
||||
try:
|
||||
import curses
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Big help string that gets displayed when the user hits 'h'
|
||||
HELP_STR = \
|
||||
"""This screen lets you view and configure various options
|
||||
in deluge.
|
||||
|
||||
There are three main sections to this screen. Only one
|
||||
section is active at a time. You can switch the active
|
||||
section by hitting TAB (or Shift-TAB to go back one)
|
||||
|
||||
The section on the left displays the various categories
|
||||
that the settings fall in. You can navigate the list
|
||||
using the up/down arrows
|
||||
|
||||
The section on the right shows the settings for the
|
||||
selected category. When this section is active
|
||||
you can navigate the various settings with the up/down
|
||||
arrows. Special keys for each input type are described
|
||||
below.
|
||||
|
||||
The final section is at the bottom right, the:
|
||||
[Cancel] [Apply] [OK] buttons. When this section
|
||||
is active, simply select the option you want using
|
||||
the arrow keys and press Enter to confim.
|
||||
|
||||
|
||||
Special keys for various input types are as follows:
|
||||
- For text inputs you can simply type in the value.
|
||||
|
||||
- For numeric inputs (indicated by the value being
|
||||
in []s), you can type a value, or use PageUp and
|
||||
PageDown to increment/decrement the value.
|
||||
|
||||
- For checkbox inputs use the spacebar to toggle
|
||||
|
||||
- For checkbox plus something else inputs (the
|
||||
something else being only visible when you
|
||||
check the box) you can toggle the check with
|
||||
space, use the right arrow to edit the other
|
||||
value, and escape to get back to the check box.
|
||||
|
||||
|
||||
"""
|
||||
HELP_LINES = HELP_STR.split('\n')
|
||||
|
||||
|
||||
class ZONE:
|
||||
CATEGORIES = 0
|
||||
PREFRENCES = 1
|
||||
ACTIONS = 2
|
||||
|
||||
class Preferences(BaseMode):
|
||||
def __init__(self, parent_mode, core_config, console_config, active_port, status, stdscr, encoding=None):
|
||||
self.parent_mode = parent_mode
|
||||
self.categories = [_("Downloads"), _("Network"), _("Bandwidth"),
|
||||
_("Interface"), _("Other"), _("Daemon"), _("Queue"), _("Proxy"),
|
||||
_("Cache")] # , _("Plugins")]
|
||||
self.cur_cat = 0
|
||||
self.popup = None
|
||||
self.messages = deque()
|
||||
self.action_input = None
|
||||
|
||||
self.core_config = core_config
|
||||
self.console_config = console_config
|
||||
self.active_port = active_port
|
||||
self.status = status
|
||||
|
||||
self.active_zone = ZONE.CATEGORIES
|
||||
|
||||
# how wide is the left 'pane' with categories
|
||||
self.div_off = 15
|
||||
|
||||
BaseMode.__init__(self, stdscr, encoding, False)
|
||||
|
||||
# create the panes
|
||||
self.prefs_width = self.cols-self.div_off-1
|
||||
self.prefs_height = self.rows-4
|
||||
self.panes = [
|
||||
DownloadsPane(self.div_off+2, self, self.prefs_width),
|
||||
NetworkPane(self.div_off+2, self, self.prefs_width),
|
||||
BandwidthPane(self.div_off+2, self, self.prefs_width),
|
||||
InterfacePane(self.div_off+2, self, self.prefs_width),
|
||||
OtherPane(self.div_off+2, self, self.prefs_width),
|
||||
DaemonPane(self.div_off+2, self, self.prefs_width),
|
||||
QueuePane(self.div_off+2, self, self.prefs_width),
|
||||
ProxyPane(self.div_off+2, self, self.prefs_width),
|
||||
CachePane(self.div_off+2, self, self.prefs_width)
|
||||
]
|
||||
|
||||
self.action_input = SelectInput(self,None,None,["Cancel","Apply","OK"],[0,1,2],0)
|
||||
self.refresh()
|
||||
|
||||
def __draw_catetories(self):
|
||||
for i,category in enumerate(self.categories):
|
||||
if i == self.cur_cat and self.active_zone == ZONE.CATEGORIES:
|
||||
self.add_string(i+1,"- {!black,white,bold!}%s"%category,pad=False)
|
||||
elif i == self.cur_cat:
|
||||
self.add_string(i+1,"- {!black,white!}%s"%category,pad=False)
|
||||
else:
|
||||
self.add_string(i+1,"- %s"%category)
|
||||
self.stdscr.vline(1,self.div_off,'|',self.rows-2)
|
||||
|
||||
def __draw_preferences(self):
|
||||
self.panes[self.cur_cat].render(self,self.stdscr, self.prefs_width, self.active_zone == ZONE.PREFRENCES)
|
||||
|
||||
def __draw_actions(self):
|
||||
selected = self.active_zone == ZONE.ACTIONS
|
||||
self.stdscr.hline(self.rows-3,self.div_off+1,"_",self.cols)
|
||||
self.action_input.render(self.stdscr,self.rows-2,self.cols,selected,self.cols-22)
|
||||
|
||||
def refresh(self):
|
||||
if self.popup == None and self.messages:
|
||||
title,msg = self.messages.popleft()
|
||||
self.popup = MessagePopup(self,title,msg)
|
||||
|
||||
self.stdscr.clear()
|
||||
self.add_string(0,self.statusbars.topbar)
|
||||
hstr = "%sPress [h] for help"%(" "*(self.cols - len(self.statusbars.bottombar) - 10))
|
||||
self.add_string(self.rows - 1, "%s%s"%(self.statusbars.bottombar,hstr))
|
||||
|
||||
self.__draw_catetories()
|
||||
self.__draw_actions()
|
||||
|
||||
# do this last since it moves the cursor
|
||||
self.__draw_preferences()
|
||||
|
||||
self.stdscr.noutrefresh()
|
||||
|
||||
if self.popup:
|
||||
self.popup.refresh()
|
||||
|
||||
curses.doupdate()
|
||||
|
||||
def __category_read(self, c):
|
||||
# Navigate prefs
|
||||
if c == curses.KEY_UP:
|
||||
self.cur_cat = max(0,self.cur_cat-1)
|
||||
elif c == curses.KEY_DOWN:
|
||||
self.cur_cat = min(len(self.categories)-1,self.cur_cat+1)
|
||||
|
||||
def __prefs_read(self, c):
|
||||
self.panes[self.cur_cat].handle_read(c)
|
||||
|
||||
def __apply_prefs(self):
|
||||
new_core_config = {}
|
||||
for pane in self.panes:
|
||||
if not isinstance(pane,InterfacePane):
|
||||
pane.add_config_values(new_core_config)
|
||||
# Apply Core Prefs
|
||||
if client.connected():
|
||||
# Only do this if we're connected to a daemon
|
||||
config_to_set = {}
|
||||
for key in new_core_config.keys():
|
||||
# The values do not match so this needs to be updated
|
||||
if self.core_config[key] != new_core_config[key]:
|
||||
config_to_set[key] = new_core_config[key]
|
||||
|
||||
if config_to_set:
|
||||
# Set each changed config value in the core
|
||||
client.core.set_config(config_to_set)
|
||||
client.force_call(True)
|
||||
# Update the configuration
|
||||
self.core_config.update(config_to_set)
|
||||
|
||||
# Update Interface Prefs
|
||||
new_console_config = {}
|
||||
didupdate = False
|
||||
for pane in self.panes:
|
||||
# could just access panes by index, but that would break if panes
|
||||
# are ever reordered, so do it the slightly slower but safer way
|
||||
if isinstance(pane,InterfacePane):
|
||||
pane.add_config_values(new_console_config)
|
||||
for key in new_console_config.keys():
|
||||
# The values do not match so this needs to be updated
|
||||
if self.console_config[key] != new_console_config[key]:
|
||||
self.console_config[key] = new_console_config[key]
|
||||
didupdate = True
|
||||
if didupdate:
|
||||
# changed something, save config and tell alltorrents
|
||||
self.console_config.save()
|
||||
self.parent_mode.update_config()
|
||||
|
||||
|
||||
def __update_preferences(self,core_config):
|
||||
self.core_config = core_config
|
||||
for pane in self.panes:
|
||||
pane.update_values(core_config)
|
||||
|
||||
def __actions_read(self, c):
|
||||
self.action_input.handle_read(c)
|
||||
if c == curses.KEY_ENTER or c == 10:
|
||||
# take action
|
||||
if self.action_input.selidx == 0: # cancel
|
||||
self.back_to_parent()
|
||||
elif self.action_input.selidx == 1: # apply
|
||||
self.__apply_prefs()
|
||||
client.core.get_config().addCallback(self.__update_preferences)
|
||||
elif self.action_input.selidx == 2: # OK
|
||||
self.__apply_prefs()
|
||||
self.back_to_parent()
|
||||
|
||||
|
||||
def back_to_parent(self):
|
||||
self.stdscr.clear()
|
||||
component.get("ConsoleUI").set_mode(self.parent_mode)
|
||||
self.parent_mode.resume()
|
||||
|
||||
def _doRead(self):
|
||||
c = self.stdscr.getch()
|
||||
|
||||
if self.popup:
|
||||
if self.popup.handle_read(c):
|
||||
self.popup = None
|
||||
self.refresh()
|
||||
return
|
||||
|
||||
if c > 31 and c < 256:
|
||||
if chr(c) == 'Q':
|
||||
from twisted.internet import reactor
|
||||
if client.connected():
|
||||
def on_disconnect(result):
|
||||
reactor.stop()
|
||||
client.disconnect().addCallback(on_disconnect)
|
||||
else:
|
||||
reactor.stop()
|
||||
return
|
||||
elif chr(c) == 'h':
|
||||
self.popup = Popup(self,"Preferences Help")
|
||||
for l in HELP_LINES:
|
||||
self.popup.add_line(l)
|
||||
|
||||
if c == 9:
|
||||
self.active_zone += 1
|
||||
if self.active_zone > ZONE.ACTIONS:
|
||||
self.active_zone = ZONE.CATEGORIES
|
||||
|
||||
elif c == curses.KEY_BTAB:
|
||||
self.active_zone -= 1
|
||||
if self.active_zone < ZONE.CATEGORIES:
|
||||
self.active_zone = ZONE.ACTIONS
|
||||
|
||||
elif c == 114 and isinstance(self.panes[self.cur_cat],CachePane):
|
||||
client.core.get_cache_status().addCallback(self.panes[self.cur_cat].update_cache_status)
|
||||
|
||||
else:
|
||||
if self.active_zone == ZONE.CATEGORIES:
|
||||
self.__category_read(c)
|
||||
elif self.active_zone == ZONE.PREFRENCES:
|
||||
self.__prefs_read(c)
|
||||
elif self.active_zone == ZONE.ACTIONS:
|
||||
self.__actions_read(c)
|
||||
|
||||
self.refresh()
|
||||
|
||||
|
||||
161
deluge/ui/console/modes/torrent_actions.py
Normal file
@ -0,0 +1,161 @@
|
||||
# torrent_actions.py
|
||||
#
|
||||
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
# You may redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License, as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# deluge is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with deluge. If not, write to:
|
||||
# The Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# In addition, as a special exception, the copyright holders give
|
||||
# permission to link the code of portions of this program with the OpenSSL
|
||||
# library.
|
||||
# You must obey the GNU General Public License in all respects for all of
|
||||
# the code used other than OpenSSL. If you modify file(s) with this
|
||||
# exception, you may extend this exception to your version of the file(s),
|
||||
# but you are not obligated to do so. If you do not wish to do so, delete
|
||||
# this exception statement from your version. If you delete this exception
|
||||
# statement from all source files in the program, then also delete it here.
|
||||
#
|
||||
#
|
||||
|
||||
from deluge.ui.client import client
|
||||
from popup import SelectablePopup
|
||||
from input_popup import InputPopup
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class ACTION:
|
||||
PAUSE=0
|
||||
RESUME=1
|
||||
REANNOUNCE=2
|
||||
EDIT_TRACKERS=3
|
||||
RECHECK=4
|
||||
REMOVE=5
|
||||
REMOVE_DATA=6
|
||||
REMOVE_NODATA=7
|
||||
DETAILS=8
|
||||
MOVE_STORAGE=9
|
||||
QUEUE=10
|
||||
QUEUE_TOP=11
|
||||
QUEUE_UP=12
|
||||
QUEUE_DOWN=13
|
||||
QUEUE_BOTTOM=14
|
||||
|
||||
def action_error(error,mode):
|
||||
rerr = error.value
|
||||
mode.report_message("An Error Occurred","%s got error %s: %s"%(rerr.method,rerr.exception_type,rerr.exception_msg))
|
||||
mode.refresh()
|
||||
|
||||
def torrent_action(idx, data, mode, ids):
|
||||
if ids:
|
||||
if data==ACTION.PAUSE:
|
||||
log.debug("Pausing torrents: %s",ids)
|
||||
client.core.pause_torrent(ids).addErrback(action_error,mode)
|
||||
elif data==ACTION.RESUME:
|
||||
log.debug("Resuming torrents: %s", ids)
|
||||
client.core.resume_torrent(ids).addErrback(action_error,mode)
|
||||
elif data==ACTION.QUEUE:
|
||||
def do_queue(idx,qact,mode,ids):
|
||||
if qact == ACTION.QUEUE_TOP:
|
||||
log.debug("Queuing torrents top")
|
||||
client.core.queue_top(ids)
|
||||
elif qact == ACTION.QUEUE_UP:
|
||||
log.debug("Queuing torrents up")
|
||||
client.core.queue_up(ids)
|
||||
elif qact == ACTION.QUEUE_DOWN:
|
||||
log.debug("Queuing torrents down")
|
||||
client.core.queue_down(ids)
|
||||
elif qact == ACTION.QUEUE_BOTTOM:
|
||||
log.debug("Queuing torrents bottom")
|
||||
client.core.queue_bottom(ids)
|
||||
if len(ids) == 1:
|
||||
mode.clear_marks()
|
||||
return True
|
||||
popup = SelectablePopup(mode,"Queue Action",do_queue,mode,ids)
|
||||
popup.add_line("_Top",data=ACTION.QUEUE_TOP)
|
||||
popup.add_line("_Up",data=ACTION.QUEUE_UP)
|
||||
popup.add_line("_Down",data=ACTION.QUEUE_DOWN)
|
||||
popup.add_line("_Bottom",data=ACTION.QUEUE_BOTTOM)
|
||||
mode.set_popup(popup)
|
||||
return False
|
||||
elif data==ACTION.REMOVE:
|
||||
def do_remove(idx,data,mode,ids):
|
||||
if data:
|
||||
wd = data==ACTION.REMOVE_DATA
|
||||
for tid in ids:
|
||||
log.debug("Removing torrent: %s,%d",tid,wd)
|
||||
client.core.remove_torrent(tid,wd).addErrback(action_error,mode)
|
||||
if len(ids) == 1:
|
||||
mode.clear_marks()
|
||||
return True
|
||||
popup = SelectablePopup(mode,"Confirm Remove",do_remove,mode,ids)
|
||||
popup.add_line("Are you sure you want to remove the marked torrents?",selectable=False)
|
||||
popup.add_line("Remove with _data",data=ACTION.REMOVE_DATA)
|
||||
popup.add_line("Remove _torrent",data=ACTION.REMOVE_NODATA)
|
||||
popup.add_line("_Cancel",data=0)
|
||||
mode.set_popup(popup)
|
||||
return False
|
||||
elif data==ACTION.MOVE_STORAGE:
|
||||
def do_move(res):
|
||||
import os.path
|
||||
if os.path.exists(res["path"]) and not os.path.isdir(res["path"]):
|
||||
mode.report_message("Cannot Move Storage","{!error!}%s exists and is not a directory"%res["path"])
|
||||
else:
|
||||
log.debug("Moving %s to: %s",ids,res["path"])
|
||||
client.core.move_storage(ids,res["path"]).addErrback(action_error,mode)
|
||||
if len(ids) == 1:
|
||||
mode.clear_marks()
|
||||
return True
|
||||
popup = InputPopup(mode,"Move Storage (Esc to cancel)",close_cb=do_move)
|
||||
popup.add_text_input("Enter path to move to:","path")
|
||||
mode.set_popup(popup)
|
||||
return False
|
||||
elif data==ACTION.RECHECK:
|
||||
log.debug("Rechecking torrents: %s", ids)
|
||||
client.core.force_recheck(ids).addErrback(action_error,mode)
|
||||
elif data==ACTION.REANNOUNCE:
|
||||
log.debug("Reannouncing torrents: %s",ids)
|
||||
client.core.force_reannounce(ids).addErrback(action_error,mode)
|
||||
elif data==ACTION.DETAILS:
|
||||
log.debug("Torrent details")
|
||||
tid = mode.current_torrent_id()
|
||||
if tid:
|
||||
mode.show_torrent_details(tid)
|
||||
else:
|
||||
log.error("No current torrent in _torrent_action, this is a bug")
|
||||
if len(ids) == 1:
|
||||
mode.clear_marks()
|
||||
return True
|
||||
|
||||
# Creates the popup. mode is the calling mode, tids is a list of torrents to take action upon
|
||||
def torrent_actions_popup(mode,tids,details=False):
|
||||
popup = SelectablePopup(mode,"Torrent Actions",torrent_action,mode,tids)
|
||||
popup.add_line("_Pause",data=ACTION.PAUSE)
|
||||
popup.add_line("_Resume",data=ACTION.RESUME)
|
||||
popup.add_divider()
|
||||
popup.add_line("Queue",data=ACTION.QUEUE)
|
||||
popup.add_divider()
|
||||
popup.add_line("_Update Tracker",data=ACTION.REANNOUNCE)
|
||||
popup.add_divider()
|
||||
popup.add_line("Remo_ve Torrent",data=ACTION.REMOVE)
|
||||
popup.add_line("_Force Recheck",data=ACTION.RECHECK)
|
||||
popup.add_line("_Move Storage",data=ACTION.MOVE_STORAGE)
|
||||
if details:
|
||||
popup.add_divider()
|
||||
popup.add_line("Torrent _Details",data=ACTION.DETAILS)
|
||||
mode.set_popup(popup)
|
||||
536
deluge/ui/console/modes/torrentdetail.py
Normal file
@ -0,0 +1,536 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# torrentdetail.py
|
||||
#
|
||||
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||
#
|
||||
# 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 deluge.component as component
|
||||
from basemode import BaseMode
|
||||
import deluge.common
|
||||
from deluge.ui.client import client
|
||||
|
||||
from sys import maxint
|
||||
from collections import deque
|
||||
|
||||
from deluge.ui.sessionproxy import SessionProxy
|
||||
|
||||
from popup import Popup,SelectablePopup,MessagePopup
|
||||
from add_util import add_torrent
|
||||
from input_popup import InputPopup
|
||||
import format_utils
|
||||
|
||||
from torrent_actions import torrent_actions_popup
|
||||
|
||||
try:
|
||||
import curses
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Big help string that gets displayed when the user hits 'h'
|
||||
HELP_STR = """\
|
||||
This screen shows detailed information about a torrent, and also the \
|
||||
information about the individual files in the torrent.
|
||||
|
||||
You can navigate the file list with the Up/Down arrows and use space to \
|
||||
collapse/expand the file tree.
|
||||
|
||||
All popup windows can be closed/canceled by hitting the Esc key \
|
||||
(you might need to wait a second for an Esc to register)
|
||||
|
||||
The actions you can perform and the keys to perform them are as follows:
|
||||
|
||||
{!info!}'h'{!normal!} - Show this help
|
||||
|
||||
{!info!}'a'{!normal!} - Show torrent actions popup. Here you can do things like \
|
||||
pause/resume, remove, recheck and so on.
|
||||
|
||||
{!info!}'m'{!normal!} - Mark a file
|
||||
{!info!}'c'{!normal!} - Un-mark all files
|
||||
|
||||
{!info!}Space{!normal!} - Expand/Collapse currently selected folder
|
||||
|
||||
{!info!}Enter{!normal!} - Show priority popup in which you can set the \
|
||||
download priority of selected files.
|
||||
|
||||
{!info!}Left Arrow{!normal!} - Go back to torrent overview.
|
||||
"""
|
||||
|
||||
class TorrentDetail(BaseMode, component.Component):
|
||||
def __init__(self, alltorrentmode, torrentid, stdscr, encoding=None):
|
||||
self.alltorrentmode = alltorrentmode
|
||||
self.torrentid = torrentid
|
||||
self.torrent_state = None
|
||||
self.popup = None
|
||||
self.messages = deque()
|
||||
self._status_keys = ["files", "name","state","download_payload_rate","upload_payload_rate",
|
||||
"progress","eta","all_time_download","total_uploaded", "ratio",
|
||||
"num_seeds","total_seeds","num_peers","total_peers", "active_time",
|
||||
"seeding_time","time_added","distributed_copies", "num_pieces",
|
||||
"piece_length","save_path","file_progress","file_priorities","message"]
|
||||
self._info_fields = [
|
||||
("Name",None,("name",)),
|
||||
("State", None, ("state",)),
|
||||
("Status",None,("message",)),
|
||||
("Down Speed", format_utils.format_speed, ("download_payload_rate",)),
|
||||
("Up Speed", format_utils.format_speed, ("upload_payload_rate",)),
|
||||
("Progress", format_utils.format_progress, ("progress",)),
|
||||
("ETA", deluge.common.ftime, ("eta",)),
|
||||
("Path", None, ("save_path",)),
|
||||
("Downloaded",deluge.common.fsize,("all_time_download",)),
|
||||
("Uploaded", deluge.common.fsize,("total_uploaded",)),
|
||||
("Share Ratio", lambda x:x < 0 and "∞" or "%.3f"%x, ("ratio",)),
|
||||
("Seeders",format_utils.format_seeds_peers,("num_seeds","total_seeds")),
|
||||
("Peers",format_utils.format_seeds_peers,("num_peers","total_peers")),
|
||||
("Active Time",deluge.common.ftime,("active_time",)),
|
||||
("Seeding Time",deluge.common.ftime,("seeding_time",)),
|
||||
("Date Added",deluge.common.fdate,("time_added",)),
|
||||
("Availability", lambda x:x < 0 and "∞" or "%.3f"%x, ("distributed_copies",)),
|
||||
("Pieces", format_utils.format_pieces, ("num_pieces","piece_length")),
|
||||
]
|
||||
self.file_list = None
|
||||
self.current_file = None
|
||||
self.current_file_idx = 0
|
||||
self.file_limit = maxint
|
||||
self.file_off = 0
|
||||
self.more_to_draw = False
|
||||
|
||||
self.column_string = ""
|
||||
self.files_sep = None
|
||||
|
||||
self.marked = {}
|
||||
|
||||
BaseMode.__init__(self, stdscr, encoding)
|
||||
component.Component.__init__(self, "TorrentDetail", 1, depend=["SessionProxy"])
|
||||
|
||||
self.__split_help()
|
||||
|
||||
self.column_names = ["Filename", "Size", "Progress", "Priority"]
|
||||
self.__update_columns()
|
||||
|
||||
component.start(["TorrentDetail"])
|
||||
curses.curs_set(0)
|
||||
self.stdscr.notimeout(0)
|
||||
|
||||
# component start/update
|
||||
def start(self):
|
||||
component.get("SessionProxy").get_torrent_status(self.torrentid, self._status_keys).addCallback(self.set_state)
|
||||
def update(self):
|
||||
component.get("SessionProxy").get_torrent_status(self.torrentid, self._status_keys).addCallback(self.set_state)
|
||||
|
||||
def set_state(self, state):
|
||||
log.debug("got state")
|
||||
need_prio_update = False
|
||||
if not self.file_list:
|
||||
# don't keep getting the files once we've got them once
|
||||
if state.get("files"):
|
||||
self.files_sep = "{!green,black,bold,underline!}%s"%(("Files (torrent has %d files)"%len(state["files"])).center(self.cols))
|
||||
self.file_list,self.file_dict = self.build_file_list(state["files"],state["file_progress"],state["file_priorities"])
|
||||
self._status_keys.remove("files")
|
||||
else:
|
||||
self.files_sep = "{!green,black,bold,underline!}%s"%(("Files (File list unknown)").center(self.cols))
|
||||
need_prio_update = True
|
||||
self.__fill_progress(self.file_list,state["file_progress"])
|
||||
for i,prio in enumerate(state["file_priorities"]):
|
||||
if self.file_dict[i][6] != prio:
|
||||
need_prio_update = True
|
||||
self.file_dict[i][6] = prio
|
||||
if need_prio_update:
|
||||
self.__fill_prio(self.file_list)
|
||||
del state["file_progress"]
|
||||
del state["file_priorities"]
|
||||
self.torrent_state = state
|
||||
self.refresh()
|
||||
|
||||
def __split_help(self):
|
||||
self.__help_lines = format_utils.wrap_string(HELP_STR,(self.cols/2)-2)
|
||||
|
||||
# split file list into directory tree. this function assumes all files in a
|
||||
# particular directory are returned together. it won't work otherwise.
|
||||
# returned list is a list of lists of the form:
|
||||
# [file/dir_name,index,size,children,expanded,progress,priority]
|
||||
# for directories index values count down from maxint (for marking usage),
|
||||
# for files the index is the value returned in the
|
||||
# state object for use with other libtorrent calls (i.e. setting prio)
|
||||
#
|
||||
# Also returns a dictionary that maps index values to the file leaves
|
||||
# for fast updating of progress and priorities
|
||||
def build_file_list(self, file_tuples,prog,prio):
|
||||
ret = []
|
||||
retdict = {}
|
||||
diridx = maxint
|
||||
for f in file_tuples:
|
||||
cur = ret
|
||||
ps = f["path"].split("/")
|
||||
fin = ps[-1]
|
||||
for p in ps:
|
||||
if not cur or p != cur[-1][0]:
|
||||
cl = []
|
||||
if p == fin:
|
||||
ent = [p,f["index"],f["size"],cl,False,
|
||||
format_utils.format_progress(prog[f["index"]]*100),
|
||||
prio[f["index"]]]
|
||||
retdict[f["index"]] = ent
|
||||
else:
|
||||
ent = [p,diridx,-1,cl,False,0,-1]
|
||||
retdict[diridx] = ent
|
||||
diridx-=1
|
||||
cur.append(ent)
|
||||
cur = cl
|
||||
else:
|
||||
cur = cur[-1][3]
|
||||
self.__build_sizes(ret)
|
||||
self.__fill_progress(ret,prog)
|
||||
return (ret,retdict)
|
||||
|
||||
# fill in the sizes of the directory entries based on their children
|
||||
def __build_sizes(self, fs):
|
||||
ret = 0
|
||||
for f in fs:
|
||||
if f[2] == -1:
|
||||
val = self.__build_sizes(f[3])
|
||||
ret += val
|
||||
f[2] = val
|
||||
else:
|
||||
ret += f[2]
|
||||
return ret
|
||||
|
||||
# fills in progress fields in all entries based on progs
|
||||
# returns the # of bytes complete in all the children of fs
|
||||
def __fill_progress(self,fs,progs):
|
||||
if not progs: return 0
|
||||
tb = 0
|
||||
for f in fs:
|
||||
if f[3]: # dir, has some children
|
||||
bd = self.__fill_progress(f[3],progs)
|
||||
f[5] = format_utils.format_progress((bd/f[2])*100)
|
||||
else: # file, update own prog and add to total
|
||||
bd = f[2]*progs[f[1]]
|
||||
f[5] = format_utils.format_progress(progs[f[1]]*100)
|
||||
tb += bd
|
||||
return tb
|
||||
|
||||
def __fill_prio(self,fs):
|
||||
for f in fs:
|
||||
if f[3]: # dir, so fill in children and compute our prio
|
||||
self.__fill_prio(f[3])
|
||||
s = set([e[6] for e in f[3]]) # pull out all child prios and turn into a set
|
||||
if len(s) > 1:
|
||||
f[6] = -2 # mixed
|
||||
else:
|
||||
f[6] = s.pop()
|
||||
|
||||
def __update_columns(self):
|
||||
self.column_widths = [-1,15,15,20]
|
||||
req = sum(filter(lambda x:x >= 0,self.column_widths))
|
||||
if (req > self.cols): # can't satisfy requests, just spread out evenly
|
||||
cw = int(self.cols/len(self.column_names))
|
||||
for i in range(0,len(self.column_widths)):
|
||||
self.column_widths[i] = cw
|
||||
else:
|
||||
rem = self.cols - req
|
||||
var_cols = len(filter(lambda x: x < 0,self.column_widths))
|
||||
vw = int(rem/var_cols)
|
||||
for i in range(0, len(self.column_widths)):
|
||||
if (self.column_widths[i] < 0):
|
||||
self.column_widths[i] = vw
|
||||
|
||||
self.column_string = "{!green,black,bold!}%s"%("".join(["%s%s"%(self.column_names[i]," "*(self.column_widths[i]-len(self.column_names[i]))) for i in range(0,len(self.column_names))]))
|
||||
|
||||
|
||||
def report_message(self,title,message):
|
||||
self.messages.append((title,message))
|
||||
|
||||
def clear_marks(self):
|
||||
self.marked = {}
|
||||
|
||||
def set_popup(self,pu):
|
||||
self.popup = pu
|
||||
self.refresh()
|
||||
|
||||
|
||||
def draw_files(self,files,depth,off,idx):
|
||||
for fl in files:
|
||||
# kick out if we're going to draw too low on the screen
|
||||
if (off >= self.rows-1):
|
||||
self.more_to_draw = True
|
||||
return -1,-1
|
||||
|
||||
self.file_limit = idx
|
||||
|
||||
if idx >= self.file_off:
|
||||
# set fg/bg colors based on if we are selected/marked or not
|
||||
|
||||
# default values
|
||||
fg = "white"
|
||||
bg = "black"
|
||||
|
||||
if fl[1] in self.marked:
|
||||
bg = "blue"
|
||||
|
||||
if idx == self.current_file_idx:
|
||||
self.current_file = fl
|
||||
bg = "white"
|
||||
if fl[1] in self.marked:
|
||||
fg = "blue"
|
||||
else:
|
||||
fg = "black"
|
||||
|
||||
color_string = "{!%s,%s!}"%(fg,bg)
|
||||
|
||||
#actually draw the dir/file string
|
||||
if fl[3] and fl[4]: # this is an expanded directory
|
||||
xchar = 'v'
|
||||
elif fl[3]: # collapsed directory
|
||||
xchar = '>'
|
||||
else: # file
|
||||
xchar = '-'
|
||||
|
||||
r = format_utils.format_row(["%s%s %s"%(" "*depth,xchar,fl[0]),
|
||||
deluge.common.fsize(fl[2]),fl[5],
|
||||
format_utils.format_priority(fl[6])],
|
||||
self.column_widths)
|
||||
|
||||
self.add_string(off,"%s%s"%(color_string,r),trim=False)
|
||||
off += 1
|
||||
|
||||
if fl[3] and fl[4]:
|
||||
# recurse if we have children and are expanded
|
||||
off,idx = self.draw_files(fl[3],depth+1,off,idx+1)
|
||||
if off < 0: return (off,idx)
|
||||
else:
|
||||
idx += 1
|
||||
|
||||
return (off,idx)
|
||||
|
||||
def on_resize(self, *args):
|
||||
BaseMode.on_resize_norefresh(self, *args)
|
||||
self.__update_columns()
|
||||
self.__split_help()
|
||||
if self.popup:
|
||||
self.popup.handle_resize()
|
||||
self.refresh()
|
||||
|
||||
def refresh(self,lines=None):
|
||||
# show a message popup if there's anything queued
|
||||
if self.popup == None and self.messages:
|
||||
title,msg = self.messages.popleft()
|
||||
self.popup = MessagePopup(self,title,msg)
|
||||
|
||||
# Update the status bars
|
||||
self.stdscr.clear()
|
||||
self.add_string(0,self.statusbars.topbar)
|
||||
hstr = "%sPress [h] for help"%(" "*(self.cols - len(self.statusbars.bottombar) - 10))
|
||||
self.add_string(self.rows - 1, "%s%s"%(self.statusbars.bottombar,hstr))
|
||||
|
||||
if self.files_sep:
|
||||
self.add_string((self.rows/2)-1,self.files_sep)
|
||||
|
||||
off = 1
|
||||
if self.torrent_state:
|
||||
for f in self._info_fields:
|
||||
if off >= (self.rows/2): break
|
||||
if f[1] != None:
|
||||
args = []
|
||||
try:
|
||||
for key in f[2]:
|
||||
args.append(self.torrent_state[key])
|
||||
except:
|
||||
log.debug("Could not get info field: %s",e)
|
||||
continue
|
||||
info = f[1](*args)
|
||||
else:
|
||||
info = self.torrent_state[f[2][0]]
|
||||
|
||||
self.add_string(off,"{!info!}%s: {!input!}%s"%(f[0],info))
|
||||
off += 1
|
||||
else:
|
||||
self.add_string(1, "Waiting for torrent state")
|
||||
|
||||
off = self.rows/2
|
||||
self.add_string(off,self.column_string)
|
||||
if self.file_list:
|
||||
off += 1
|
||||
self.more_to_draw = False
|
||||
self.draw_files(self.file_list,0,off,0)
|
||||
|
||||
#self.stdscr.redrawwin()
|
||||
self.stdscr.noutrefresh()
|
||||
|
||||
if self.popup:
|
||||
self.popup.refresh()
|
||||
|
||||
curses.doupdate()
|
||||
|
||||
# expand or collapse the current file
|
||||
def expcol_cur_file(self):
|
||||
self.current_file[4] = not self.current_file[4]
|
||||
self.refresh()
|
||||
|
||||
def file_list_down(self):
|
||||
if (self.current_file_idx + 1) > self.file_limit:
|
||||
if self.more_to_draw:
|
||||
self.current_file_idx += 1
|
||||
self.file_off += 1
|
||||
else:
|
||||
return
|
||||
else:
|
||||
self.current_file_idx += 1
|
||||
|
||||
self.refresh()
|
||||
|
||||
def file_list_up(self):
|
||||
self.current_file_idx = max(0,self.current_file_idx-1)
|
||||
self.file_off = min(self.file_off,self.current_file_idx)
|
||||
self.refresh()
|
||||
|
||||
def back_to_overview(self):
|
||||
component.stop(["TorrentDetail"])
|
||||
component.deregister("TorrentDetail")
|
||||
self.stdscr.clear()
|
||||
component.get("ConsoleUI").set_mode(self.alltorrentmode)
|
||||
self.alltorrentmode.resume()
|
||||
|
||||
# build list of priorities for all files in the torrent
|
||||
# based on what is currently selected and a selected priority.
|
||||
def build_prio_list(self, files, ret_list, parent_prio, selected_prio):
|
||||
# has a priority been set on my parent (if so, I inherit it)
|
||||
for f in files:
|
||||
if f[3]: # dir, check if i'm setting on whole dir, then recurse
|
||||
if f[1] in self.marked: # marked, recurse and update all children with new prio
|
||||
parent_prio = selected_prio
|
||||
self.build_prio_list(f[3],ret_list,parent_prio,selected_prio)
|
||||
parent_prio = -1
|
||||
else: # not marked, just recurse
|
||||
self.build_prio_list(f[3],ret_list,parent_prio,selected_prio)
|
||||
else: # file, need to add to list
|
||||
if f[1] in self.marked or parent_prio >= 0:
|
||||
# selected (or parent selected), use requested priority
|
||||
ret_list.append((f[1],selected_prio))
|
||||
else:
|
||||
# not selected, just keep old priority
|
||||
ret_list.append((f[1],f[6]))
|
||||
|
||||
def do_priority(self, idx, data):
|
||||
plist = []
|
||||
self.build_prio_list(self.file_list,plist,-1,data)
|
||||
plist.sort()
|
||||
priorities = [p[1] for p in plist]
|
||||
log.debug("priorities: %s", priorities)
|
||||
|
||||
client.core.set_torrent_file_priorities(self.torrentid, priorities)
|
||||
|
||||
if len(self.marked) == 1:
|
||||
self.marked = {}
|
||||
return True
|
||||
|
||||
# show popup for priority selections
|
||||
def show_priority_popup(self):
|
||||
if self.marked:
|
||||
self.popup = SelectablePopup(self,"Set File Priority",self.do_priority)
|
||||
self.popup.add_line("_Do Not Download",data=deluge.common.FILE_PRIORITY["Do Not Download"])
|
||||
self.popup.add_line("_Normal Priority",data=deluge.common.FILE_PRIORITY["Normal Priority"])
|
||||
self.popup.add_line("_High Priority",data=deluge.common.FILE_PRIORITY["High Priority"])
|
||||
self.popup.add_line("H_ighest Priority",data=deluge.common.FILE_PRIORITY["Highest Priority"])
|
||||
|
||||
def __mark_unmark(self,idx):
|
||||
if idx in self.marked:
|
||||
del self.marked[idx]
|
||||
else:
|
||||
self.marked[idx] = True
|
||||
|
||||
def _doRead(self):
|
||||
c = self.stdscr.getch()
|
||||
|
||||
if self.popup:
|
||||
if self.popup.handle_read(c):
|
||||
self.popup = None
|
||||
self.refresh()
|
||||
return
|
||||
|
||||
if c > 31 and c < 256:
|
||||
if chr(c) == 'Q':
|
||||
from twisted.internet import reactor
|
||||
if client.connected():
|
||||
def on_disconnect(result):
|
||||
reactor.stop()
|
||||
client.disconnect().addCallback(on_disconnect)
|
||||
else:
|
||||
reactor.stop()
|
||||
return
|
||||
elif chr(c) == 'q':
|
||||
self.back_to_overview()
|
||||
return
|
||||
|
||||
if c == 27 or c == curses.KEY_LEFT:
|
||||
self.back_to_overview()
|
||||
return
|
||||
|
||||
if not self.torrent_state:
|
||||
# actions below only makes sense if there is a torrent state
|
||||
return
|
||||
|
||||
# Navigate the torrent list
|
||||
if c == curses.KEY_UP:
|
||||
self.file_list_up()
|
||||
elif c == curses.KEY_PPAGE:
|
||||
pass
|
||||
elif c == curses.KEY_DOWN:
|
||||
self.file_list_down()
|
||||
elif c == curses.KEY_NPAGE:
|
||||
pass
|
||||
|
||||
# Enter Key
|
||||
elif c == curses.KEY_ENTER or c == 10:
|
||||
self.marked[self.current_file[1]] = True
|
||||
self.show_priority_popup()
|
||||
|
||||
# space
|
||||
elif c == 32:
|
||||
self.expcol_cur_file()
|
||||
else:
|
||||
if c > 31 and c < 256:
|
||||
if chr(c) == 'm':
|
||||
if self.current_file:
|
||||
self.__mark_unmark(self.current_file[1])
|
||||
elif chr(c) == 'c':
|
||||
self.marked = {}
|
||||
elif chr(c) == 'a':
|
||||
torrent_actions_popup(self,[self.torrentid],details=False)
|
||||
return
|
||||
elif chr(c) == 'h':
|
||||
self.popup = Popup(self,"Help",init_lines=self.__help_lines)
|
||||
|
||||
self.refresh()
|
||||
@ -41,7 +41,6 @@ class StatusBars(component.Component):
|
||||
def __init__(self):
|
||||
component.Component.__init__(self, "StatusBars", 2, depend=["CoreConfig"])
|
||||
self.config = component.get("CoreConfig")
|
||||
self.screen = component.get("ConsoleUI").screen
|
||||
|
||||
# Hold some values we get from the core
|
||||
self.connections = 0
|
||||
@ -49,6 +48,10 @@ class StatusBars(component.Component):
|
||||
self.upload = ""
|
||||
self.dht = 0
|
||||
|
||||
# Default values
|
||||
self.topbar = "{!status!}Deluge %s Console - " % deluge.common.get_version()
|
||||
self.bottombar = "{!status!}C: %s" % self.connections
|
||||
|
||||
def start(self):
|
||||
self.update()
|
||||
|
||||
@ -77,30 +80,28 @@ class StatusBars(component.Component):
|
||||
|
||||
def update_statusbars(self):
|
||||
# Update the topbar string
|
||||
self.screen.topbar = "{!status!}Deluge %s Console - " % deluge.common.get_version()
|
||||
self.topbar = "{!status!}Deluge %s Console - " % deluge.common.get_version()
|
||||
if client.connected():
|
||||
info = client.connection_info()
|
||||
self.screen.topbar += "%s@%s:%s" % (info[2], info[0], info[1])
|
||||
self.topbar += "%s@%s:%s" % (info[2], info[0], info[1])
|
||||
else:
|
||||
self.screen.topbar += "Not Connected"
|
||||
self.topbar += "Not Connected"
|
||||
|
||||
# Update the bottombar string
|
||||
self.screen.bottombar = "{!status!}C: %s" % self.connections
|
||||
self.bottombar = "{!status!}C: %s" % self.connections
|
||||
|
||||
if self.config["max_connections_global"] > -1:
|
||||
self.screen.bottombar += " (%s)" % self.config["max_connections_global"]
|
||||
self.bottombar += " (%s)" % self.config["max_connections_global"]
|
||||
|
||||
self.screen.bottombar += " D: %s/s" % self.download
|
||||
self.bottombar += " D: %s/s" % self.download
|
||||
|
||||
if self.config["max_download_speed"] > -1:
|
||||
self.screen.bottombar += " (%s KiB/s)" % self.config["max_download_speed"]
|
||||
self.bottombar += " (%s KiB/s)" % self.config["max_download_speed"]
|
||||
|
||||
self.screen.bottombar += " U: %s/s" % self.upload
|
||||
self.bottombar += " U: %s/s" % self.upload
|
||||
|
||||
if self.config["max_upload_speed"] > -1:
|
||||
self.screen.bottombar += " (%s KiB/s)" % self.config["max_upload_speed"]
|
||||
self.bottombar += " (%s KiB/s)" % self.config["max_upload_speed"]
|
||||
|
||||
if self.config["dht"]:
|
||||
self.screen.bottombar += " DHT: %s" % self.dht
|
||||
|
||||
self.screen.refresh()
|
||||
self.bottombar += " DHT: %s" % self.dht
|
||||
|
||||
@ -45,8 +45,8 @@ class CoreConfig(component.Component):
|
||||
log.debug("CoreConfig init..")
|
||||
component.Component.__init__(self, "CoreConfig")
|
||||
self.config = {}
|
||||
def on_configvaluechanged_event(event):
|
||||
self.config[event.key] = event.value
|
||||
def on_configvaluechanged_event(key, value):
|
||||
self.config[key] = value
|
||||
client.register_event_handler("ConfigValueChangedEvent", on_configvaluechanged_event)
|
||||
|
||||
def start(self):
|
||||
|
||||
|
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 |