Merge branch 'master' into plugins-namespace

This commit is contained in:
Pedro Algarvio 2011-06-03 17:48:22 +01:00
commit 87e767d4c1
488 changed files with 20864 additions and 10562 deletions

View file

@ -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
View file

@ -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
View file

@ -1,3 +1,4 @@
#!/usr/bin/env python
import os
# Paths to exclude

View file

@ -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

View file

@ -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()

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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")

View file

@ -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)

View file

@ -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):
"""

View file

@ -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):
"""

View file

@ -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()

View file

@ -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

View file

@ -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!")

View 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)

View file

@ -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]

View file

@ -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

View file

@ -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;

View file

@ -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

View file

@ -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]

View file

@ -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()

View file

@ -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:

View file

@ -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):
"""

View file

@ -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)

View file

@ -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)

View file

@ -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__ = """"""

View file

@ -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!")

View file

@ -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):

View file

@ -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"):
@ -104,7 +104,7 @@ class Core(CorePluginBase):
# 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
@ -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):

View file

@ -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")

View file

@ -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")

View file

@ -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):

View file

@ -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();
},

View file

@ -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/*"]}

View file

@ -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

View file

@ -213,7 +213,7 @@
<property name="max_length">65535</property>
<property name="invisible_char">&#x25CF;</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>

View file

@ -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/*"]}

View file

@ -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):

View file

@ -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)

View file

@ -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()).

View file

@ -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

View file

@ -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
)

View file

@ -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

View file

@ -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

View file

@ -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

Binary file not shown.

View 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()

View file

@ -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)

View file

@ -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

View 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)

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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")

View 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()

View file

@ -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")

View file

@ -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")

View 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

View file

@ -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):

View 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)

View 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)

View file

@ -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)

View file

@ -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
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)
# 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)
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,201 +277,21 @@ 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)
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
def match_torrent(self, string):
"""
@ -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))

View file

View 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)

View 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)

View 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()

View 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

View 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

View 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()

View 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

View 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

View file

@ -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]

View 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()

View 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

View 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()

View 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)

View 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()

View file

@ -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

View file

@ -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):

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 722 B

After

Width:  |  Height:  |  Size: 722 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 6 KiB

After

Width:  |  Height:  |  Size: 6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 742 B

After

Width:  |  Height:  |  Size: 742 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 590 B

After

Width:  |  Height:  |  Size: 590 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

Some files were not shown because too many files have changed in this diff Show more