def __init__(self): """ Initialize rtcontrol. """ super(RtorrentControl, self).__init__() self.prompt = PromptDecorator(self) self.plain_output_format = False
class RtorrentControl(ScriptBaseWithConfig): ### Keep things wrapped to fit under this comment... ############################## """ Control and inspect rTorrent from the command line. Filter expressions take the form "<field>=<value>", and all expressions must be met (AND). If a field name is omitted, "name" is assumed. You can also use uppercase OR to build a list of alternative conditions. For numeric fields, a leading "+" means greater than, a leading "-" means less than. For string fields, the value is a glob pattern (*, ?, [a-z], [!a-z]), or a regex match enclosed by slashes. All string comparisons are case-ignoring. Multiple values separated by a comma indicate several possible choices (OR). "!" in front of a filter value negates it (NOT). See https://pyrocore.readthedocs.io/en/latest/usage.html#rtcontrol for more. Examples: - All 1:1 seeds ratio=+1 - All active torrents xfer=+0 - All seeding torrents up=+0 - Slow torrents down=+0 down=-5k - Older than 2 weeks completed=+2w - Big stuff size=+4g - 1:1 seeds not on NAS ratio=+1 'realpath=!/mnt/*' - Music kind=flac,mp3 """ # argument description for the usage information ARGS_HELP = "<filter>..." # additonal stuff appended after the command handler's docstring ADDITIONAL_HELP = [ "", "", "Use --help to get a list of all options.", "Use --help-fields to list all fields and their description.", ] # additional values for output formatting FORMATTER_DEFAULTS = dict(now=time.time(), ) # choices for --ignore IGNORE_OPTIONS = ('0', '1') # choices for --prio PRIO_OPTIONS = ('0', '1', '2', '3') # choices for --alter ALTER_MODES = ('append', 'remove') # action options that perform some change on selected items ACTION_MODES = ( Bunch(name="start", options=("--start", ), help="start torrent"), Bunch(name="close", options=("--close", "--stop"), help="stop torrent", method="stop"), Bunch(name="hash_check", label="HASH", options=("-H", "--hash-check"), help="hash-check torrent", interactive=True), # TODO: Bunch(name="announce", options=("--announce",), help="announce right now", interactive=True), # TODO: --pause, --resume? # TODO: implement --clean-partial #self.add_bool_option("--clean-partial", # help="remove partially downloaded 'off'ed files (also stops downloads)") Bunch(name="delete", options=("--delete", ), help="remove torrent from client", interactive=True), Bunch(name="purge", options=("--purge", "--delete-partial"), help="delete PARTIAL data files and remove torrent from client", interactive=True), Bunch(name="cull", options=("--cull", "--exterminate", "--delete-all"), help="delete ALL data files and remove torrent from client", interactive=True), Bunch( name="throttle", options=( "-T", "--throttle", ), argshelp="NAME", method="set_throttle", help="assign to named throttle group (NULL=unlimited, NONE=global)", interactive=True), Bunch(name="tag", options=("--tag", ), argshelp='"TAG +TAG -TAG..."', help="add or remove tag(s)", interactive=False), Bunch(name="custom", label="SET_CUSTOM", options=("--custom", ), argshelp='KEY=VALUE', method="set_custom", help="set value of 'custom_KEY' field (KEY might also be 1..5)", interactive=False), Bunch(name="exec", label="EXEC", options=("--exec", "--xmlrpc"), argshelp='CMD', method="execute", help="execute XMLRPC command pattern", interactive=True), # TODO: --move / --link output_format / the formatted result is the target path # if the target contains a '//' in place of a '/', directories # after that are auto-created # "--move tracker_dated", with a custom output format # like "tracker_dated = ~/done//$(alias)s/$(completed).7s", # will move to ~/done/OBT/2010-08 for example # self.add_value_option("--move", "TARGET", # help="move data to given target directory (implies -i, can be combined with --delete)") # TODO: --copy, and --move/--link across devices ) def __init__(self): """ Initialize rtcontrol. """ super(RtorrentControl, self).__init__() self.prompt = PromptDecorator(self) self.plain_output_format = False self.raw_output_format = None def add_options(self): """ Add program options. """ super(RtorrentControl, self).add_options() # basic options self.add_bool_option( "--help-fields", help="show available fields and their description") self.add_bool_option( "-n", "--dry-run", help="don't commit changes, just tell what would happen") self.add_bool_option("--detach", help="run the process in the background") self.prompt.add_options() # output control self.add_bool_option("-S", "--shell", help="escape output following shell rules") self.add_bool_option( "-0", "--nul", "--print0", help="use a NUL character instead of a linebreak after items") self.add_bool_option("-c", "--column-headers", help="print column headers") self.add_bool_option("-+", "--stats", help="add sum / avg / median of numerical fields") self.add_bool_option( "--summary", help="print only statistical summary, without the items") #self.add_bool_option("-f", "--full", # help="print full torrent details") self.add_bool_option( "--json", help= "dump default fields of all items as JSON (use '-o f1,f2,...' to specify fields)" ) self.add_value_option( "-o", "--output-format", "FORMAT", help="specify display format (use '-o-' to disable item display)") self.add_value_option( "-O", "--output-template", "FILE", help="pass control of output formatting to the specified template") self.add_value_option( "-s", "--sort-fields", "[-]FIELD[,...] [-s...]", action='append', default=[], help= "fields used for sorting, descending if prefixed with a '-'; '-s*' uses output field list" ) self.add_bool_option("-r", "--reverse-sort", help="reverse the sort order") self.add_value_option( "-A", "--anneal", "MODE [-A...]", type='choice', action='append', default=[], choices=('dupes+', 'dupes-', 'dupes=', 'invert', 'unique'), help="modify result set using some pre-defined methods") self.add_value_option( "-/", "--select", "[N-]M", help="select result subset by item position (counting from 1)") self.add_bool_option( "-V", "--view-only", help="show search result only in default ncurses view") self.add_value_option( "--to-view", "--to", "NAME", help="show search result only in named ncurses view") self.add_bool_option("--append-view", "--append", help="DEPRECATED: use '--alter append' instead") self.add_value_option( "--alter-view", "--alter", "MODE", type='choice', default=None, choices=self.ALTER_MODES, help= "alter view according to mode: {} (modifies -V and --to behaviour)" .format(', '.join(self.ALTER_MODES))) self.add_bool_option( "--tee-view", "--tee", help= "ADDITIONALLY show search results in ncurses view (modifies -V and --to behaviour)" ) self.add_value_option( "--from-view", "--from", "NAME", help= "select only items that are on view NAME (NAME can be an info hash to quickly select a single item)" ) self.add_value_option( "-M", "--modify-view", "NAME", help= "get items from given view and write result back to it (short-cut to combine --from-view and --to-view)" ) self.add_value_option( "-Q", "--fast-query", "LEVEL", type='choice', default='=', choices=('=', '0', '1', '2'), help= "enable query optimization (=: use config; 0: off; 1: safe; 2: danger seeker)" ) self.add_value_option("--call", "CMD", help="call an OS command pattern in the shell") self.add_value_option("--spawn", "CMD [--spawn ...]", action="append", default=[], help="execute OS command pattern(s) directly") # TODO: implement -S # self.add_bool_option("-S", "--summary", # help="print statistics") # torrent state change (actions) for action in self.ACTION_MODES: action.setdefault("label", action.name.upper()) action.setdefault("method", action.name) action.setdefault("interactive", False) action.setdefault("argshelp", "") action.setdefault("args", ()) if action.argshelp: self.add_value_option( *action.options + (action.argshelp, ), **{ "help": action.help + (" (implies -i)" if action.interactive else "") }) else: self.add_bool_option( *action.options, **{ "help": action.help + (" (implies -i)" if action.interactive else "") }) self.add_value_option("--ignore", "|".join(self.IGNORE_OPTIONS), type="choice", choices=self.IGNORE_OPTIONS, help="set 'ignore commands' status on torrent") self.add_value_option("--prio", "|".join(self.PRIO_OPTIONS), type="choice", choices=self.PRIO_OPTIONS, help="set priority of torrent") self.add_bool_option( "-F", "--flush", help="flush changes immediately (save session data)") def help_completion_fields(self): """ Return valid field names. """ for name, field in sorted(engine.FieldDefinition.FIELDS.items()): if issubclass(field._matcher, matching.BoolFilter): yield "%s=no" % (name, ) yield "%s=yes" % (name, ) continue elif issubclass(field._matcher, matching.PatternFilter): yield "%s=" % (name, ) yield "%s=/" % (name, ) yield "%s=?" % (name, ) yield "%s=\"'*'\"" % (name, ) continue elif issubclass(field._matcher, matching.NumericFilterBase): for i in range(10): yield "%s=%d" % (name, i) else: yield "%s=" % (name, ) yield r"%s=+" % (name, ) yield r"%s=-" % (name, ) yield "custom_" yield "kind_" # TODO: refactor to engine.TorrentProxy as format() method def format_item(self, item, defaults=None, stencil=None): """ Format an item. """ from pyrobase.osutil import shell_escape try: item_text = fmt.to_console( formatting.format_item(self.options.output_format, item, defaults)) except (NameError, ValueError, TypeError), exc: self.fatal( "Trouble with formatting item %r\n\n FORMAT = %r\n\n REASON =" % (item, self.options.output_format), exc) raise # in --debug mode if self.options.shell: item_text = '\t'.join( shell_escape(i) for i in item_text.split('\t')) # Justify headers according to stencil if stencil: item_text = '\t'.join( i.ljust(len(s)) for i, s in zip(item_text.split('\t'), stencil)) return item_text
class RtorrentControl(ScriptBaseWithConfig): ### Keep things wrapped to fit under this comment... ############################## """ Control and inspect rTorrent from the command line. Filter expressions take the form "<field>=<value>", and all expressions must be met (AND). If a field name is omitted, "name" is assumed. You can also use uppercase OR to build a list of alternative conditions. For numeric fields, a leading "+" means greater than, a leading "-" means less than. For string fields, the value is a glob pattern (*, ?, [a-z], [!a-z]). Multiple values separated by a comma indicate several possible choices (OR). "!" in front of a filter value negates it (NOT). Examples: - All 1:1 seeds ratio=+1 - All active torrents xfer=+0 - All seeding torrents up=+0 - Slow torrents down=+0 down=-5k - Older than 2 weeks completed=+2w - Big stuff size=+4g - 1:1 seeds not on NAS ratio=+1 'realpath=!/mnt/*' - Music kind=flac,mp3 """ # argument description for the usage information ARGS_HELP = "<filter>..." # additonal stuff appended after the command handler's docstring ADDITIONAL_HELP = ["", "", "Use --help to get a list of all options.", "Use --help-fields to list all fields and their description.", ] # additional values for output formatting FORMATTER_DEFAULTS = dict( now = time.time(), ) # choices for --ignore IGNORE_OPTIONS = ('0', '1') # choices for --prio PRIO_OPTIONS = ('0', '1', '2', '3') # action options that perform some change on selected items ACTION_MODES = ( Bunch(name="start", options=("--start",), help="start torrent"), Bunch(name="close", options=("--close", "--stop"), help="stop torrent", method="stop"), Bunch(name="hash_check", label="HASH", options=("-H", "--hash-check"), help="hash-check torrent", interactive=True), # TODO: Bunch(name="announce", options=("--announce",), help="announce right now", interactive=True), # TODO: --pause, --resume? # TODO: implement --clean-partial #self.add_bool_option("--clean-partial", # help="remove partially downloaded 'off'ed files (also stops downloads)") Bunch(name="delete", options=("--delete",), help="remove torrent from client", interactive=True), Bunch(name="purge", options=("--purge", "--delete-partial"), help="delete PARTIAL data files and remove torrent from client", interactive=True), Bunch(name="cull", options=("--cull", "--exterminate", "--delete-all"), help="delete ALL data files and remove torrent from client", interactive=True), Bunch(name="throttle", options=("-T", "--throttle",), argshelp="NAME", method="set_throttle", help="assign to named throttle group (NULL=unlimited, NONE=global)", interactive=True), Bunch(name="tag", options=("--tag",), argshelp='"TAG +TAG -TAG..."', help="add or remove tag(s)", interactive=False), Bunch(name="custom", label="SET_CUSTOM", options=("--custom",), argshelp='KEY=VALUE', method="set_custom", help="set value of 'custom_KEY' field (KEY might also be 1..5)", interactive=False), Bunch(name="exec", label="EXEC", options=("--exec", "--xmlrpc"), argshelp='CMD', method="execute", help="execute XMLRPC command pattern", interactive=True), # TODO: --with-dupes / -D Include any items that reference the same data paths as the selected ones # TODO: --no-dupes / --unique / -U Exclude any items that reference the same data paths as any other (from deletion, etc.) # TODO: --move / --link output_format / the formatted result is the target path # if the target contains a '//' in place of a '/', directories # after that are auto-created # "--move tracker_dated", with a custom output format # like "tracker_dated = ~/done//$(alias)s/$(completed).7s", # will move to ~/done/OBT/2010-08 for example # self.add_value_option("--move", "TARGET", # help="move data to given target directory (implies -i, can be combined with --delete)") # TODO: --copy, and --move/--link across devices ) def __init__(self): """ Initialize rtcontrol. """ super(RtorrentControl, self).__init__() self.prompt = PromptDecorator(self) self.plain_output_format = False def add_options(self): """ Add program options. """ super(RtorrentControl, self).add_options() # basic options self.add_bool_option("--help-fields", help="show available fields and their description") self.add_bool_option("-n", "--dry-run", help="don't commit changes, just tell what would happen") self.add_bool_option("--detach", help="run the process in the background") self.prompt.add_options() # output control self.add_bool_option("-S", "--shell", help="escape output following shell rules") self.add_bool_option("-0", "--nul", "--print0", help="use a NUL character instead of a linebreak after items") self.add_bool_option("-c", "--column-headers", help="print column headers") self.add_bool_option("-+", "--stats", help="add sum / avg / median of numerical fields") self.add_bool_option("--summary", help="print only statistical summary, without the items") #self.add_bool_option("-f", "--full", # help="print full torrent details") self.add_bool_option("--json", help="dump all items as JSON (use '-o f1,f2,...' to specify fields)") self.add_value_option("-o", "--output-format", "FORMAT", help="specify display format (use '-o-' to disable item display)") self.add_value_option("-O", "--output-template", "FILE", help="pass control of output formatting to the specified template") self.add_value_option("-s", "--sort-fields", "[-]FIELD[,...]", help="fields used for sorting, descending if prefixed with a '-'; '-s*' uses output field list") self.add_bool_option("-r", "--reverse-sort", help="reverse the sort order") self.add_value_option("-/", "--select", "[N-]M", help="select result subset by item position (counting from 1)") self.add_bool_option("-V", "--view-only", help="show search result only in default ncurses view") self.add_value_option("--to-view", "NAME", help="show search result only in named ncurses view") self.add_bool_option("--tee-view", help="ADDITIONALLY show search results in ncurses view (modifies -V and --to-view behaviour)") self.add_value_option("--from-view", "NAME", help="select only items that are on view NAME") self.add_value_option("-M", "--modify-view", "NAME", help="get items from given view and write result back to it (short-cut to combine --from-view and --to-view)") self.add_value_option("--call", "CMD", help="call an OS command pattern in the shell") self.add_value_option("--spawn", "CMD [--spawn ...]", action="append", default=[], help="execute OS command pattern(s) directly") # TODO: implement -S # self.add_bool_option("-S", "--summary", # help="print statistics") # torrent state change (actions) for action in self.ACTION_MODES: action.setdefault("label", action.name.upper()) action.setdefault("method", action.name) action.setdefault("interactive", False) action.setdefault("argshelp", "") action.setdefault("args", ()) if action.argshelp: self.add_value_option(*action.options + (action.argshelp,), **{"help": action.help + (" (implies -i)" if action.interactive else "")}) else: self.add_bool_option(*action.options, **{"help": action.help + (" (implies -i)" if action.interactive else "")}) self.add_value_option("--ignore", "|".join(self.IGNORE_OPTIONS), type="choice", choices=self.IGNORE_OPTIONS, help="set 'ignore commands' status on torrent") self.add_value_option("--prio", "|".join(self.PRIO_OPTIONS), type="choice", choices=self.PRIO_OPTIONS, help="set priority of torrent") self.add_bool_option("-F", "--flush", help="flush changes immediately (save session data)") def help_completion_fields(self): """ Return valid field names. """ for name, field in sorted(engine.FieldDefinition.FIELDS.items()): if issubclass(field._matcher, matching.BoolFilter): yield "%s=no" % (name,) yield "%s=yes" % (name,) continue elif issubclass(field._matcher, matching.PatternFilter): yield "%s=" % (name,) yield "%s=/" % (name,) yield "%s=?" % (name,) yield "%s=\"'*'\"" % (name,) continue elif issubclass(field._matcher, matching.NumericFilterBase): for i in range(10): yield "%s=%d" % (name, i) else: yield "%s=" % (name,) yield r"%s=+" % (name,) yield r"%s=-" % (name,) yield "custom_" yield "kind_" # TODO: refactor to engine.TorrentProxy as format() method def format_item(self, item, defaults=None, stencil=None): """ Format an item. """ try: item_text = fmt.to_console(formatting.format_item(self.options.output_format, item, defaults)) except (NameError, ValueError, TypeError), exc: self.fatal("Trouble with formatting item %r\n\n FORMAT = %r\n\n REASON =" % (item, self.options.output_format), exc) raise # in --debug mode # Escape for shell use? def shell_escape(text, safe=re.compile(r"^[-._,+a-zA-Z0-9]+$")): "Escape helper" return text if safe.match(text) else "'%s'" % text.replace("'", r"'\''") if self.options.shell: item_text = '\t'.join(shell_escape(i) for i in item_text.split('\t')) # Justify headers according to stencil if stencil: item_text = '\t'.join(i.ljust(len(s)) for i, s in zip(item_text.split('\t'), stencil)) return item_text