summaryrefslogtreecommitdiffstats
path: root/deluge/ui/console/cmdline
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 21:38:38 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 21:38:38 +0000
commit2e2851dc13d73352530dd4495c7e05603b2e520d (patch)
tree622b9cd8e5d32091c9aa9e4937b533975a40356c /deluge/ui/console/cmdline
parentInitial commit. (diff)
downloaddeluge-2e2851dc13d73352530dd4495c7e05603b2e520d.tar.xz
deluge-2e2851dc13d73352530dd4495c7e05603b2e520d.zip
Adding upstream version 2.1.2~dev0+20240219.upstream/2.1.2_dev0+20240219upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'deluge/ui/console/cmdline')
-rw-r--r--deluge/ui/console/cmdline/__init__.py0
-rw-r--r--deluge/ui/console/cmdline/command.py211
-rw-r--r--deluge/ui/console/cmdline/commands/__init__.py3
-rw-r--r--deluge/ui/console/cmdline/commands/add.py117
-rw-r--r--deluge/ui/console/cmdline/commands/cache.py28
-rw-r--r--deluge/ui/console/cmdline/commands/config.py136
-rw-r--r--deluge/ui/console/cmdline/commands/connect.py78
-rw-r--r--deluge/ui/console/cmdline/commands/debug.py37
-rw-r--r--deluge/ui/console/cmdline/commands/gui.py27
-rw-r--r--deluge/ui/console/cmdline/commands/halt.py32
-rw-r--r--deluge/ui/console/cmdline/commands/help.py71
-rw-r--r--deluge/ui/console/cmdline/commands/info.py488
-rw-r--r--deluge/ui/console/cmdline/commands/manage.py114
-rw-r--r--deluge/ui/console/cmdline/commands/move.py94
-rw-r--r--deluge/ui/console/cmdline/commands/pause.py45
-rw-r--r--deluge/ui/console/cmdline/commands/plugin.py140
-rw-r--r--deluge/ui/console/cmdline/commands/quit.py22
-rw-r--r--deluge/ui/console/cmdline/commands/recheck.py44
-rw-r--r--deluge/ui/console/cmdline/commands/resume.py45
-rw-r--r--deluge/ui/console/cmdline/commands/rm.py82
-rw-r--r--deluge/ui/console/cmdline/commands/status.py116
-rw-r--r--deluge/ui/console/cmdline/commands/update_tracker.py44
22 files changed, 1974 insertions, 0 deletions
diff --git a/deluge/ui/console/cmdline/__init__.py b/deluge/ui/console/cmdline/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/deluge/ui/console/cmdline/__init__.py
diff --git a/deluge/ui/console/cmdline/command.py b/deluge/ui/console/cmdline/command.py
new file mode 100644
index 0000000..63dc926
--- /dev/null
+++ b/deluge/ui/console/cmdline/command.py
@@ -0,0 +1,211 @@
+#
+# 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>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+import logging
+import shlex
+
+from twisted.internet import defer
+
+from deluge.ui.client import client
+from deluge.ui.console.parser import OptionParser, OptionParserError
+from deluge.ui.console.utils.colors import strip_colors
+
+log = logging.getLogger(__name__)
+
+
+class Commander:
+ def __init__(self, cmds, interactive=False):
+ self._commands = cmds
+ self.interactive = interactive
+
+ def write(self, line):
+ print(strip_colors(line))
+
+ def do_command(self, cmd_line):
+ """Run a console command.
+
+ Args:
+ cmd_line (str): Console command.
+
+ Returns:
+ Deferred: A deferred that fires when the command has been executed.
+
+ """
+ options = self.parse_command(cmd_line)
+ if options:
+ return self.exec_command(options)
+ return defer.succeed(None)
+
+ def exit(self, status=0, msg=None):
+ self._exit = True
+ if msg:
+ print(msg)
+
+ def parse_command(self, cmd_line):
+ """Parse a console command and process with argparse.
+
+ Args:
+ cmd_line (str): Console command.
+
+ Returns:
+ argparse.Namespace: The parsed command.
+
+ """
+ if not cmd_line:
+ return
+ cmd, _, line = cmd_line.partition(' ')
+ try:
+ parser = self._commands[cmd].create_parser()
+ except KeyError:
+ self.write('{!error!}Unknown command: %s' % cmd)
+ return
+
+ try:
+ args = [cmd] + self._commands[cmd].split(line)
+ except ValueError as ex:
+ self.write('{!error!}Error parsing command: %s' % ex)
+ return
+
+ # 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 = parser.parse_args(args=args)
+ options.command = cmd
+ except TypeError as ex:
+ self.write('{!error!}Error parsing options: %s' % ex)
+ import traceback
+
+ self.write('%s' % traceback.format_exc())
+ return
+ except OptionParserError as ex:
+ import traceback
+
+ log.warning('Error parsing command "%s": %s', args, ex)
+ self.write('{!error!} %s' % ex)
+ parser.print_help()
+ return
+
+ if getattr(parser, '_exit', False):
+ return
+ return options
+
+ def exec_command(self, options, *args):
+ """Execute a console command.
+
+ Args:
+ options (argparse.Namespace): The command to execute.
+
+ Returns:
+ Deferred: A deferred that fires when command has been executed.
+
+ """
+ try:
+ ret = self._commands[options.command].handle(options)
+ except Exception as ex: # pylint: disable=broad-except
+ self.write('{!error!} %s' % ex)
+ log.exception(ex)
+ import traceback
+
+ self.write('%s' % traceback.format_exc())
+ return defer.succeed(True)
+ else:
+ return ret
+
+
+class BaseCommand:
+ usage = None
+ interactive_only = False
+ aliases = []
+ _name = 'base'
+ epilog = ''
+
+ def complete(self, text, *args):
+ return []
+
+ def handle(self, options):
+ pass
+
+ @property
+ def name(self):
+ return self._name
+
+ @property
+ def name_with_alias(self):
+ return '/'.join([self._name] + self.aliases)
+
+ @property
+ def description(self):
+ return self.__doc__
+
+ def split(self, text):
+ text = text.replace('\\', '\\\\')
+ result = shlex.split(text)
+ for i, s in enumerate(result):
+ result[i] = s.replace(r'\ ', ' ')
+ result = [s for s in result if s != '']
+ return result
+
+ def create_parser(self):
+ opts = {
+ 'prog': self.name_with_alias,
+ 'description': self.__doc__,
+ 'epilog': self.epilog,
+ }
+ if self.usage:
+ opts['usage'] = self.usage
+ parser = OptionParser(**opts)
+ parser.add_argument(self.name, metavar='')
+ parser.base_parser = parser
+ self.add_arguments(parser)
+ return parser
+
+ def add_subparser(self, subparsers):
+ opts = {
+ 'prog': self.name_with_alias,
+ 'help': self.__doc__,
+ 'description': self.__doc__,
+ }
+ if self.usage:
+ opts['usage'] = self.usage
+
+ # A workaround for aliases showing as duplicate command names in help output.
+ for cmd_name in sorted([self.name] + self.aliases):
+ if cmd_name not in subparsers._name_parser_map:
+ if cmd_name in self.aliases:
+ opts['help'] = _('`%s` alias' % self.name)
+ parser = subparsers.add_parser(cmd_name, **opts)
+ break
+
+ self.add_arguments(parser)
+
+ def add_arguments(self, parser):
+ pass
diff --git a/deluge/ui/console/cmdline/commands/__init__.py b/deluge/ui/console/cmdline/commands/__init__.py
new file mode 100644
index 0000000..39dbefe
--- /dev/null
+++ b/deluge/ui/console/cmdline/commands/__init__.py
@@ -0,0 +1,3 @@
+from deluge.ui.console.cmdline.command import BaseCommand
+
+__all__ = ['BaseCommand']
diff --git a/deluge/ui/console/cmdline/commands/add.py b/deluge/ui/console/cmdline/commands/add.py
new file mode 100644
index 0000000..706ae16
--- /dev/null
+++ b/deluge/ui/console/cmdline/commands/add.py
@@ -0,0 +1,117 @@
+#
+# Copyright (C) 2008-2009 Ido Abramovich <ido.deluge@gmail.com>
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+import os
+from base64 import b64encode
+from urllib.parse import urlparse
+from urllib.request import url2pathname
+
+from twisted.internet import defer
+
+import deluge.common
+import deluge.component as component
+from deluge.ui.client import client
+
+from . import BaseCommand
+
+
+class Command(BaseCommand):
+ """Add torrents"""
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ '-p', '--path', dest='path', help=_('Download folder for torrent')
+ )
+ parser.add_argument(
+ '-m',
+ '--move-path',
+ dest='move_completed_path',
+ help=_('Move the completed torrent to this folder'),
+ )
+ parser.add_argument(
+ 'torrents',
+ metavar='<torrent>',
+ nargs='+',
+ help=_('One or more torrent files, URLs or magnet URIs'),
+ )
+
+ def handle(self, options):
+ self.console = component.get('ConsoleUI')
+
+ t_options = {}
+ if options.path:
+ t_options['download_location'] = os.path.abspath(
+ os.path.expanduser(options.path)
+ )
+
+ if options.move_completed_path:
+ t_options['move_completed'] = True
+ t_options['move_completed_path'] = os.path.abspath(
+ os.path.expanduser(options.move_completed_path)
+ )
+
+ def on_success(result):
+ if not result:
+ self.console.write('{!error!}Torrent was not added: Already in session')
+ else:
+ 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 torrent in options.torrents:
+ if not torrent.strip():
+ continue
+ if deluge.common.is_url(torrent):
+ self.console.write(
+ '{!info!}Attempting to add torrent from URL: %s' % torrent
+ )
+ deferreds.append(
+ client.core.add_torrent_url(torrent, t_options)
+ .addCallback(on_success)
+ .addErrback(on_fail)
+ )
+ elif deluge.common.is_magnet(torrent):
+ self.console.write(
+ '{!info!}Attempting to add torrent from magnet URI: %s' % torrent
+ )
+ deferreds.append(
+ client.core.add_torrent_magnet(torrent, t_options)
+ .addCallback(on_success)
+ .addErrback(on_fail)
+ )
+ else:
+ # Just a file
+ if urlparse(torrent).scheme == 'file':
+ torrent = url2pathname(urlparse(torrent).path)
+ path = os.path.abspath(os.path.expanduser(torrent))
+ if not os.path.exists(path):
+ self.console.write('{!error!}%s does not exist!' % path)
+ continue
+ if not os.path.isfile(path):
+ self.console.write('{!error!}This is a directory!')
+ continue
+ self.console.write('{!info!}Attempting to add torrent: %s' % path)
+ filename = os.path.split(path)[-1]
+ with open(path, 'rb') as _file:
+ filedump = b64encode(_file.read())
+ deferreds.append(
+ client.core.add_torrent_file_async(filename, filedump, t_options)
+ .addCallback(on_success)
+ .addErrback(on_fail)
+ )
+
+ return defer.DeferredList(deferreds)
+
+ def complete(self, line):
+ return component.get('ConsoleUI').tab_complete_path(
+ line, ext='.torrent', sort='date'
+ )
diff --git a/deluge/ui/console/cmdline/commands/cache.py b/deluge/ui/console/cmdline/commands/cache.py
new file mode 100644
index 0000000..fe6cd58
--- /dev/null
+++ b/deluge/ui/console/cmdline/commands/cache.py
@@ -0,0 +1,28 @@
+#
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+import deluge.component as component
+from deluge.ui.client import client
+from deluge.ui.common import DISK_CACHE_KEYS
+
+from . import BaseCommand
+
+
+class Command(BaseCommand):
+ """Show information about the disk cache"""
+
+ def handle(self, options):
+ self.console = component.get('ConsoleUI')
+
+ def on_cache_status(status):
+ for key, value in sorted(status.items()):
+ self.console.write(f'{{!info!}}{key}: {{!input!}}{value}')
+
+ return client.core.get_session_status(DISK_CACHE_KEYS).addCallback(
+ on_cache_status
+ )
diff --git a/deluge/ui/console/cmdline/commands/config.py b/deluge/ui/console/cmdline/commands/config.py
new file mode 100644
index 0000000..8b31ca3
--- /dev/null
+++ b/deluge/ui/console/cmdline/commands/config.py
@@ -0,0 +1,136 @@
+#
+# Copyright (C) 2008-2009 Ido Abramovich <ido.deluge@gmail.com>
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+import json
+import logging
+import re
+
+import deluge.component as component
+import deluge.ui.console.utils.colors as colors
+from deluge.ui.client import client
+
+from . import BaseCommand
+
+log = logging.getLogger(__name__)
+
+
+def json_eval(source):
+ """Evaluates string as json data and returns Python objects."""
+ if source == '':
+ return source
+
+ src = source.splitlines()[0]
+
+ # Substitutions to enable usage of pythonic syntax.
+ if src.startswith('(') and src.endswith(')'):
+ src = re.sub(r'^\((.*)\)$', r'[\1]', src)
+ elif src.lower() in ('true', 'false'):
+ src = src.lower()
+ elif src.lower() == 'none':
+ src = 'null'
+
+ try:
+ return json.loads(src)
+ except ValueError:
+ return src
+
+
+class Command(BaseCommand):
+ """Show and set configuration values"""
+
+ usage = _('Usage: config [--set <key> <value>] [<key> [<key>...] ]')
+
+ def add_arguments(self, parser):
+ set_group = parser.add_argument_group('setting a value')
+ set_group.add_argument(
+ '-s',
+ '--set',
+ action='store',
+ metavar='<key>',
+ help=_('set value for this key'),
+ )
+ set_group.add_argument(
+ 'values', metavar='<value>', nargs='+', help=_('Value to set')
+ )
+ get_group = parser.add_argument_group('getting values')
+ get_group.add_argument(
+ 'keys',
+ metavar='<keys>',
+ nargs='*',
+ help=_('one or more keys separated by space'),
+ )
+
+ def handle(self, options):
+ self.console = component.get('ConsoleUI')
+ if options.set:
+ return self._set_config(options)
+ else:
+ return self._get_config(options)
+
+ def _get_config(self, options):
+ def _on_get_config(config):
+ string = ''
+ for key in sorted(config):
+ if key not in options.values:
+ continue
+
+ color = '{!white,black,bold!}'
+ value = config[key]
+ try:
+ color = colors.type_color[type(value)]
+ except KeyError:
+ pass
+
+ # We need to format dicts for printing
+ if isinstance(value, dict):
+ import pprint
+
+ value = pprint.pformat(value, 2, 80)
+ new_value = []
+ for line in value.splitlines():
+ new_value.append(f'{color}{line}')
+ value = '\n'.join(new_value)
+
+ string += f'{key}: {color}{value}\n'
+ self.console.write(string.strip())
+
+ return client.core.get_config().addCallback(_on_get_config)
+
+ def _set_config(self, options):
+ config = component.get('CoreConfig')
+ key = options.set
+ val = ' '.join(options.values)
+
+ try:
+ val = json_eval(val)
+ except Exception as ex:
+ self.console.write('{!error!}%s' % ex)
+ return
+
+ if key not in config:
+ self.console.write('{!error!}Invalid key: %s' % key)
+ return
+
+ if not isinstance(config[key], type(val)):
+ try:
+ val = type(config[key])(val)
+ except TypeError:
+ self.config.write(
+ '{!error!}Configuration value provided has incorrect type.'
+ )
+ return
+
+ def on_set_config(result):
+ self.console.write('{!success!}Configuration value successfully updated.')
+
+ self.console.write(f'Setting "{key}" to: {val!r}')
+ return client.core.set_config({key: val}).addCallback(on_set_config)
+
+ def complete(self, text):
+ return [k for k in component.get('CoreConfig') if k.startswith(text)]
diff --git a/deluge/ui/console/cmdline/commands/connect.py b/deluge/ui/console/cmdline/commands/connect.py
new file mode 100644
index 0000000..4c76de3
--- /dev/null
+++ b/deluge/ui/console/cmdline/commands/connect.py
@@ -0,0 +1,78 @@
+#
+# Copyright (C) 2008-2009 Ido Abramovich <ido.deluge@gmail.com>
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+import logging
+
+import deluge.component as component
+from deluge.ui.client import client
+
+from . import BaseCommand
+
+log = logging.getLogger(__name__)
+
+
+class Command(BaseCommand):
+ """Connect to a new deluge server"""
+
+ usage = _('Usage: connect <host[:port]> [<username>] [<password>]')
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ 'host', help=_('Daemon host and port'), metavar='<host[:port]>'
+ )
+ parser.add_argument(
+ 'username', help=_('Username'), metavar='<username>', nargs='?', default=''
+ )
+ parser.add_argument(
+ 'password', help=_('Password'), metavar='<password>', nargs='?', default=''
+ )
+
+ def add_parser(self, subparsers):
+ parser = subparsers.add_parser(
+ self.name, help=self.__doc__, description=self.__doc__, prog='connect'
+ )
+ self.add_arguments(parser)
+
+ def handle(self, options):
+ self.console = component.get('ConsoleUI')
+
+ host = options.host
+ try:
+ host, port = host.split(':')
+ port = int(port)
+ except ValueError:
+ port = 58846
+
+ def do_connect():
+ d = client.connect(host, port, options.username, options.password)
+
+ def on_connect(result):
+ if self.console.interactive:
+ self.console.write(f'{{!success!}}Connected to {host}:{port}!')
+ return component.start()
+
+ def on_connect_fail(result):
+ self.console.write(
+ f'{{!error!}}Failed to connect to {host}:{port} with reason: {result.value.message}'
+ )
+ return result
+
+ d.addCallbacks(on_connect, on_connect_fail)
+ return d
+
+ if client.connected():
+
+ def on_disconnect(result):
+ if self.console.statusbars:
+ self.console.statusbars.update_statusbars()
+ return do_connect()
+
+ return client.disconnect().addCallback(on_disconnect)
+ else:
+ return do_connect()
diff --git a/deluge/ui/console/cmdline/commands/debug.py b/deluge/ui/console/cmdline/commands/debug.py
new file mode 100644
index 0000000..af48a8b
--- /dev/null
+++ b/deluge/ui/console/cmdline/commands/debug.py
@@ -0,0 +1,37 @@
+#
+# Copyright (C) 2008-2009 Ido Abramovich <ido.deluge@gmail.com>
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+from twisted.internet import defer
+
+import deluge.component as component
+import deluge.log
+
+from . import BaseCommand
+
+
+class Command(BaseCommand):
+ """Enable and disable debugging"""
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ 'state', metavar='<on|off>', choices=['on', 'off'], help=_('The new state')
+ )
+
+ def handle(self, options):
+ if options.state == 'on':
+ deluge.log.set_logger_level('debug')
+ elif options.state == 'off':
+ deluge.log.set_logger_level('error')
+ else:
+ component.get('ConsoleUI').write('{!error!}%s' % self.usage)
+
+ return defer.succeed(True)
+
+ def complete(self, text):
+ return [x for x in ['on', 'off'] if x.startswith(text)]
diff --git a/deluge/ui/console/cmdline/commands/gui.py b/deluge/ui/console/cmdline/commands/gui.py
new file mode 100644
index 0000000..575bc9b
--- /dev/null
+++ b/deluge/ui/console/cmdline/commands/gui.py
@@ -0,0 +1,27 @@
+#
+# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+import logging
+
+import deluge.component as component
+
+from . import BaseCommand
+
+log = logging.getLogger(__name__)
+
+
+class Command(BaseCommand):
+ """Enable interactive mode"""
+
+ interactive_only = True
+
+ def handle(self, options):
+ console = component.get('ConsoleUI')
+ at = console.set_mode('TorrentList')
+ at.go_top = True
+ at.resume()
diff --git a/deluge/ui/console/cmdline/commands/halt.py b/deluge/ui/console/cmdline/commands/halt.py
new file mode 100644
index 0000000..608f2de
--- /dev/null
+++ b/deluge/ui/console/cmdline/commands/halt.py
@@ -0,0 +1,32 @@
+#
+# Copyright (C) 2008-2009 Ido Abramovich <ido.deluge@gmail.com>
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+import deluge.component as component
+from deluge.ui.client import client
+
+from . import BaseCommand
+
+
+class Command(BaseCommand):
+ """Shutdown the deluge server."""
+
+ def handle(self, options):
+ self.console = component.get('ConsoleUI')
+
+ def on_shutdown(result):
+ self.console.write('{!success!}Daemon was shutdown')
+
+ def on_shutdown_fail(reason):
+ self.console.write('{!error!}Unable to shutdown daemon: %s' % reason)
+
+ return (
+ client.daemon.shutdown()
+ .addCallback(on_shutdown)
+ .addErrback(on_shutdown_fail)
+ )
diff --git a/deluge/ui/console/cmdline/commands/help.py b/deluge/ui/console/cmdline/commands/help.py
new file mode 100644
index 0000000..754dadb
--- /dev/null
+++ b/deluge/ui/console/cmdline/commands/help.py
@@ -0,0 +1,71 @@
+#
+# Copyright (C) 2008-2009 Ido Abramovich <ido.deluge@gmail.com>
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+import logging
+
+from twisted.internet import defer
+
+import deluge.component as component
+
+from . import BaseCommand
+
+log = logging.getLogger(__name__)
+
+
+class Command(BaseCommand):
+ """Displays help on other commands"""
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ 'commands', metavar='<command>', nargs='*', help=_('One or more commands')
+ )
+
+ def handle(self, options):
+ self.console = component.get('ConsoleUI')
+ self._commands = self.console._commands
+ deferred = defer.succeed(True)
+ if options.commands:
+ for arg in options.commands:
+ try:
+ cmd = self._commands[arg]
+ except KeyError:
+ self.console.write('{!error!}Unknown command %s' % arg)
+ continue
+ try:
+ parser = cmd.create_parser()
+ self.console.write(parser.format_help())
+ except AttributeError:
+ self.console.write(cmd.__doc__ or 'No help for this command')
+ self.console.write(' ')
+ else:
+ self.console.set_batch_write(True)
+ cmds_doc = ''
+ for cmd in sorted(self._commands):
+ if cmd in self._commands[cmd].aliases:
+ continue
+ parser = self._commands[cmd].create_parser()
+ cmd_doc = (
+ '{!info!}'
+ + '%-9s' % self._commands[cmd].name_with_alias
+ + '{!input!} - '
+ + self._commands[cmd].__doc__
+ + '\n '
+ + parser.format_usage()
+ or ''
+ )
+ cmds_doc += parser.formatter.format_colors(cmd_doc)
+ self.console.write(cmds_doc)
+ self.console.write(' ')
+ self.console.write('For help on a specific command, use `<command> --help`')
+ self.console.set_batch_write(False)
+
+ return deferred
+
+ def complete(self, line):
+ return [x for x in component.get('ConsoleUI')._commands if x.startswith(line)]
diff --git a/deluge/ui/console/cmdline/commands/info.py b/deluge/ui/console/cmdline/commands/info.py
new file mode 100644
index 0000000..7ea9a67
--- /dev/null
+++ b/deluge/ui/console/cmdline/commands/info.py
@@ -0,0 +1,488 @@
+#
+# Copyright (C) 2008-2009 Ido Abramovich <ido.deluge@gmail.com>
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+from os.path import sep as dirsep
+
+import deluge.component as component
+import deluge.ui.console.utils.colors as colors
+from deluge.common import TORRENT_STATE, fsize, fspeed
+from deluge.ui.client import client
+from deluge.ui.common import FILE_PRIORITY
+from deluge.ui.console.utils.format_utils import (
+ f_progressbar,
+ f_seedrank_dash,
+ format_date_never,
+ format_progress,
+ format_time,
+ ftotal_sized,
+ pad_string,
+ remove_formatting,
+ shorten_hash,
+ strwidth,
+ trim_string,
+)
+
+from . import BaseCommand
+
+STATUS_KEYS = [
+ 'state',
+ 'download_location',
+ 'tracker_host',
+ 'tracker_status',
+ 'next_announce',
+ 'name',
+ 'total_size',
+ 'progress',
+ 'num_seeds',
+ 'total_seeds',
+ 'num_peers',
+ 'total_peers',
+ 'eta',
+ 'download_payload_rate',
+ 'upload_payload_rate',
+ 'ratio',
+ 'distributed_copies',
+ 'num_pieces',
+ 'piece_length',
+ 'total_done',
+ 'files',
+ 'file_priorities',
+ 'file_progress',
+ 'peers',
+ 'is_seed',
+ 'is_finished',
+ 'active_time',
+ 'seeding_time',
+ 'time_since_transfer',
+ 'last_seen_complete',
+ 'seed_rank',
+ 'all_time_download',
+ 'total_uploaded',
+ 'total_payload_download',
+ 'total_payload_upload',
+ 'time_added',
+ 'label',
+]
+
+# Add filter specific state to torrent states
+STATES = ['Active'] + TORRENT_STATE
+
+
+class Command(BaseCommand):
+ """Show information about the torrents"""
+
+ sort_help = 'sort items. Possible keys: ' + ', '.join(STATUS_KEYS)
+
+ epilog = """
+ You can give the first few characters of a torrent-id to identify the torrent.
+
+ Tab Completion in interactive mode (info *pattern*<tab>):\n
+ | First press of <tab> will output up to 15 matches;
+ | hitting <tab> a second time, will print 15 more matches;
+ | and a third press will print all remaining matches.
+ | (To modify behaviour of third <tab>, set `third_tab_lists_all` to False)
+"""
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ '-v',
+ '--verbose',
+ action='store_true',
+ default=False,
+ dest='verbose',
+ help=_('Show more information per torrent.'),
+ )
+ parser.add_argument(
+ '-d',
+ '--detailed',
+ action='store_true',
+ default=False,
+ dest='detailed',
+ help=_('Show more detailed information including files and peers.'),
+ )
+ parser.add_argument(
+ '-s',
+ '--state',
+ action='store',
+ dest='state',
+ help=_('Show torrents with state STATE: %s.' % (', '.join(STATES))),
+ )
+ parser.add_argument(
+ '--sort',
+ action='store',
+ type=str,
+ default='',
+ dest='sort',
+ help=self.sort_help,
+ )
+ parser.add_argument(
+ '--sort-reverse',
+ action='store',
+ type=str,
+ default='',
+ dest='sort_rev',
+ help=_('Same as --sort but items are in reverse order.'),
+ )
+ parser.add_argument(
+ 'torrent_ids',
+ metavar='<torrent-id>',
+ nargs='*',
+ help=_('One or more torrent ids. If none is given, list all'),
+ )
+
+ def add_subparser(self, subparsers):
+ parser = subparsers.add_parser(
+ self.name,
+ prog=self.name,
+ help=self.__doc__,
+ description=self.__doc__,
+ epilog=self.epilog,
+ )
+ self.add_arguments(parser)
+
+ def handle(self, options):
+ self.console = component.get('ConsoleUI')
+ # Compile a list of torrent_ids to request the status of
+ torrent_ids = []
+
+ if options.torrent_ids:
+ for t_id in options.torrent_ids:
+ torrent_ids.extend(self.console.match_torrent(t_id))
+ else:
+ torrent_ids.extend(self.console.match_torrent(''))
+
+ def on_torrents_status(status):
+ # Print out the information for each torrent
+ sort_key = options.sort
+ sort_reverse = False
+ if not sort_key:
+ sort_key = options.sort_rev
+ sort_reverse = True
+ if not sort_key:
+ sort_key = 'name'
+ sort_reverse = False
+ if sort_key not in STATUS_KEYS:
+ self.console.write('')
+ self.console.write(
+ '{!error!}Unknown sort key: ' + sort_key + ', will sort on name'
+ )
+ sort_key = 'name'
+ sort_reverse = False
+ for key, value in sorted(
+ status.items(),
+ key=lambda x: x[1].get(sort_key),
+ reverse=sort_reverse,
+ ):
+ self.show_info(key, status[key], options.verbose, options.detailed)
+
+ def on_torrents_status_fail(reason):
+ self.console.write('{!error!}Error getting torrent info: %s' % reason)
+
+ status_dict = {'id': torrent_ids}
+
+ if options.state:
+ options.state = options.state.capitalize()
+ if options.state in STATES:
+ status_dict.state = options.state
+ else:
+ self.console.write('Invalid state: %s' % options.state)
+ self.console.write('Possible values are: %s.' % (', '.join(STATES)))
+ return
+
+ d = client.core.get_torrents_status(status_dict, STATUS_KEYS)
+ d.addCallback(on_torrents_status)
+ d.addErrback(on_torrents_status_fail)
+ return d
+
+ def show_file_info(self, torrent_id, status):
+ spaces_per_level = 2
+
+ if hasattr(self.console, 'screen'):
+ cols = self.console.screen.cols
+ else:
+ cols = 80
+
+ prevpath = []
+ for index, torrent_file in enumerate(status['files']):
+ filename = torrent_file['path'].split(dirsep)[-1]
+ filepath = torrent_file['path'].split(dirsep)[:-1]
+
+ for depth, subdir in enumerate(filepath):
+ indent = ' ' * depth * spaces_per_level
+ if depth >= len(prevpath):
+ self.console.write(f'{indent}{{!cyan!}}{subdir}')
+ elif subdir != prevpath[depth]:
+ self.console.write(f'{indent}{{!cyan!}}{subdir}')
+
+ depth = len(filepath)
+
+ indent = ' ' * depth * spaces_per_level
+
+ col_filename = indent + filename
+ col_size = ' ({!cyan!}%s{!input!})' % fsize(torrent_file['size'])
+ col_progress = ' {!input!}%.2f%%' % (status['file_progress'][index] * 100)
+
+ col_priority = ' {!info!}Priority: '
+
+ file_priority = FILE_PRIORITY[status['file_priorities'][index]]
+
+ if status['file_progress'][index] != 1.0:
+ if file_priority == 'Skip':
+ col_priority += '{!error!}'
+ else:
+ col_priority += '{!success!}'
+ else:
+ col_priority += '{!input!}'
+ col_priority += file_priority
+
+ def tlen(string):
+ return strwidth(remove_formatting(string))
+
+ col_all_info = col_size + col_progress + col_priority
+ # Check how much space we've got left after writing all the info
+ space_left = cols - tlen(col_all_info)
+ # And how much we will potentially have with the longest possible column
+ maxlen_space_left = cols - tlen(' (1000.0 MiB) 100.00% Priority: Normal')
+ if maxlen_space_left > tlen(col_filename) + 1:
+ # If there is enough space, pad it all nicely
+ col_all_info = ''
+ col_all_info += ' ('
+ spaces_to_add = tlen(' (1000.0 MiB)') - tlen(col_size)
+ col_all_info += ' ' * spaces_to_add
+ col_all_info += col_size[2:]
+ spaces_to_add = tlen(' 100.00%') - tlen(col_progress)
+ col_all_info += ' ' * spaces_to_add
+ col_all_info += col_progress
+ spaces_to_add = tlen(' Priority: Normal') - tlen(col_priority)
+ col_all_info += col_priority
+ col_all_info += ' ' * spaces_to_add
+ # And remember to put it to the left!
+ col_filename = pad_string(
+ col_filename, maxlen_space_left - 2, side='right'
+ )
+ elif space_left > tlen(col_filename) + 1:
+ # If there is enough space, put the info to the right
+ col_filename = pad_string(col_filename, space_left - 2, side='right')
+ else:
+ # And if there is not, shorten the name
+ col_filename = trim_string(col_filename, space_left, True)
+ self.console.write(col_filename + col_all_info)
+
+ prevpath = filepath
+
+ def show_peer_info(self, torrent_id, status):
+ if len(status['peers']) == 0:
+ self.console.write(' None')
+ else:
+ s = ''
+ for peer in status['peers']:
+ if peer['seed']:
+ s += '%sSeed\t{!input!}' % colors.state_color['Seeding']
+ else:
+ s += '%sPeer\t{!input!}' % colors.state_color['Downloading']
+
+ s += peer['country'] + '\t'
+
+ if peer['ip'].count(':') == 1:
+ # IPv4
+ s += peer['ip']
+ else:
+ # IPv6
+ s += '[{}]:{}'.format(
+ ':'.join(peer['ip'].split(':')[:-1]),
+ peer['ip'].split(':')[-1],
+ )
+
+ c = peer['client']
+ s += '\t' + c
+
+ if len(c) < 16:
+ s += '\t\t'
+ else:
+ s += '\t'
+ s += '{}{}\t{}{}'.format(
+ colors.state_color['Seeding'],
+ fspeed(peer['up_speed']),
+ colors.state_color['Downloading'],
+ fspeed(peer['down_speed']),
+ )
+ s += '\n'
+
+ self.console.write(s[:-1])
+
+ def show_info(self, torrent_id, status, verbose=False, detailed=False):
+ """
+ Writes out the torrents information to the screen.
+
+ Format depends on switches given.
+ """
+ self.console.set_batch_write(True)
+
+ if hasattr(self.console, 'screen'):
+ cols = self.console.screen.cols
+ else:
+ cols = 80
+
+ sep = ' '
+
+ if verbose or detailed:
+ self.console.write('{!info!}Name: {!input!}%s' % (status['name']))
+ self.console.write('{!info!}ID: {!input!}%s' % (torrent_id))
+ s = '{{!info!}}State: {}{}'.format(
+ colors.state_color[status['state']],
+ status['state'],
+ )
+ # Only show speed if active
+ if status['state'] in ('Seeding', 'Downloading'):
+ if status['state'] != 'Seeding':
+ s += sep
+ s += '{!info!}Down Speed: {!input!}%s' % fspeed(
+ status['download_payload_rate'], shortform=True
+ )
+ s += sep
+ s += '{!info!}Up Speed: {!input!}%s' % fspeed(
+ status['upload_payload_rate'], shortform=True
+ )
+ self.console.write(s)
+
+ if status['state'] in ('Seeding', 'Downloading', 'Queued'):
+ s = '{{!info!}}Seeds: {{!input!}}{} ({})'.format(
+ status['num_seeds'],
+ status['total_seeds'],
+ )
+ s += sep
+ s += '{{!info!}}Peers: {{!input!}}{} ({})'.format(
+ status['num_peers'],
+ status['total_peers'],
+ )
+ s += sep
+ s += (
+ '{!info!}Availability: {!input!}%.2f' % status['distributed_copies']
+ )
+ s += sep
+ s += '{!info!}Seed Rank: {!input!}%s' % f_seedrank_dash(
+ status['seed_rank'], status['seeding_time']
+ )
+ self.console.write(s)
+
+ total_done = fsize(status['total_done'], shortform=True)
+ total_size = fsize(status['total_size'], shortform=True)
+ if total_done == total_size:
+ s = '{!info!}Size: {!input!}%s' % (total_size)
+ else:
+ s = f'{{!info!}}Size: {{!input!}}{total_done}/{total_size}'
+ s += sep
+ s += '{!info!}Downloaded: {!input!}%s' % fsize(
+ status['all_time_download'], shortform=True
+ )
+ s += sep
+ s += '{!info!}Uploaded: {!input!}%s' % fsize(
+ status['total_uploaded'], shortform=True
+ )
+ s += sep
+ s += '{!info!}Share Ratio: {!input!}%.2f' % status['ratio']
+ self.console.write(s)
+
+ s = '{!info!}ETA: {!input!}%s' % format_time(status['eta'])
+ s += sep
+ s += '{!info!}Seeding: {!input!}%s' % format_time(status['seeding_time'])
+ s += sep
+ s += '{!info!}Active: {!input!}%s' % format_time(status['active_time'])
+ self.console.write(s)
+
+ s = '{!info!}Last Transfer: {!input!}%s' % format_time(
+ status['time_since_transfer']
+ )
+ s += sep
+ s += '{!info!}Complete Seen: {!input!}%s' % format_date_never(
+ status['last_seen_complete']
+ )
+ self.console.write(s)
+
+ s = '{!info!}Tracker: {!input!}%s' % status['tracker_host']
+ self.console.write(s)
+
+ self.console.write(
+ '{!info!}Tracker status: {!input!}%s' % status['tracker_status']
+ )
+
+ if not status['is_finished']:
+ pbar = f_progressbar(
+ status['progress'], cols - (13 + len('%.2f%%' % status['progress']))
+ )
+ s = '{{!info!}}Progress: {{!input!}}{:.2f}% {}'.format(
+ status['progress'], pbar
+ )
+ self.console.write(s)
+
+ s = '{!info!}Download Folder: {!input!}%s' % status['download_location']
+ self.console.write(s)
+
+ if 'label' in status:
+ s = '{!info!}Label: {!input!}%s' % status['label']
+ self.console.write(s)
+
+ if detailed:
+ self.console.write('\n{!info!}Files in torrent')
+ self.show_file_info(torrent_id, status)
+ self.console.write('{!info!}Connected peers')
+ self.show_peer_info(torrent_id, status)
+ else:
+ up_color = colors.state_color['Seeding']
+ down_color = colors.state_color['Downloading']
+
+ s = '{}{}'.format(
+ colors.state_color[status['state']],
+ '[' + status['state'][0] + ']',
+ )
+
+ s += ' {!info!}' + format_progress(status['progress']).rjust(6, ' ')
+ s += ' {!input!}%s' % (status['name'])
+
+ # Shorten the ID if it's necessary. Pretty hacky
+ # XXX: should make a nice function for it that can partition and shorten stuff
+ space_left = cols - strwidth('[S] 99.99% ' + status['name'])
+
+ if self.console.interactive and space_left >= len(sep + torrent_id):
+ # Not enough line space so shorten the hash (for interactive mode).
+ torrent_id = shorten_hash(torrent_id, space_left)
+ s += sep
+ s += '{!cyan!}%s' % torrent_id
+ self.console.write(s)
+
+ dl_info = '{!info!}DL: {!input!}'
+ dl_info += '%s' % ftotal_sized(
+ status['all_time_download'], status['total_payload_download']
+ )
+
+ if status['download_payload_rate'] > 0:
+ dl_info += ' @ {}{}'.format(
+ down_color,
+ fspeed(status['download_payload_rate'], shortform=True),
+ )
+
+ ul_info = ' {!info!}UL: {!input!}'
+ ul_info += '%s' % ftotal_sized(
+ status['total_uploaded'], status['total_payload_upload']
+ )
+ if status['upload_payload_rate'] > 0:
+ ul_info += ' @ {}{}'.format(
+ up_color,
+ fspeed(status['upload_payload_rate'], shortform=True),
+ )
+
+ eta = ' {!info!}ETA: {!magenta!}%s' % format_time(status['eta'])
+
+ self.console.write(' ' + dl_info + ul_info + eta + '\n')
+
+ self.console.set_batch_write(False)
+
+ def complete(self, line):
+ # We use the ConsoleUI torrent tab complete method
+ return component.get('ConsoleUI').tab_complete_torrent(line)
diff --git a/deluge/ui/console/cmdline/commands/manage.py b/deluge/ui/console/cmdline/commands/manage.py
new file mode 100644
index 0000000..e5ea9b2
--- /dev/null
+++ b/deluge/ui/console/cmdline/commands/manage.py
@@ -0,0 +1,114 @@
+#
+# Copyright (C) 2008-2009 Ido Abramovich <ido.deluge@gmail.com>
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+import logging
+
+from twisted.internet import defer
+
+import deluge.component as component
+from deluge.ui.client import client
+from deluge.ui.console.utils.common import TORRENT_OPTIONS
+
+from . import BaseCommand
+
+log = logging.getLogger(__name__)
+
+
+class Command(BaseCommand):
+ """Show and manage per-torrent options"""
+
+ usage = _('Usage: manage <torrent-id> [--set <key> <value>] [<key> [<key>...] ]')
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ 'torrent',
+ metavar='<torrent>',
+ help=_('an expression matched against torrent ids and torrent names'),
+ )
+ set_group = parser.add_argument_group('setting a value')
+ set_group.add_argument(
+ '-s',
+ '--set',
+ action='store',
+ metavar='<key>',
+ help=_('set value for this key'),
+ )
+ set_group.add_argument(
+ 'values', metavar='<value>', nargs='+', help=_('Value to set')
+ )
+ get_group = parser.add_argument_group('getting values')
+ get_group.add_argument(
+ 'keys',
+ metavar='<keys>',
+ nargs='*',
+ help=_('one or more keys separated by space'),
+ )
+
+ def handle(self, options):
+ self.console = component.get('ConsoleUI')
+ if options.set:
+ return self._set_option(options)
+ else:
+ return self._get_option(options)
+
+ def _get_option(self, options):
+ def on_torrents_status(status):
+ for torrentid, data in status.items():
+ self.console.write('')
+ if 'name' in data:
+ self.console.write('{!info!}Name: {!input!}%s' % data.get('name'))
+ self.console.write('{!info!}ID: {!input!}%s' % torrentid)
+ for k, v in data.items():
+ if k != 'name':
+ self.console.write(f'{{!info!}}{k}: {{!input!}}{v}')
+
+ def on_torrents_status_fail(reason):
+ self.console.write('{!error!}Failed to get torrent data.')
+
+ torrent_ids = self.console.match_torrent(options.torrent)
+
+ request_options = []
+ for opt in options.values:
+ if opt not in TORRENT_OPTIONS:
+ self.console.write('{!error!}Unknown torrent option: %s' % opt)
+ return
+ request_options.append(opt)
+ if not request_options:
+ request_options = list(TORRENT_OPTIONS)
+ request_options.append('name')
+
+ d = client.core.get_torrents_status({'id': torrent_ids}, request_options)
+ d.addCallbacks(on_torrents_status, on_torrents_status_fail)
+ return d
+
+ def _set_option(self, options):
+ deferred = defer.Deferred()
+ key = options.set
+ val = ' '.join(options.values)
+ torrent_ids = self.console.match_torrent(options.torrent)
+
+ if key not in TORRENT_OPTIONS:
+ self.console.write('{!error!}Invalid key: %s' % key)
+ return
+
+ val = TORRENT_OPTIONS[key](val)
+
+ def on_set_config(result):
+ self.console.write('{!success!}Torrent option successfully updated.')
+ deferred.callback(True)
+
+ self.console.write(f'Setting {key} to {val} for torrents {torrent_ids}..')
+ client.core.set_torrent_options(torrent_ids, {key: val}).addCallback(
+ on_set_config
+ )
+ return deferred
+
+ def complete(self, line):
+ # We use the ConsoleUI torrent tab complete method
+ return component.get('ConsoleUI').tab_complete_torrent(line)
diff --git a/deluge/ui/console/cmdline/commands/move.py b/deluge/ui/console/cmdline/commands/move.py
new file mode 100644
index 0000000..67ee0af
--- /dev/null
+++ b/deluge/ui/console/cmdline/commands/move.py
@@ -0,0 +1,94 @@
+#
+# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+import logging
+import os.path
+
+import deluge.component as component
+from deluge.ui.client import client
+
+from . import BaseCommand
+
+log = logging.getLogger(__name__)
+
+
+class Command(BaseCommand):
+ """Move torrents' storage location"""
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ 'torrent_ids',
+ metavar='<torrent-id>',
+ nargs='+',
+ help=_('One or more torrent ids'),
+ )
+ parser.add_argument(
+ 'path', metavar='<path>', help=_('The path to move the torrents to')
+ )
+
+ def handle(self, options):
+ self.console = component.get('ConsoleUI')
+
+ if os.path.exists(options.path) and not os.path.isdir(options.path):
+ self.console.write(
+ '{!error!}Cannot Move Download Folder: %s exists and is not a directory'
+ % options.path
+ )
+ return
+
+ ids = []
+ names = []
+ for t_id in options.torrent_ids:
+ tid = self.console.match_torrent(t_id)
+ ids.extend(tid)
+ names.append(self.console.get_torrent_name(tid))
+
+ def on_move(res):
+ msg = 'Moved "{}" to {}'.format(', '.join(names), options.path)
+ self.console.write(msg)
+ log.info(msg)
+
+ d = client.core.move_storage(ids, options.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
diff --git a/deluge/ui/console/cmdline/commands/pause.py b/deluge/ui/console/cmdline/commands/pause.py
new file mode 100644
index 0000000..1334242
--- /dev/null
+++ b/deluge/ui/console/cmdline/commands/pause.py
@@ -0,0 +1,45 @@
+#
+# Copyright (C) 2008-2009 Ido Abramovich <ido.deluge@gmail.com>
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+import deluge.component as component
+from deluge.ui.client import client
+
+from . import BaseCommand
+
+
+class Command(BaseCommand):
+ """Pause torrents"""
+
+ usage = 'pause [ * | <torrent-id> [<torrent-id> ...] ]'
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ 'torrent_ids',
+ metavar='<torrent-id>',
+ nargs='+',
+ help=_('One or more torrent ids. Use "*" to pause all torrents'),
+ )
+
+ def handle(self, options):
+ self.console = component.get('ConsoleUI')
+
+ if options.torrent_ids[0] == '*':
+ client.core.pause_session()
+ return
+
+ torrent_ids = []
+ for arg in options.torrent_ids:
+ torrent_ids.extend(self.console.match_torrent(arg))
+
+ if torrent_ids:
+ return client.core.pause_torrent(torrent_ids)
+
+ def complete(self, line):
+ # We use the ConsoleUI torrent tab complete method
+ return component.get('ConsoleUI').tab_complete_torrent(line)
diff --git a/deluge/ui/console/cmdline/commands/plugin.py b/deluge/ui/console/cmdline/commands/plugin.py
new file mode 100644
index 0000000..c424cb2
--- /dev/null
+++ b/deluge/ui/console/cmdline/commands/plugin.py
@@ -0,0 +1,140 @@
+#
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+import deluge.component as component
+import deluge.configmanager
+from deluge.ui.client import client
+
+from . import BaseCommand
+
+
+class Command(BaseCommand):
+ """Manage plugins"""
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ '-l',
+ '--list',
+ action='store_true',
+ default=False,
+ dest='list',
+ help=_('Lists available plugins'),
+ )
+ parser.add_argument(
+ '-s',
+ '--show',
+ action='store_true',
+ default=False,
+ dest='show',
+ help=_('Shows enabled plugins'),
+ )
+ parser.add_argument(
+ '-e', '--enable', dest='enable', nargs='+', help=_('Enables a plugin')
+ )
+ parser.add_argument(
+ '-d', '--disable', dest='disable', nargs='+', help=_('Disables a plugin')
+ )
+ parser.add_argument(
+ '-r',
+ '--reload',
+ action='store_true',
+ default=False,
+ dest='reload',
+ help=_('Reload list of available plugins'),
+ )
+ parser.add_argument(
+ '-i', '--install', help=_('Install a plugin from an .egg file')
+ )
+
+ def handle(self, options):
+ self.console = component.get('ConsoleUI')
+
+ if options.reload:
+ client.core.pluginmanager.rescan_plugins()
+ self.console.write('{!green!}Plugin list successfully reloaded')
+ return
+
+ elif options.list:
+
+ def on_available_plugins(result):
+ self.console.write('{!info!}Available Plugins:')
+ for p in result:
+ self.console.write('{!input!} ' + p)
+
+ return client.core.get_available_plugins().addCallback(on_available_plugins)
+
+ elif options.show:
+
+ def on_enabled_plugins(result):
+ self.console.write('{!info!}Enabled Plugins:')
+ for p in result:
+ self.console.write('{!input!} ' + p)
+
+ return client.core.get_enabled_plugins().addCallback(on_enabled_plugins)
+
+ elif options.enable:
+
+ def on_available_plugins(result):
+ plugins = {}
+ for p in result:
+ plugins[p.lower()] = p
+ for arg in options.enable:
+ if arg.lower() in plugins:
+ client.core.enable_plugin(plugins[arg.lower()])
+
+ return client.core.get_available_plugins().addCallback(on_available_plugins)
+
+ elif options.disable:
+
+ def on_enabled_plugins(result):
+ plugins = {}
+ for p in result:
+ plugins[p.lower()] = p
+ for arg in options.disable:
+ if arg.lower() in plugins:
+ client.core.disable_plugin(plugins[arg.lower()])
+
+ return client.core.get_enabled_plugins().addCallback(on_enabled_plugins)
+
+ elif options.install:
+ import os.path
+ import shutil
+ from base64 import b64encode
+
+ filepath = options.install
+
+ if not os.path.exists(filepath):
+ self.console.write('{!error!}Invalid path: %s' % filepath)
+ return
+
+ config_dir = deluge.configmanager.get_config_dir()
+ filename = os.path.split(filepath)[1]
+ shutil.copyfile(filepath, os.path.join(config_dir, 'plugins', filename))
+
+ client.core.rescan_plugins()
+
+ if not client.is_localhost():
+ # We need to send this plugin to the daemon
+ with open(filepath, 'rb') as _file:
+ filedump = b64encode(_file.read())
+ try:
+ client.core.upload_plugin(filename, filedump)
+ client.core.rescan_plugins()
+ except Exception:
+ self.console.write(
+ '{!error!}An error occurred, plugin was not installed'
+ )
+
+ self.console.write(
+ '{!green!}Plugin was successfully installed: %s' % filename
+ )
+
+ def complete(self, line):
+ return component.get('ConsoleUI').tab_complete_path(
+ line, ext='.egg', sort='name', dirs_first=-1
+ )
diff --git a/deluge/ui/console/cmdline/commands/quit.py b/deluge/ui/console/cmdline/commands/quit.py
new file mode 100644
index 0000000..4459dfc
--- /dev/null
+++ b/deluge/ui/console/cmdline/commands/quit.py
@@ -0,0 +1,22 @@
+#
+# Copyright (C) 2008-2009 Ido Abramovich <ido.deluge@gmail.com>
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+import deluge.component as component
+
+from . import BaseCommand
+
+
+class Command(BaseCommand):
+ """Exit the client"""
+
+ aliases = ['exit']
+ interactive_only = True
+
+ def handle(self, options):
+ component.get('ConsoleUI').quit()
diff --git a/deluge/ui/console/cmdline/commands/recheck.py b/deluge/ui/console/cmdline/commands/recheck.py
new file mode 100644
index 0000000..046cb0b
--- /dev/null
+++ b/deluge/ui/console/cmdline/commands/recheck.py
@@ -0,0 +1,44 @@
+#
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+import deluge.component as component
+from deluge.ui.client import client
+
+from . import BaseCommand
+
+
+class Command(BaseCommand):
+ """Forces a recheck of the torrent data"""
+
+ usage = 'recheck [ * | <torrent-id> [<torrent-id> ...] ]'
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ 'torrent_ids',
+ metavar='<torrent-id>',
+ nargs='+',
+ help=_('One or more torrent ids'),
+ )
+
+ def handle(self, options):
+ self.console = component.get('ConsoleUI')
+
+ if options.torrent_ids[0] == '*':
+ client.core.force_recheck(self.console.match_torrent(''))
+ return
+
+ torrent_ids = []
+ for arg in options.torrent_ids:
+ torrent_ids.extend(self.console.match_torrent(arg))
+
+ if torrent_ids:
+ return client.core.force_recheck(torrent_ids)
+
+ def complete(self, line):
+ # We use the ConsoleUI torrent tab complete method
+ return component.get('ConsoleUI').tab_complete_torrent(line)
diff --git a/deluge/ui/console/cmdline/commands/resume.py b/deluge/ui/console/cmdline/commands/resume.py
new file mode 100644
index 0000000..27b8528
--- /dev/null
+++ b/deluge/ui/console/cmdline/commands/resume.py
@@ -0,0 +1,45 @@
+#
+# Copyright (C) 2008-2009 Ido Abramovich <ido.deluge@gmail.com>
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+import deluge.component as component
+from deluge.ui.client import client
+
+from . import BaseCommand
+
+
+class Command(BaseCommand):
+ """Resume torrents"""
+
+ usage = _('Usage: resume [ * | <torrent-id> [<torrent-id> ...] ]')
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ 'torrent_ids',
+ metavar='<torrent-id>',
+ nargs='+',
+ help=_('One or more torrent ids. Use "*" to resume all torrents'),
+ )
+
+ def handle(self, options):
+ self.console = component.get('ConsoleUI')
+
+ if options.torrent_ids[0] == '*':
+ client.core.resume_session()
+ return
+
+ torrent_ids = []
+ for t_id in options.torrent_ids:
+ torrent_ids.extend(self.console.match_torrent(t_id))
+
+ if torrent_ids:
+ return client.core.resume_torrent(torrent_ids)
+
+ def complete(self, line):
+ # We use the ConsoleUI torrent tab complete method
+ return component.get('ConsoleUI').tab_complete_torrent(line)
diff --git a/deluge/ui/console/cmdline/commands/rm.py b/deluge/ui/console/cmdline/commands/rm.py
new file mode 100644
index 0000000..4a3fd00
--- /dev/null
+++ b/deluge/ui/console/cmdline/commands/rm.py
@@ -0,0 +1,82 @@
+#
+# Copyright (C) 2008-2009 Ido Abramovich <ido.deluge@gmail.com>
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+import logging
+
+import deluge.component as component
+from deluge.ui.client import client
+
+from . import BaseCommand
+
+log = logging.getLogger(__name__)
+
+
+class Command(BaseCommand):
+ """Remove a torrent"""
+
+ aliases = ['del']
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ '--remove_data',
+ action='store_true',
+ default=False,
+ help=_('Also removes the torrent data'),
+ )
+ parser.add_argument(
+ '-c',
+ '--confirm',
+ action='store_true',
+ default=False,
+ help=_('List the matching torrents without removing.'),
+ )
+ parser.add_argument(
+ 'torrent_ids',
+ metavar='<torrent-id>',
+ nargs='+',
+ help=_('One or more torrent ids'),
+ )
+
+ def handle(self, options):
+ self.console = component.get('ConsoleUI')
+ torrent_ids = self.console.match_torrents(options.torrent_ids)
+
+ if not options.confirm:
+ self.console.write(
+ '{!info!}%d %s %s{!info!}'
+ % (
+ len(torrent_ids),
+ _n('torrent', 'torrents', len(torrent_ids)),
+ _n('match', 'matches', len(torrent_ids)),
+ )
+ )
+ for t_id in torrent_ids:
+ name = self.console.get_torrent_name(t_id)
+ self.console.write('* %-50s (%s)' % (name, t_id))
+ self.console.write(
+ _('Confirm with -c to remove the listed torrents (Count: %d)')
+ % len(torrent_ids)
+ )
+ return
+
+ def on_removed_finished(errors):
+ if errors:
+ self.console.write(
+ 'Error(s) occurred when trying to delete torrent(s).'
+ )
+ for t_id, e_msg in errors:
+ self.console.write(f'Error removing torrent {t_id} : {e_msg}')
+
+ log.info('Removing %d torrents', len(torrent_ids))
+ d = client.core.remove_torrents(torrent_ids, options.remove_data)
+ d.addCallback(on_removed_finished)
+
+ def complete(self, line):
+ # We use the ConsoleUI torrent tab complete method
+ return component.get('ConsoleUI').tab_complete_torrent(line)
diff --git a/deluge/ui/console/cmdline/commands/status.py b/deluge/ui/console/cmdline/commands/status.py
new file mode 100644
index 0000000..05c9796
--- /dev/null
+++ b/deluge/ui/console/cmdline/commands/status.py
@@ -0,0 +1,116 @@
+#
+# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+import logging
+
+from twisted.internet import defer
+
+import deluge.component as component
+from deluge.common import TORRENT_STATE, fspeed
+from deluge.ui.client import client
+
+from . import BaseCommand
+
+log = logging.getLogger(__name__)
+
+
+class Command(BaseCommand):
+ """Shows various status information from the daemon"""
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ '-r',
+ '--raw',
+ action='store_true',
+ default=False,
+ dest='raw',
+ help=_(
+ 'Raw values for upload/download rates (without KiB/s suffix)'
+ '(useful for scripts that want to do their own parsing)'
+ ),
+ )
+ parser.add_argument(
+ '-n',
+ '--no-torrents',
+ action='store_false',
+ default=True,
+ dest='show_torrents',
+ help=_('Do not show torrent status (Improves command speed)'),
+ )
+
+ def handle(self, options):
+ self.console = component.get('ConsoleUI')
+ self.status = None
+ self.torrents = 1 if options.show_torrents else 0
+ self.raw = options.raw
+
+ def on_session_status(status):
+ self.status = status
+
+ def on_torrents_status(status):
+ self.torrents = status
+
+ def on_torrents_status_fail(reason):
+ log.warning('Failed to retrieve session status: %s', reason)
+ self.torrents = -2
+
+ deferreds = []
+
+ ds = client.core.get_session_status(
+ [
+ 'peer.num_peers_connected',
+ 'payload_upload_rate',
+ 'payload_download_rate',
+ 'dht.dht_nodes',
+ ]
+ )
+ ds.addCallback(on_session_status)
+ deferreds.append(ds)
+
+ 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).addCallback(self.print_status)
+
+ def print_status(self, *args):
+ 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' % fspeed(self.status['payload_upload_rate'])
+ )
+ self.console.write(
+ '{!info!}Total download: %s'
+ % fspeed(self.status['payload_download_rate'])
+ )
+ self.console.write('{!info!}DHT Nodes: %i' % self.status['dht.dht_nodes'])
+
+ if isinstance(self.torrents, int):
+ if self.torrents == -2:
+ self.console.write('{!error!}Error getting torrent info')
+ else:
+ self.console.write('{!info!}Total torrents: %i' % len(self.torrents))
+ state_counts = {}
+ for state in TORRENT_STATE:
+ state_counts[state] = 0
+ for t in self.torrents:
+ s = self.torrents[t]
+ state_counts[s['state']] += 1
+ for state in TORRENT_STATE:
+ self.console.write('{!info!} %s: %i' % (state, state_counts[state]))
+
+ self.console.set_batch_write(False)
diff --git a/deluge/ui/console/cmdline/commands/update_tracker.py b/deluge/ui/console/cmdline/commands/update_tracker.py
new file mode 100644
index 0000000..c05569d
--- /dev/null
+++ b/deluge/ui/console/cmdline/commands/update_tracker.py
@@ -0,0 +1,44 @@
+#
+# Copyright (C) 2008-2009 Ido Abramovich <ido.deluge@gmail.com>
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+import deluge.component as component
+from deluge.ui.client import client
+
+from . import BaseCommand
+
+
+class Command(BaseCommand):
+ """Update tracker for torrent(s)"""
+
+ usage = 'update_tracker [ * | <torrent-id> [<torrent-id> ...] ]'
+ aliases = ['reannounce']
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ 'torrent_ids',
+ metavar='<torrent-id>',
+ nargs='+',
+ help='One or more torrent ids. "*" updates all torrents',
+ )
+
+ def handle(self, options):
+ self.console = component.get('ConsoleUI')
+ args = options.torrent_ids
+ if options.torrent_ids[0] == '*':
+ 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)