diff options
Diffstat (limited to 'deluge/ui/console/cmdline')
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) |