class Core(CorePluginBase): def enable(self): self.config = ConfigManager("execute.conf", DEFAULT_CONFIG) event_manager = component.get("EventManager") self.registered_events = {} # Go through the commands list and register event handlers for command in self.config["commands"]: event = command[EXECUTE_EVENT] if event in self.registered_events: continue def create_event_handler(event): def event_handler(torrent_id, *arg): self.execute_commands(torrent_id, event, *arg) return event_handler event_handler = create_event_handler(event) event_manager.register_event_handler(EVENT_MAP[event], event_handler) self.registered_events[event] = event_handler log.debug("Execute core plugin enabled!") def execute_commands(self, torrent_id, event, *arg): torrent = component.get("TorrentManager").torrents[torrent_id] info = torrent.get_status(["name", "save_path", "move_on_completed", "move_on_completed_path"]) # Grab the torrent name and save path torrent_name = info["name"] if event == "complete": save_path = info["move_on_completed_path"] if info["move_on_completed"] else info["save_path"] elif event == "added" and arg[0]: # No futher action as from_state (arg[0]) is True return else: save_path = info["save_path"] log.debug("[execute] Running commands for %s", event) def log_error(result, command): (stdout, stderr, exit_code) = result if exit_code: log.warn("[execute] command '%s' failed with exit code %d", command, exit_code) if stdout: log.warn("[execute] stdout: %s", stdout) if stderr: log.warn("[execute] stderr: %s", stderr) # Go through and execute all the commands for command in self.config["commands"]: if command[EXECUTE_EVENT] == event: command = os.path.expandvars(command[EXECUTE_COMMAND]) command = os.path.expanduser(command) log.debug("[execute] running %s", command) d = getProcessOutputAndValue(command, (torrent_id, torrent_name, save_path), env=os.environ) d.addCallback(log_error, command) def disable(self): self.config.save() event_manager = component.get("EventManager") for event, handler in self.registered_events.iteritems(): event_manager.deregister_event_handler(event, handler) log.debug("Execute core plugin disabled!") ### Exported RPC methods ### @export def add_command(self, event, command): command_id = hashlib.sha1(str(time.time())).hexdigest() self.config["commands"].append((command_id, event, command)) self.config.save() component.get("EventManager").emit(ExecuteCommandAddedEvent(command_id, event, command)) @export def get_commands(self): return self.config["commands"] @export def remove_command(self, command_id): for command in self.config["commands"]: if command[EXECUTE_ID] == command_id: self.config["commands"].remove(command) component.get("EventManager").emit(ExecuteCommandRemovedEvent(command_id)) break self.config.save() @export def save_command(self, command_id, event, cmd): for i, command in enumerate(self.config["commands"]): if command[EXECUTE_ID] == command_id: self.config["commands"][i] = (command_id, event, cmd) break self.config.save()
class Core(CorePluginBase): """ self.labels = {label_id:label_options_dict} self.torrent_labels = {torrent_id:label_id} """ def enable(self): log.info('*** Start Label plugin ***') self.plugin = component.get('CorePluginManager') self.plugin.register_status_field('label', self._status_get_label) # __init__ core = component.get('Core') self.config = ConfigManager('label.conf', defaults=CONFIG_DEFAULTS) self.core_cfg = ConfigManager('core.conf') # reduce typing, assigning some values to self... self.torrents = core.torrentmanager.torrents self.labels = self.config['labels'] self.torrent_labels = self.config['torrent_labels'] self.clean_initial_config() component.get('EventManager').register_event_handler('TorrentAddedEvent', self.post_torrent_add) component.get('EventManager').register_event_handler('TorrentRemovedEvent', self.post_torrent_remove) # register tree: component.get('FilterManager').register_tree_field('label', self.init_filter_dict) log.debug('Label plugin enabled..') def disable(self): self.plugin.deregister_status_field('label') component.get('FilterManager').deregister_tree_field('label') component.get('EventManager').deregister_event_handler('TorrentAddedEvent', self.post_torrent_add) component.get('EventManager').deregister_event_handler('TorrentRemovedEvent', self.post_torrent_remove) def update(self): pass def init_filter_dict(self): filter_dict = dict([(label, 0) for label in self.labels]) filter_dict['All'] = len(self.torrents) return filter_dict # Plugin hooks # def post_torrent_add(self, torrent_id, from_state): if from_state: return log.debug('post_torrent_add') torrent = self.torrents[torrent_id] for label_id, options in self.labels.items(): if options['auto_add']: if self._has_auto_match(torrent, options): self.set_torrent(torrent_id, label_id) return def post_torrent_remove(self, torrent_id): log.debug('post_torrent_remove') if torrent_id in self.torrent_labels: del self.torrent_labels[torrent_id] # Utils # def clean_config(self): """remove invalid data from config-file""" for torrent_id, label_id in list(self.torrent_labels.items()): if (label_id not in self.labels) or (torrent_id not in self.torrents): log.debug('label: rm %s:%s', torrent_id, label_id) del self.torrent_labels[torrent_id] def clean_initial_config(self): """ *add any new keys in OPTIONS_DEFAULTS *set all None values to default <-fix development config """ log.debug(list(self.labels)) for key in self.labels: options = dict(OPTIONS_DEFAULTS) options.update(self.labels[key]) self.labels[key] = options for label, options in self.labels.items(): for key, value in options.items(): if value is None: self.labels[label][key] = OPTIONS_DEFAULTS[key] def save_config(self): self.clean_config() self.config.save() @export def get_labels(self): return sorted(self.labels) # Labels: @export def add(self, label_id): """add a label see label_set_options for more options. """ label_id = label_id.lower() check_input(RE_VALID.match(label_id), _('Invalid label, valid characters:[a-z0-9_-]')) check_input(label_id, _('Empty Label')) check_input(not (label_id in self.labels), _('Label already exists')) self.labels[label_id] = dict(OPTIONS_DEFAULTS) self.config.save() @export def remove(self, label_id): """remove a label""" check_input(label_id in self.labels, _('Unknown Label')) del self.labels[label_id] self.clean_config() self.config.save() def _set_torrent_options(self, torrent_id, label_id): options = self.labels[label_id] torrent = self.torrents[torrent_id] if not options['move_completed_path']: options['move_completed_path'] = '' # no None. if options['apply_max']: torrent.set_max_download_speed(options['max_download_speed']) torrent.set_max_upload_speed(options['max_upload_speed']) torrent.set_max_connections(options['max_connections']) torrent.set_max_upload_slots(options['max_upload_slots']) torrent.set_prioritize_first_last_pieces(options['prioritize_first_last']) if options['apply_queue']: torrent.set_auto_managed(options['is_auto_managed']) torrent.set_stop_at_ratio(options['stop_at_ratio']) torrent.set_stop_ratio(options['stop_ratio']) torrent.set_remove_at_ratio(options['remove_at_ratio']) if options['apply_move_completed']: torrent.set_options( { 'move_completed': options['move_completed'], 'move_completed_path': options['move_completed_path'] } ) def _unset_torrent_options(self, torrent_id, label_id): options = self.labels[label_id] torrent = self.torrents[torrent_id] if options['apply_max']: torrent.set_max_download_speed(self.core_cfg.config['max_download_speed_per_torrent']) torrent.set_max_upload_speed(self.core_cfg.config['max_upload_speed_per_torrent']) torrent.set_max_connections(self.core_cfg.config['max_connections_per_torrent']) torrent.set_max_upload_slots(self.core_cfg.config['max_upload_slots_per_torrent']) torrent.set_prioritize_first_last_pieces(self.core_cfg.config['prioritize_first_last_pieces']) if options['apply_queue']: torrent.set_auto_managed(self.core_cfg.config['auto_managed']) torrent.set_stop_at_ratio(self.core_cfg.config['stop_seed_at_ratio']) torrent.set_stop_ratio(self.core_cfg.config['stop_seed_ratio']) torrent.set_remove_at_ratio(self.core_cfg.config['remove_seed_at_ratio']) if options['apply_move_completed']: torrent.set_options( { 'move_completed': self.core_cfg.config['move_completed'], 'move_completed_path': self.core_cfg.config['move_completed_path'] } ) def _has_auto_match(self, torrent, label_options): """match for auto_add fields""" for tracker_match in label_options['auto_add_trackers']: for tracker in torrent.trackers: if tracker_match in tracker['url']: return True return False @export def set_options(self, label_id, options_dict): """update the label options options_dict : {"max_download_speed":float(), "max_upload_speed":float(), "max_connections":int(), "max_upload_slots":int(), #"prioritize_first_last":bool(), "apply_max":bool(), "move_completed_to":string() or None } """ check_input(label_id in self.labels, _('Unknown Label')) for key in options_dict: if key not in OPTIONS_DEFAULTS: raise Exception('label: Invalid options_dict key:%s' % key) self.labels[label_id].update(options_dict) # apply for torrent_id, label in self.torrent_labels.items(): if label_id == label and torrent_id in self.torrents: self._set_torrent_options(torrent_id, label_id) # auto add options = self.labels[label_id] if options['auto_add']: for torrent_id, torrent in self.torrents.items(): if self._has_auto_match(torrent, options): self.set_torrent(torrent_id, label_id) self.config.save() @export def get_options(self, label_id): """returns the label options""" return self.labels[label_id] @export def set_torrent(self, torrent_id, label_id): """ assign a label to a torrent removes a label if the label_id parameter is empty. """ if label_id == NO_LABEL: label_id = None check_input((not label_id) or (label_id in self.labels), _('Unknown Label')) check_input(torrent_id in self.torrents, _('Unknown Torrent')) if torrent_id in self.torrent_labels: self._unset_torrent_options(torrent_id, self.torrent_labels[torrent_id]) del self.torrent_labels[torrent_id] self.clean_config() if label_id: self.torrent_labels[torrent_id] = label_id self._set_torrent_options(torrent_id, label_id) self.config.save() @export def get_config(self): """see : label_set_config""" return dict((key, self.config[key]) for key in CORE_OPTIONS if key in self.config.config) @export def set_config(self, options): """global_options:""" if options: for key, value in options.items: if key in CORE_OPTIONS: self.config[key] = value self.config.save() def _status_get_label(self, torrent_id): return self.torrent_labels.get(torrent_id) or ''
class ConnectionManager(BaseMode): def __init__(self, stdscr, encoding=None): self.popup = None self.statuses = {} self.messages = deque() self.config = ConfigManager("hostlist.conf.1.2", DEFAULT_CONFIG) BaseMode.__init__(self, stdscr, encoding) self.__update_statuses() self.__update_popup() def __update_popup(self): self.popup = SelectablePopup(self,"Select Host",self.__host_selected) self.popup.add_line("{!white,black,bold!}'Q'=quit, 'r'=refresh, 'a'=add new host, 'D'=delete host",selectable=False) for host in self.config["hosts"]: if host[0] in self.statuses: self.popup.add_line("%s:%d [Online] (%s)"%(host[1],host[2],self.statuses[host[0]]),data=host[0],foreground="green") else: self.popup.add_line("%s:%d [Offline]"%(host[1],host[2]),data=host[0],foreground="red") self.inlist = True self.refresh() def __update_statuses(self): """Updates the host status""" def on_connect(result, c, host_id): def on_info(info, c): self.statuses[host_id] = info self.__update_popup() c.disconnect() def on_info_fail(reason, c): if host_id in self.statuses: del self.statuses[host_id] c.disconnect() d = c.daemon.info() d.addCallback(on_info, c) d.addErrback(on_info_fail, c) def on_connect_failed(reason, host_id): if host_id in self.statuses: del self.statuses[host_id] for host in self.config["hosts"]: c = deluge.ui.client.Client() hadr = host[1] port = host[2] user = host[3] password = host[4] d = c.connect(hadr, port, user, password) d.addCallback(on_connect, c, host[0]) d.addErrback(on_connect_failed, host[0]) def __on_connected(self,result): component.start() self.stdscr.erase() at = AllTorrents(self.stdscr, self.encoding) component.get("ConsoleUI").set_mode(at) at.resume() def __host_selected(self, idx, data): for host in self.config["hosts"]: if host[0] == data and host[0] in self.statuses: client.connect(host[1], host[2], host[3], host[4]).addCallback(self.__on_connected) return False def __do_add(self,result): hostname = result["hostname"] try: port = int(result["port"]) except ValueError: self.report_message("Can't add host","Invalid port. Must be an integer") return username = result["username"] password = result["password"] for host in self.config["hosts"]: if (host[1],host[2],host[3]) == (hostname, port, username): self.report_message("Can't add host","Host already in list") return newid = hashlib.sha1(str(time.time())).hexdigest() self.config["hosts"].append((newid, hostname, port, username, password)) self.config.save() self.__update_popup() def __add_popup(self): self.inlist = False self.popup = InputPopup(self,"Add Host (up & down arrows to navigate, esc to cancel)",close_cb=self.__do_add) self.popup.add_text_input("Hostname:","hostname") self.popup.add_text_input("Port:","port") self.popup.add_text_input("Username:"******"username") self.popup.add_text_input("Password:"******"password") self.refresh() def __delete_current_host(self): idx,data = self.popup.current_selection() log.debug("deleting host: %s",data) for host in self.config["hosts"]: if host[0] == data: self.config["hosts"].remove(host) break self.config.save() def report_message(self,title,message): self.messages.append((title,message)) def refresh(self): self.stdscr.erase() self.draw_statusbars() self.stdscr.noutrefresh() if self.popup == None and self.messages: title,msg = self.messages.popleft() self.popup = MessagePopup(self,title,msg) if not self.popup: self.__update_popup() self.popup.refresh() curses.doupdate() def on_resize(self, *args): BaseMode.on_resize_norefresh(self, *args) if self.popup: self.popup.handle_resize() self.stdscr.erase() self.refresh() def _doRead(self): # Read the character c = self.stdscr.getch() if c > 31 and c < 256: if chr(c) == 'q' and self.inlist: return if chr(c) == 'Q': from twisted.internet import reactor if client.connected(): def on_disconnect(result): reactor.stop() client.disconnect().addCallback(on_disconnect) else: reactor.stop() return if chr(c) == 'D' and self.inlist: self.__delete_current_host() self.__update_popup() return if chr(c) == 'r' and self.inlist: self.__update_statuses() if chr(c) == 'a' and self.inlist: self.__add_popup() return if self.popup: if self.popup.handle_read(c): self.popup = None self.refresh() return
class Core(CorePluginBase): """ self.labels = {label_id:label_options_dict} self.torrent_labels = {torrent_id:label_id} """ def enable(self): log.info("*** Start Label plugin ***") self.plugin = component.get("CorePluginManager") self.plugin.register_status_field("label", self._status_get_label) #__init__ core = component.get("Core") self.config = ConfigManager("label.conf", defaults=CONFIG_DEFAULTS) self.core_cfg = ConfigManager("core.conf") #reduce typing, assigning some values to self... self.torrents = core.torrentmanager.torrents self.labels = self.config["labels"] self.torrent_labels = self.config["torrent_labels"] self.clean_initial_config() component.get("EventManager").register_event_handler("TorrentAddedEvent", self.post_torrent_add) component.get("EventManager").register_event_handler("TorrentRemovedEvent", self.post_torrent_remove) #register tree: component.get("FilterManager").register_tree_field("label", self.init_filter_dict) log.debug("Label plugin enabled..") def disable(self): self.plugin.deregister_status_field("label") component.get("FilterManager").deregister_tree_field("label") component.get("EventManager").deregister_event_handler("TorrentAddedEvent", self.post_torrent_add) component.get("EventManager").deregister_event_handler("TorrentRemovedEvent", self.post_torrent_remove) def update(self): pass def init_filter_dict(self): return dict( [(label, 0) for label in self.labels.keys()]) ## Plugin hooks ## def post_torrent_add(self, torrent_id, from_state): log.debug("post_torrent_add") torrent = self.torrents[torrent_id] for label_id, options in self.labels.iteritems(): if options["auto_add"]: if self._has_auto_match(torrent, options): self.set_torrent(torrent_id, label_id) return def post_torrent_remove(self, torrent_id): log.debug("post_torrent_remove") if torrent_id in self.torrent_labels: del self.torrent_labels[torrent_id] ## Utils ## def clean_config(self): """remove invalid data from config-file""" for torrent_id, label_id in list(self.torrent_labels.iteritems()): if (not label_id in self.labels) or (not torrent_id in self.torrents): log.debug("label: rm %s:%s" % (torrent_id,label_id)) del self.torrent_labels[torrent_id] def clean_initial_config(self): """ *add any new keys in OPTIONS_DEFAULTS *set all None values to default <-fix development config """ log.debug(self.labels.keys()) for key in self.labels.keys(): options = dict(OPTIONS_DEFAULTS) options.update(self.labels[key]) self.labels[key] = options for label, options in self.labels.items(): for key, value in options.items(): if value == None: self.labels[label][key] = OPTIONS_DEFAULTS[key] def save_config(self): self.clean_config() self.config.save() @export def get_labels(self): return sorted(self.labels.keys()) #Labels: @export def add(self, label_id): """add a label see label_set_options for more options. """ label_id = label_id.lower() CheckInput(RE_VALID.match(label_id) , _("Invalid label, valid characters:[a-z0-9_-]")) CheckInput(label_id, _("Empty Label")) CheckInput(not (label_id in self.labels) , _("Label already exists")) self.labels[label_id] = dict(OPTIONS_DEFAULTS) self.config.save() @export def remove(self, label_id): """remove a label""" CheckInput(label_id in self.labels, _("Unknown Label")) del self.labels[label_id] self.clean_config() self.config.save() def _set_torrent_options(self, torrent_id, label_id): options = self.labels[label_id] torrent = self.torrents[torrent_id] if not options["move_completed_path"]: options["move_completed_path"] = "" #no None. if options["apply_max"]: torrent.set_max_download_speed(options["max_download_speed"]) torrent.set_max_upload_speed(options["max_upload_speed"]) torrent.set_max_connections(options["max_connections"]) torrent.set_max_upload_slots(options["max_upload_slots"]) torrent.set_prioritize_first_last(options["prioritize_first_last"]) if options["apply_queue"]: torrent.set_auto_managed(options['is_auto_managed']) torrent.set_stop_at_ratio(options['stop_at_ratio']) torrent.set_stop_ratio(options['stop_ratio']) torrent.set_remove_at_ratio(options['remove_at_ratio']) if options["apply_move_completed"]: torrent.set_options( { "move_completed": options["move_completed"], "move_completed_path": options["move_completed_path"] } ) def _unset_torrent_options(self, torrent_id, label_id): options = self.labels[label_id] torrent = self.torrents[torrent_id] if options["apply_max"]: torrent.set_max_download_speed(self.core_cfg.config["max_download_speed_per_torrent"]) torrent.set_max_upload_speed(self.core_cfg.config["max_upload_speed_per_torrent"]) torrent.set_max_connections(self.core_cfg.config["max_connections_per_torrent"]) torrent.set_max_upload_slots(self.core_cfg.config["max_upload_slots_per_torrent"]) torrent.set_prioritize_first_last(self.core_cfg.config["prioritize_first_last_pieces"]) if options["apply_queue"]: torrent.set_auto_managed(self.core_cfg.config['auto_managed']) torrent.set_stop_at_ratio(self.core_cfg.config['stop_seed_at_ratio']) torrent.set_stop_ratio(self.core_cfg.config['stop_seed_ratio']) torrent.set_remove_at_ratio(self.core_cfg.config['remove_seed_at_ratio']) if options["apply_move_completed"]: torrent.set_options( { "move_completed": self.core_cfg.config["move_completed"], "move_completed_path": self.core_cfg.config["move_completed_path"] } ) def _has_auto_match(self, torrent ,label_options): """match for auto_add fields""" for tracker_match in label_options["auto_add_trackers"]: for tracker in torrent.trackers: if tracker_match in tracker["url"]: return True return False @export def set_options(self, label_id, options_dict): """update the label options options_dict : {"max_download_speed":float(), "max_upload_speed":float(), "max_connections":int(), "max_upload_slots":int(), #"prioritize_first_last":bool(), "apply_max":bool(), "move_completed_to":string() or None } """ CheckInput(label_id in self.labels , _("Unknown Label")) for key in options_dict.keys(): if not key in OPTIONS_DEFAULTS: raise Exception("label: Invalid options_dict key:%s" % key) self.labels[label_id].update(options_dict) #apply for torrent_id,label in self.torrent_labels.iteritems(): if label_id == label and torrent_id in self.torrents: self._set_torrent_options(torrent_id , label_id) #auto add options = self.labels[label_id] if options["auto_add"]: for torrent_id, torrent in self.torrents.iteritems(): if self._has_auto_match(torrent, options): self.set_torrent(torrent_id , label_id) self.config.save() @export def get_options(self, label_id): """returns the label options""" return self.labels[label_id] @export def set_torrent(self, torrent_id , label_id): """ assign a label to a torrent removes a label if the label_id parameter is empty. """ if label_id == NO_LABEL: label_id = None CheckInput((not label_id) or (label_id in self.labels) , _("Unknown Label")) CheckInput(torrent_id in self.torrents , _("Unknown Torrent")) if torrent_id in self.torrent_labels: self._unset_torrent_options(torrent_id, self.torrent_labels[torrent_id]) del self.torrent_labels[torrent_id] self.clean_config() if label_id: self.torrent_labels[torrent_id] = label_id self._set_torrent_options(torrent_id, label_id) self.config.save() @export def get_config(self): """see : label_set_config""" return dict((key, self.config[key]) for key in CORE_OPTIONS if key in self.config.config) @export def set_config(self, options): """global_options:""" if options: for key, value in options.items: if key in CORE_OPTIONS: self.config[key] = value self.config.save() def _status_get_label(self, torrent_id): return self.torrent_labels.get(torrent_id) or ""
class Core(CorePluginBase): def enable(self): self.config = ConfigManager("execute.conf", DEFAULT_CONFIG) event_manager = component.get("EventManager") self.registered_events = {} self.preremoved_cache = {} # Go through the commands list and register event handlers for command in self.config["commands"]: event = command[EXECUTE_EVENT] if event in self.registered_events: continue def create_event_handler(event): def event_handler(torrent_id, *arg): #log.debug("Bleh %s", *arg) #log.debug("Bleh %s", arg) self.execute_commands(torrent_id, event, *arg) return event_handler event_handler = create_event_handler(event) event_manager.register_event_handler(EVENT_MAP[event], event_handler) if event == "removed": event_manager.register_event_handler("PreTorrentRemovedEvent", self.on_preremoved) self.registered_events[event] = event_handler log.debug("Execute core plugin enabled!") def on_preremoved(self, torrent_id): # Get and store the torrent info before it is removed torrent = component.get("TorrentManager").torrents[torrent_id] info = torrent.get_status(["name", "download_location"]) self.preremoved_cache[torrent_id] = [ utf8_encoded(torrent_id), utf8_encoded(info["name"]), utf8_encoded(info["download_location"]) ] def execute_commands(self, torrent_id, event, *arg): log.debug("EXECUTE: did we start execute_commands? Event is %s", event) torrent = component.get("TorrentManager").torrents[torrent_id] if event == "added": # and arg[0]: log.debug("EXECUTE: can't get from state, ending") # No futher action as from_state (arg[0]) is True return elif event == "removed": torrent_id, torrent_name, download_location = self.preremoved_cache.pop( torrent_id) else: log.debug("EXECUTE: Status: %s", torrent.get_status({})) info = torrent.get_status([ "name", "save_path", "move_on_completed", "move_on_completed_path" ]) log.debug("EXECUTE: Info: %s", info) # Grab the torrent name and download location # getProcessOutputAndValue requires args to be str torrent_id = utf8_encoded(torrent_id) log.debug("EXECUTE: TorrentID: %s", torrent_id) torrent_name = utf8_encoded(info["name"]) log.debug("EXECUTE: Torrent name: %s", torrent_name) download_location = utf8_encoded( info["move_on_completed_path"] ) if info["move_on_completed"] else utf8_encoded(info["save_path"]) # Grab the torrent label log.debug("EXECUTE: download_location: %s", download_location) get_label = component.get("Core").get_torrent_status( torrent_id, ["label"]) label = utf8_encoded(get_label["label"]) log.debug("EXECUTE: Label: %s", label) log.debug("EXECUTE:Running commands for %s", event) def log_error(result, command): (stdout, stderr, exit_code) = result if exit_code: log.warn("Command '%s' failed with exit code %d", command, exit_code) if stdout: log.warn("stdout: %s", stdout) if stderr: log.warn("stderr: %s", stderr) log.debug("EXECUTE: Start of new code") #get label from torrent get_label = component.get("Core").get_torrent_status( torrent_id, ["label"]) label = get_label["label"] # Go through and execute all the commands log.debug( "EXECUTE: Starting the loop of commands. Label marked as: %s", label) for command in self.config["commands"]: log.debug("EXECUTE: Command is as follows: %s", command) ##Edited due to label not being a deciding factor on which script to use ##if command[EXECUTE_EVENT] == event and command[EXECUTE_LABEL].upper() == label.upper(): ## log.debug("EXECUTE: Label and event have been matched.") if command[EXECUTE_EVENT] == event: log.debug("EXECUTE: Event have been matched.") delay = command[EXECUTE_DELAY] if delay.isdigit(): log.debug( "EXECUTE: Going to delay the script now by %s seconds.", delay) time.sleep(float(delay)) else: log.debug( "EXECUTE: Delay is not a number, so delay was not run. Current delay is: %s", delay) if isinstance(torrent_name, unicode): log.warn("EXECUTE: torrent_name was unicode") torrent_name = torrent_name.encode('utf-8') # Mark args based on params cmd = command[EXECUTE_COMMAND] log.debug("EXECUTE: Raw Command: %s", cmd) cmd_args = [ torrent_id.encode('utf-8'), torrent_name.encode('utf-8'), download_location.encode('utf-8'), label.encode('utf-8') ] log.warn( "EXECUTE: Command processed. Command: %s; Arguments: %s", cmd, cmd_args) if command[EXECUTE_TYPE] == "script": log.debug("EXECUTE: This is a script") cmd = os.path.expandvars(cmd) cmd = os.path.expanduser(cmd) ## EDITED 180514 Never to be run on windows commented out #cmd_args = [torrent_id, torrent_name, download_location] ##if windows_check: ## # Escape ampersand on windows (see #2784) ## cmd_args = [cmd_args.replace("&", "^^^&") for cmd_arg in cmd_args] ## cmd = [cmd.replace("&", "^^^&") for cmd_arg in cmd] ##Output is command, torrent id, torrent name, download locaation, label if os.path.isfile(cmd) and os.access(cmd, os.X_OK): log.warn("EXECUTE: Running command with args: %s %s", cmd, cmd_args) d = getProcessOutputAndValue(cmd, cmd_args, env=os.environ) d.addCallback(log_error, cmd) else: log.error( "EXECUTE: Execute script not found or not executable" ) if command[EXECUTE_TYPE] == "url": url = cmd log.debug("EXECUTE: Calling the following URL: %s", url) req = urllib2.Request(url) response = urllib2.urlopen(req) the_page = response.read() log.warn("EXECUTE: URL response page: %s", the_page) def disable(self): self.config.save() event_manager = component.get("EventManager") for event, handler in self.registered_events.iteritems(): event_manager.deregister_event_handler(event, handler) log.debug("Execute core plugin disabled!") # Exported RPC methods # @export def add_command(self, event, command, type, torrentlabel, delay): command_id = hashlib.sha1(str(time.time())).hexdigest() self.config["commands"].append( (command_id, event, command, type, torrentlabel, delay)) self.config.save() component.get("EventManager").emit( ExecuteCommandAddedEvent(command_id, event, command, type, torrentlabel, delay)) @export def get_commands(self): return self.config["commands"] @export def remove_command(self, command_id): for command in self.config["commands"]: if command[EXECUTE_ID] == command_id: self.config["commands"].remove(command) component.get("EventManager").emit( ExecuteCommandRemovedEvent(command_id)) break self.config.save() @export def save_command(self, command_id, event, cmd): for i, command in enumerate(self.config["commands"]): if command[EXECUTE_ID] == command_id: type = command[EXECUTE_TYPE] label = command[EXECUTE_LABEL] delay = command[EXECUTE_DELAY] self.config["commands"][i] = (command_id, event, cmd, type, label, delay) break self.config.save()
class Core(CorePluginBase): def enable(self): self.config = ConfigManager("execute.conf", DEFAULT_CONFIG) event_manager = component.get("EventManager") self.torrent_manager = component.get("TorrentManager") self.registered_events = {} self.preremoved_cache = {} # Go through the commands list and register event handlers for command in self.config["commands"]: event = command[EXECUTE_EVENT] if event in self.registered_events: continue def create_event_handler(event): def event_handler(torrent_id): self.execute_commands(torrent_id, event) return event_handler event_handler = create_event_handler(event) event_manager.register_event_handler(EVENT_MAP[event], event_handler) if event == "removed": event_manager.register_event_handler("PreTorrentRemovedEvent", self.on_preremoved) self.registered_events[event] = event_handler log.debug("Execute core plugin enabled!") def on_preremoved(self, torrent_id): # Get and store the torrent info before it is removed torrent = component.get("TorrentManager").torrents[torrent_id] info = torrent.get_status(["name", "save_path"]) self.preremoved_cache[torrent_id] = [utf8_encoded(torrent_id), utf8_encoded(info["name"]), utf8_encoded(info["save_path"])] def execute_commands(self, torrent_id, event): if event == "added" and not self.torrent_manager.session_started: return elif event == "removed": torrent_id, torrent_name, save_path = self.preremoved_cache.pop(torrent_id) else: torrent = component.get("TorrentManager").torrents[torrent_id] info = torrent.get_status(["name", "save_path"]) # getProcessOutputAndValue requires args to be str torrent_id = utf8_encoded(torrent_id) torrent_name = utf8_encoded(info["name"]) save_path = utf8_encoded(info["save_path"]) log.debug("[execute] Running commands for %s", event) def log_error(result, command): (stdout, stderr, exit_code) = result if exit_code: log.warn("[execute] command '%s' failed with exit code %d", command, exit_code) if stdout: log.warn("[execute] stdout: %s", stdout) if stderr: log.warn("[execute] stderr: %s", stderr) # Go through and execute all the commands for command in self.config["commands"]: if command[EXECUTE_EVENT] == event: command = os.path.expandvars(command[EXECUTE_COMMAND]) command = os.path.expanduser(command) if os.path.isfile(command) and os.access(command, os.X_OK): log.debug("[execute] Running %s", command) d = getProcessOutputAndValue(command, (torrent_id, torrent_name, save_path), env=os.environ) d.addCallback(log_error, command) else: log.error("[execute] Execute script not found or not executable") def disable(self): self.config.save() event_manager = component.get("EventManager") for event, handler in self.registered_events.iteritems(): event_manager.deregister_event_handler(event, handler) log.debug("Execute core plugin disabled!") ### Exported RPC methods ### @export def add_command(self, event, command): command_id = hashlib.sha1(str(time.time())).hexdigest() self.config["commands"].append((command_id, event, command)) self.config.save() component.get("EventManager").emit(ExecuteCommandAddedEvent(command_id, event, command)) @export def get_commands(self): return self.config["commands"] @export def remove_command(self, command_id): for command in self.config["commands"]: if command[EXECUTE_ID] == command_id: self.config["commands"].remove(command) component.get("EventManager").emit(ExecuteCommandRemovedEvent(command_id)) break self.config.save() @export def save_command(self, command_id, event, cmd): for i, command in enumerate(self.config["commands"]): if command[EXECUTE_ID] == command_id: self.config["commands"][i] = (command_id, event, cmd) break self.config.save()
class WebApi(JSONComponent): """ The component that implements all the methods required for managing the web interface. The complete web json interface also exposes all the methods available from the core RPC. """ def __init__(self): super(WebApi, self).__init__("Web", depend=["SessionProxy"]) self.host_list = ConfigManager("hostlist.conf.1.2", DEFAULT_HOSTS) if not os.path.isfile(self.host_list.config_file): self.host_list.save() self.core_config = CoreConfig() self.event_queue = EventQueue() try: self.sessionproxy = component.get("SessionProxy") except KeyError: self.sessionproxy = SessionProxy() def get_host(self, host_id): """ Return the information about a host :param host_id: the id of the host :type host_id: string :returns: the host information :rtype: list """ for host in self.host_list["hosts"]: if host[0] == host_id: return host def start(self): self.core_config.start() self.sessionproxy.start() def stop(self): self.core_config.stop() self.sessionproxy.stop() @export def connect(self, host_id): """ Connect the client to a daemon :param host_id: the id of the daemon in the host list :type host_id: string :returns: the methods the daemon supports :rtype: list """ d = Deferred() def on_connected(methods): d.callback(methods) host = self.get_host(host_id) if host: self._json.connect(*host[1:]).addCallback(on_connected) return d @export def connected(self): """ The current connection state. :returns: True if the client is connected :rtype: booleon """ return client.connected() @export def disconnect(self): """ Disconnect the web interface from the connected daemon. """ client.disconnect() return True @export def update_ui(self, keys, filter_dict): """ Gather the information required for updating the web interface. :param keys: the information about the torrents to gather :type keys: list :param filter_dict: the filters to apply when selecting torrents. :type filter_dict: dictionary :returns: The torrent and ui information. :rtype: dictionary """ d = Deferred() ui_info = { "connected": client.connected(), "torrents": None, "filters": None, "stats": { "max_download": self.core_config.get("max_download_speed"), "max_upload": self.core_config.get("max_upload_speed"), "max_num_connections": self.core_config.get("max_connections_global") } } if not client.connected(): d.callback(ui_info) return d def got_connections(connections): ui_info["stats"]["num_connections"] = connections def got_stats(stats): ui_info["stats"]["upload_rate"] = stats["payload_upload_rate"] ui_info["stats"]["download_rate"] = stats["payload_download_rate"] ui_info["stats"]["download_protocol_rate"] = stats[ "download_rate"] - stats["payload_download_rate"] ui_info["stats"]["upload_protocol_rate"] = stats[ "upload_rate"] - stats["payload_upload_rate"] ui_info["stats"]["dht_nodes"] = stats["dht_nodes"] ui_info["stats"]["has_incoming_connections"] = stats[ "has_incoming_connections"] def got_filters(filters): ui_info["filters"] = filters def got_free_space(free_space): ui_info["stats"]["free_space"] = free_space def got_torrents(torrents): ui_info["torrents"] = torrents def on_complete(result): d.callback(ui_info) d1 = component.get("SessionProxy").get_torrents_status( filter_dict, keys) d1.addCallback(got_torrents) d2 = client.core.get_filter_tree() d2.addCallback(got_filters) d3 = client.core.get_session_status([ "payload_download_rate", "payload_upload_rate", "download_rate", "upload_rate", "dht_nodes", "has_incoming_connections" ]) d3.addCallback(got_stats) d4 = client.core.get_num_connections() d4.addCallback(got_connections) d5 = client.core.get_free_space( self.core_config.get("download_location")) d5.addCallback(got_free_space) dl = DeferredList([d1, d2, d3, d4, d5], consumeErrors=True) dl.addCallback(on_complete) return d def _on_got_files(self, torrent, d): files = torrent.get("files") file_progress = torrent.get("file_progress") file_priorities = torrent.get("file_priorities") paths = [] info = {} for index, torrent_file in enumerate(files): path = torrent_file["path"] paths.append(path) torrent_file["progress"] = file_progress[index] torrent_file["priority"] = file_priorities[index] torrent_file["index"] = index torrent_file["path"] = path info[path] = torrent_file # update the directory info dirname = os.path.dirname(path) while dirname: dirinfo = info.setdefault(dirname, {}) dirinfo["size"] = dirinfo.get("size", 0) + torrent_file["size"] if "priority" not in dirinfo: dirinfo["priority"] = torrent_file["priority"] else: if dirinfo["priority"] != torrent_file["priority"]: dirinfo["priority"] = 9 progresses = dirinfo.setdefault("progresses", []) progresses.append(torrent_file["size"] * (torrent_file["progress"] / 100.0)) dirinfo["progress"] = float( sum(progresses)) / dirinfo["size"] * 100 dirinfo["path"] = dirname dirname = os.path.dirname(dirname) def walk(path, item): if item["type"] == "dir": item.update(info[path]) return item else: item.update(info[path]) return item file_tree = uicommon.FileTree2(paths) file_tree.walk(walk) d.callback(file_tree.get_tree()) @export def get_torrent_status(self, torrent_id, keys): return component.get("SessionProxy").get_torrent_status( torrent_id, keys) @export def get_torrent_files(self, torrent_id): """ Gets the files for a torrent in tree format :param torrent_id: the id of the torrent to retrieve. :type torrent_id: string :returns: The torrents files in a tree :rtype: dictionary """ main_deferred = Deferred() d = component.get("SessionProxy").get_torrent_status( torrent_id, FILES_KEYS) d.addCallback(self._on_got_files, main_deferred) return main_deferred @export def download_torrent_from_url(self, url, cookie=None): """ Download a torrent file from a url to a temporary directory. :param url: the url of the torrent :type url: string :returns: the temporary file name of the torrent file :rtype: string """ def on_download_success(result): log.debug("Successfully downloaded %s to %s", url, result) return result def on_download_fail(result): if result.check(twisted.web.error.PageRedirect): new_url = urljoin(url, result.getErrorMessage().split(" to ")[1]) result = httpdownloader.download_file(new_url, tmp_file, headers=headers) result.addCallbacks(on_download_success, on_download_fail) elif result.check(twisted.web.client.PartialDownloadError): result = httpdownloader.download_file(url, tmp_file, headers=headers, allow_compression=False) result.addCallbacks(on_download_success, on_download_fail) else: log.error("Error occurred downloading torrent from %s", url) log.error("Reason: %s", result.getErrorMessage()) return result tempdir = tempfile.mkdtemp(prefix="delugeweb-") tmp_file = os.path.join(tempdir, url.split("/")[-1]) log.debug("filename: %s", tmp_file) headers = {} if cookie: headers["Cookie"] = cookie log.debug("cookie: %s", cookie) d = httpdownloader.download_file(url, tmp_file, headers=headers) d.addCallbacks(on_download_success, on_download_fail) return d @export def get_torrent_info(self, filename): """ Return information about a torrent on the filesystem. :param filename: the path to the torrent :type filename: string :returns: information about the torrent: :: { "name": the torrent name, "files_tree": the files the torrent contains, "info_hash" the torrents info_hash } :rtype: dictionary """ try: torrent_info = uicommon.TorrentInfo(filename.strip(), 2) return torrent_info.as_dict("name", "info_hash", "files_tree") except Exception, e: log.exception(e) return False
plugins_location = get_env_or_default('PLUGINS_LOCATION', config_dir + '/deluge/plugins') move_completed_path = get_env_or_default('MOVE_COMPLETED_PATH', torrents_dir + '/completed') torrentfiles_location = get_env_or_default('TORRENTFILES_LOCATION', torrents_dir + '/.torrents') download_location = get_env_or_default('DOWNLOAD_LOCATION', torrents_dir + '/.downloading') autoadd_location = get_env_or_default('AUTOADD_LOCATION', torrents_dir + '/watch/deluge') if not os.path.exists(config_dir): os.makedirs(config_dir) set_config_dir(config_dir) config = ConfigManager(config_dir + '/core.conf') config['move_completed_path'] = move_completed_path config['torrentfiles_location'] = torrentfiles_location config['download_location'] = download_location config['autoadd_location'] = autoadd_location config['geoip_db_location'] = geoip_db_location config['plugins_location'] = plugins_location config['listen_ports'] = listen_ports config['daemon_port'] = daemon_port config['random_port'] = random_port config.save()
class WebApi(JSONComponent): """ The component that implements all the methods required for managing the web interface. The complete web json interface also exposes all the methods available from the core RPC. """ def __init__(self): super(WebApi, self).__init__("Web", depend=["SessionProxy"]) self.host_list = ConfigManager("hostlist.conf.1.2", DEFAULT_HOSTS) if not os.path.isfile(self.host_list.config_file): self.host_list.save() self.core_config = CoreConfig() self.event_queue = EventQueue() try: self.sessionproxy = component.get("SessionProxy") except KeyError: self.sessionproxy = SessionProxy() def get_host(self, host_id): """ Return the information about a host :param host_id: the id of the host :type host_id: string :returns: the host information :rtype: list """ for host in self.host_list["hosts"]: if host[0] == host_id: return host def start(self): self.core_config.start() self.sessionproxy.start() def stop(self): self.core_config.stop() self.sessionproxy.stop() @export def connect(self, host_id): """ Connect the client to a daemon :param host_id: the id of the daemon in the host list :type host_id: string :returns: the methods the daemon supports :rtype: list """ d = Deferred() def on_connected(methods): d.callback(methods) host = self.get_host(host_id) if host: self._json.connect(*host[1:]).addCallback(on_connected) return d @export def connected(self): """ The current connection state. :returns: True if the client is connected :rtype: booleon """ return client.connected() @export def disconnect(self): """ Disconnect the web interface from the connected daemon. """ client.disconnect() return True @export def update_ui(self, keys, filter_dict): """ Gather the information required for updating the web interface. :param keys: the information about the torrents to gather :type keys: list :param filter_dict: the filters to apply when selecting torrents. :type filter_dict: dictionary :returns: The torrent and ui information. :rtype: dictionary """ d = Deferred() ui_info = { "connected": client.connected(), "torrents": None, "filters": None, "stats": { "max_download": self.core_config.get("max_download_speed"), "max_upload": self.core_config.get("max_upload_speed"), "max_num_connections": self.core_config.get("max_connections_global") } } if not client.connected(): d.callback(ui_info) return d def got_connections(connections): ui_info["stats"]["num_connections"] = connections def got_stats(stats): ui_info["stats"]["upload_rate"] = stats["payload_upload_rate"] ui_info["stats"]["download_rate"] = stats["payload_download_rate"] ui_info["stats"]["download_protocol_rate"] = stats["download_rate"] - stats["payload_download_rate"] ui_info["stats"]["upload_protocol_rate"] = stats["upload_rate"] - stats["payload_upload_rate"] ui_info["stats"]["dht_nodes"] = stats["dht_nodes"] ui_info["stats"]["has_incoming_connections"] = stats["has_incoming_connections"] def got_filters(filters): ui_info["filters"] = filters def got_free_space(free_space): ui_info["stats"]["free_space"] = free_space def got_torrents(torrents): ui_info["torrents"] = torrents def on_complete(result): d.callback(ui_info) d1 = component.get("SessionProxy").get_torrents_status(filter_dict, keys) d1.addCallback(got_torrents) d2 = client.core.get_filter_tree() d2.addCallback(got_filters) d3 = client.core.get_session_status([ "payload_download_rate", "payload_upload_rate", "download_rate", "upload_rate", "dht_nodes", "has_incoming_connections" ]) d3.addCallback(got_stats) d4 = client.core.get_num_connections() d4.addCallback(got_connections) d5 = client.core.get_free_space(self.core_config.get("download_location")) d5.addCallback(got_free_space) dl = DeferredList([d1, d2, d3, d4, d5], consumeErrors=True) dl.addCallback(on_complete) return d def _on_got_files(self, torrent, d): files = torrent.get("files") file_progress = torrent.get("file_progress") file_priorities = torrent.get("file_priorities") paths = [] info = {} for index, torrent_file in enumerate(files): path = torrent_file["path"] paths.append(path) torrent_file["progress"] = file_progress[index] torrent_file["priority"] = file_priorities[index] torrent_file["index"] = index torrent_file["path"] = path info[path] = torrent_file # update the directory info dirname = os.path.dirname(path) while dirname: dirinfo = info.setdefault(dirname, {}) dirinfo["size"] = dirinfo.get("size", 0) + torrent_file["size"] if "priority" not in dirinfo: dirinfo["priority"] = torrent_file["priority"] else: if dirinfo["priority"] != torrent_file["priority"]: dirinfo["priority"] = 9 progresses = dirinfo.setdefault("progresses", []) progresses.append(torrent_file["size"] * (torrent_file["progress"] / 100.0)) dirinfo["progress"] = float(sum(progresses)) / dirinfo["size"] * 100 dirinfo["path"] = dirname dirname = os.path.dirname(dirname) def walk(path, item): if item["type"] == "dir": item.update(info[path]) return item else: item.update(info[path]) return item file_tree = uicommon.FileTree2(paths) file_tree.walk(walk) d.callback(file_tree.get_tree()) @export def get_torrent_status(self, torrent_id, keys): return component.get("SessionProxy").get_torrent_status(torrent_id, keys) @export def get_torrent_files(self, torrent_id): """ Gets the files for a torrent in tree format :param torrent_id: the id of the torrent to retrieve. :type torrent_id: string :returns: The torrents files in a tree :rtype: dictionary """ main_deferred = Deferred() d = component.get("SessionProxy").get_torrent_status(torrent_id, FILES_KEYS) d.addCallback(self._on_got_files, main_deferred) return main_deferred @export def download_torrent_from_url(self, url, cookie=None): """ Download a torrent file from a url to a temporary directory. :param url: the url of the torrent :type url: string :returns: the temporary file name of the torrent file :rtype: string """ def on_download_success(result): log.debug("Successfully downloaded %s to %s", url, result) return result def on_download_fail(result): if result.check(twisted.web.error.PageRedirect): new_url = urljoin(url, result.getErrorMessage().split(" to ")[1]) result = httpdownloader.download_file(new_url, tmp_file, headers=headers) result.addCallbacks(on_download_success, on_download_fail) elif result.check(twisted.web.client.PartialDownloadError): result = httpdownloader.download_file(url, tmp_file, headers=headers, allow_compression=False) result.addCallbacks(on_download_success, on_download_fail) else: log.error("Error occurred downloading torrent from %s", url) log.error("Reason: %s", result.getErrorMessage()) return result tempdir = tempfile.mkdtemp(prefix="delugeweb-") tmp_file = os.path.join(tempdir, url.split("/")[-1]) log.debug("filename: %s", tmp_file) headers = {} if cookie: headers["Cookie"] = cookie log.debug("cookie: %s", cookie) d = httpdownloader.download_file(url, tmp_file, headers=headers) d.addCallbacks(on_download_success, on_download_fail) return d @export def get_torrent_info(self, filename): """ Return information about a torrent on the filesystem. :param filename: the path to the torrent :type filename: string :returns: information about the torrent: :: { "name": the torrent name, "files_tree": the files the torrent contains, "info_hash" the torrents info_hash } :rtype: dictionary """ try: torrent_info = uicommon.TorrentInfo(filename.strip(), 2) return torrent_info.as_dict("name", "info_hash", "files_tree") except Exception, e: log.exception(e) return False
class Core(CorePluginBase): def enable(self): self.config = ConfigManager("execute.conf", DEFAULT_CONFIG) event_manager = component.get("EventManager") self.registered_events = {} # Go through the commands list and register event handlers for command in self.config["commands"]: event = command[EXECUTE_EVENT] if event in self.registered_events: continue def create_event_handler(event): def event_handler(torrent_id): self.execute_commands(torrent_id, event) return event_handler event_handler = create_event_handler(event) event_manager.register_event_handler(EVENT_MAP[event], event_handler) self.registered_events[event] = event_handler log.debug("Execute core plugin enabled!") def execute_commands(self, torrent_id, event): torrent = component.get("TorrentManager").torrents[torrent_id] info = torrent.get_status(["name", "save_path", "move_on_completed", "move_on_completed_path"]) # Grab the torrent name and save path torrent_name = info["name"] if event == "complete": save_path = info["move_on_completed_path"] if info ["move_on_completed"] else info["save_path"] else: save_path = info["save_path"] log.debug("[execute] Running commands for %s", event) # Go through and execute all the commands for command in self.config["commands"]: if command[EXECUTE_EVENT] == event: command = os.path.expandvars(command[EXECUTE_COMMAND]) command = os.path.expanduser(command) log.debug("[execute] running %s", command) p = Popen([command, torrent_id, torrent_name, save_path], stdin=PIPE, stdout=PIPE, stderr=PIPE) if p.wait() != 0: log.warn("Execute command failed with exit code %d", p.returncode) def disable(self): self.config.save() event_manager = component.get("EventManager") for event, handler in self.registered_events.iteritems(): event_manager.deregister_event_handler(event, handler) log.debug("Execute core plugin disabled!") ### Exported RPC methods ### @export def add_command(self, event, command): command_id = hashlib.sha1(str(time.time())).hexdigest() self.config["commands"].append((command_id, event, command)) self.config.save() component.get("EventManager").emit(ExecuteCommandAddedEvent(command_id, event, command)) @export def get_commands(self): return self.config["commands"] @export def remove_command(self, command_id): for command in self.config["commands"]: if command[EXECUTE_ID] == command_id: self.config["commands"].remove(command) component.get("EventManager").emit(ExecuteCommandRemovedEvent(command_id)) break self.config.save() @export def save_command(self, command_id, event, cmd): for i, command in enumerate(self.config["commands"]): if command[EXECUTE_ID] == command_id: self.config["commands"][i] = (command_id, event, cmd) break self.config.save()
class Core(component.Component): def __init__(self, listen_interface=None, read_only_config_keys=None): log.debug('Core init...') component.Component.__init__(self, 'Core') deluge_version = deluge.common.get_version() split_version = deluge.common.VersionSplit(deluge_version).version while len(split_version) < 4: split_version.append(0) deluge_fingerprint = lt.generate_fingerprint('DE', *split_version) user_agent = 'Deluge/{} libtorrent/{}'.format(deluge_version, self.get_libtorrent_version()) # Start the libtorrent session. log.debug('Starting session (fingerprint: %s, user_agent: %s)', deluge_fingerprint, user_agent) settings_pack = {'peer_fingerprint': deluge_fingerprint, 'user_agent': user_agent, 'ignore_resume_timestamps': True} self.session = lt.session(settings_pack, flags=0) # Load the settings, if available. self._load_session_state() # Enable libtorrent extensions # Allows peers to download the metadata from the swarm directly self.session.add_extension('ut_metadata') # Ban peers that sends bad data self.session.add_extension('smart_ban') # Create the components self.eventmanager = EventManager() self.preferencesmanager = PreferencesManager() self.alertmanager = AlertManager() self.pluginmanager = PluginManager(self) self.torrentmanager = TorrentManager() self.filtermanager = FilterManager(self) self.authmanager = AuthManager() # New release check information self.new_release = None # External IP Address from libtorrent self.external_ip = None self.eventmanager.register_event_handler('ExternalIPEvent', self._on_external_ip_event) # GeoIP instance with db loaded self.geoip_instance = None # These keys will be dropped from the set_config() RPC and are # configurable from the command-line. self.read_only_config_keys = read_only_config_keys log.debug('read_only_config_keys: %s', read_only_config_keys) # Get the core config self.config = ConfigManager('core.conf') self.config.save() # If there was an interface value from the command line, use it, but # store the one in the config so we can restore it on shutdown self.__old_interface = None if listen_interface: if deluge.common.is_ip(listen_interface): self.__old_interface = self.config['listen_interface'] self.config['listen_interface'] = listen_interface else: log.error('Invalid listen interface (must be IP Address): %s', listen_interface) # New release check information self.__new_release = None # Session status timer self.session_status = {} self.session_status_timer_interval = 0.5 self.session_status_timer = task.LoopingCall(self.session.post_session_stats) self.alertmanager.register_handler('session_stats_alert', self._on_alert_session_stats) self._session_rates = {(k_rate, k_bytes): 0 for k_rate, k_bytes in SESSION_RATES_MAPPING.items()} self.session_rates_timer_interval = 2 self.session_rates_timer = task.LoopingCall(self._update_session_rates) def start(self): """Starts the core""" self.session_status_timer.start(self.session_status_timer_interval) self.session_rates_timer.start(self.session_rates_timer_interval, now=False) def stop(self): log.debug('Core stopping...') if self.session_status_timer.running: self.session_status_timer.stop() if self.session_rates_timer.running: self.session_rates_timer.stop() # Save the libtorrent session state self._save_session_state() # We stored a copy of the old interface value if self.__old_interface: self.config['listen_interface'] = self.__old_interface # Make sure the config file has been saved self.config.save() def shutdown(self): pass def apply_session_setting(self, key, value): self.apply_session_settings({key: value}) def apply_session_settings(self, settings): """Apply libtorrent session settings. Args: settings (dict): A dict of lt session settings to apply. """ self.session.apply_settings(settings) def _save_session_state(self): """Saves the libtorrent session state""" filename = 'session.state' filepath = get_config_dir(filename) filepath_bak = filepath + '.bak' filepath_tmp = filepath + '.tmp' try: if os.path.isfile(filepath): log.debug('Creating backup of %s at: %s', filename, filepath_bak) shutil.copy2(filepath, filepath_bak) except IOError as ex: log.error('Unable to backup %s to %s: %s', filepath, filepath_bak, ex) else: log.info('Saving the %s at: %s', filename, filepath) try: with open(filepath_tmp, 'wb') as _file: _file.write(lt.bencode(self.session.save_state())) _file.flush() os.fsync(_file.fileno()) shutil.move(filepath_tmp, filepath) except (IOError, EOFError) as ex: log.error('Unable to save %s: %s', filename, ex) if os.path.isfile(filepath_bak): log.info('Restoring backup of %s from: %s', filename, filepath_bak) shutil.move(filepath_bak, filepath) def _load_session_state(self): """Loads the libtorrent session state Returns: dict: A libtorrent sesion state, empty dict if unable to load it. """ filename = 'session.state' filepath = get_config_dir(filename) filepath_bak = filepath + '.bak' for _filepath in (filepath, filepath_bak): log.debug('Opening %s for load: %s', filename, _filepath) try: with open(_filepath, 'rb') as _file: state = lt.bdecode(_file.read()) except (IOError, EOFError, RuntimeError) as ex: log.warning('Unable to load %s: %s', _filepath, ex) else: log.info('Successfully loaded %s: %s', filename, _filepath) self.session.load_state(state) def _on_alert_session_stats(self, alert): """The handler for libtorrent session stats alert""" if not self.session_status: # Empty dict on startup so needs populated with session rate keys and default value. self.session_status.update({key: 0 for key in list(SESSION_RATES_MAPPING)}) self.session_status.update(alert.values) self._update_session_cache_hit_ratio() def _update_session_cache_hit_ratio(self): """Calculates the cache read/write hit ratios and updates session_status""" try: self.session_status['write_hit_ratio'] = ((self.session_status['disk.num_blocks_written'] - self.session_status['disk.num_write_ops']) / self.session_status['disk.num_blocks_written']) except ZeroDivisionError: self.session_status['write_hit_ratio'] = 0.0 try: self.session_status['read_hit_ratio'] = (self.session_status['disk.num_blocks_cache_hits'] / self.session_status['disk.num_blocks_read']) except ZeroDivisionError: self.session_status['read_hit_ratio'] = 0.0 def _update_session_rates(self): """Calculates status rates based on interval and value difference for session_status""" if not self.session_status: return for (rate_key, status_key), prev_bytes in list(self._session_rates.items()): new_bytes = self.session_status[status_key] byte_rate = (new_bytes - prev_bytes) / self.session_rates_timer_interval self.session_status[rate_key] = byte_rate # Store current value for next update. self._session_rates[(rate_key, status_key)] = new_bytes def get_new_release(self): log.debug('get_new_release') try: self.new_release = urlopen('http://download.deluge-torrent.org/version-2.0').read().strip() except URLError as ex: log.debug('Unable to get release info from website: %s', ex) return self.check_new_release() def check_new_release(self): if self.new_release: log.debug('new_release: %s', self.new_release) if deluge.common.VersionSplit(self.new_release) > deluge.common.VersionSplit(deluge.common.get_version()): component.get('EventManager').emit(NewVersionAvailableEvent(self.new_release)) return self.new_release return False def _add_torrent_file(self, filename, filedump, options, save_state=True): """Adds a torrent file to the session. Args: filename (str): The filename of the torrent. filedump (str): A base64 encoded string of torrent file contents. options (dict): The options to apply to the torrent upon adding. save_state (bool): If the state should be saved after adding the file. Returns: str: The torrent ID or None. """ try: filedump = base64.decodestring(filedump) except Exception as ex: log.error('There was an error decoding the filedump string: %s', ex) try: d = self.torrentmanager.add( filedump=filedump, options=options, filename=filename, save_state=save_state ) except RuntimeError as ex: log.error('There was an error adding the torrent file %s: %s', filename, ex) raise else: return d # Exported Methods @export def add_torrent_file(self, filename, filedump, options): """Adds a torrent file to the session. Args: filename (str): The filename of the torrent. filedump (str): A base64 encoded string of the torrent file contents. options (dict): The options to apply to the torrent upon adding. Returns: str: The torrent_id or None. """ return self._add_torrent_file(filename, filedump, options) @export def add_torrent_files(self, torrent_files): """Adds multiple torrent files to the session. Args: torrent_files (list of tuples): Torrent files as tuple of (filename, filedump, options). Returns: Deferred """ @defer.inlineCallbacks def add_torrents(): errors = [] last_index = len(torrent_files) - 1 for idx, torrent in enumerate(torrent_files): try: yield self._add_torrent_file(torrent[0], torrent[1], torrent[2], save_state=idx == last_index) except AddTorrentError as ex: log.warn('Error when adding torrent: %s', ex) errors.append(ex) defer.returnValue(errors) return task.deferLater(reactor, 0, add_torrents) @export def add_torrent_url(self, url, options, headers=None): """ Adds a torrent from a url. Deluge will attempt to fetch the torrent from url prior to adding it to the session. :param url: the url pointing to the torrent file :type url: string :param options: the options to apply to the torrent on add :type options: dict :param headers: any optional headers to send :type headers: dict :returns: a Deferred which returns the torrent_id as a str or None """ log.info('Attempting to add url %s', url) def on_download_success(filename): # We got the file, so add it to the session with open(filename, 'rb') as _file: data = _file.read() try: os.remove(filename) except OSError as ex: log.warning('Could not remove temp file: %s', ex) return self.add_torrent_file(filename, base64.encodestring(data), options) def on_download_fail(failure): # Log the error and pass the failure onto the client log.error('Failed to add torrent from url %s', url) return failure tmp_fd, tmp_file = tempfile.mkstemp(prefix='deluge_url.', suffix='.torrent') os.close(tmp_fd) d = download_file(url, tmp_file, headers=headers, force_filename=True) d.addCallbacks(on_download_success, on_download_fail) return d @export def add_torrent_magnet(self, uri, options): """ Adds a torrent from a magnet link. :param uri: the magnet link :type uri: string :param options: the options to apply to the torrent on add :type options: dict :returns: the torrent_id :rtype: string """ log.debug('Attempting to add by magnet uri: %s', uri) return self.torrentmanager.add(magnet=uri, options=options) @export def remove_torrent(self, torrent_id, remove_data): """Removes a single torrent from the session. Args: torrent_id (str): The torrent ID to remove. remove_data (bool): If True, also remove the downloaded data. Returns: bool: True if removed successfully. Raises: InvalidTorrentError: If the torrent ID does not exist in the session. """ log.debug('Removing torrent %s from the core.', torrent_id) return self.torrentmanager.remove(torrent_id, remove_data) @export def remove_torrents(self, torrent_ids, remove_data): """Remove multiple torrents from the session. Args: torrent_ids (list): The torrent IDs to remove. remove_data (bool): If True, also remove the downloaded data. Returns: list: An empty list if no errors occurred otherwise the list contains tuples of strings, a torrent ID and an error message. For example: [('<torrent_id>', 'Error removing torrent')] """ log.info('Removing %d torrents from core.', len(torrent_ids)) def do_remove_torrents(): errors = [] for torrent_id in torrent_ids: try: self.torrentmanager.remove(torrent_id, remove_data=remove_data, save_state=False) except InvalidTorrentError as ex: errors.append((torrent_id, str(ex))) # Save the session state self.torrentmanager.save_state() if errors: log.warn('Failed to remove %d of %d torrents.', len(errors), len(torrent_ids)) return errors return task.deferLater(reactor, 0, do_remove_torrents) @export def get_session_status(self, keys): """Gets the session status values for 'keys', these keys are taking from libtorrent's session status. See: http://www.rasterbar.com/products/libtorrent/manual.html#status :param keys: the keys for which we want values :type keys: list :returns: a dictionary of {key: value, ...} :rtype: dict """ if not self.session_status: return {key: 0 for key in keys} if not keys: return self.session_status status = {} for key in keys: if key in OLD_SESSION_STATUS_KEYS: new_key = OLD_SESSION_STATUS_KEYS[key] log.warning('Using deprecated session status key %s, please use %s', key, new_key) status[key] = self.session_status[new_key] else: try: status[key] = self.session_status[key] except KeyError: log.warning('Session status key does not exist: %s', key) return status @export def force_reannounce(self, torrent_ids): log.debug('Forcing reannouncment to: %s', torrent_ids) for torrent_id in torrent_ids: self.torrentmanager[torrent_id].force_reannounce() @export def pause_torrent(self, torrent_ids): log.debug('Pausing: %s', torrent_ids) for torrent_id in torrent_ids: if not self.torrentmanager[torrent_id].pause(): log.warning('Error pausing torrent %s', torrent_id) @export def connect_peer(self, torrent_id, ip, port): log.debug('adding peer %s to %s', ip, torrent_id) if not self.torrentmanager[torrent_id].connect_peer(ip, port): log.warning('Error adding peer %s:%s to %s', ip, port, torrent_id) @export def move_storage(self, torrent_ids, dest): log.debug('Moving storage %s to %s', torrent_ids, dest) for torrent_id in torrent_ids: if not self.torrentmanager[torrent_id].move_storage(dest): log.warning('Error moving torrent %s to %s', torrent_id, dest) @export def pause_session(self): """Pause all torrents in the session""" if not self.session.is_paused(): self.session.pause() component.get('EventManager').emit(SessionPausedEvent()) @export def resume_session(self): """Resume all torrents in the session""" if self.session.is_paused(): self.session.resume() for torrent_id in self.torrentmanager.torrents: self.torrentmanager[torrent_id].update_state() component.get('EventManager').emit(SessionResumedEvent()) @export def resume_torrent(self, torrent_ids): log.debug('Resuming: %s', torrent_ids) for torrent_id in torrent_ids: self.torrentmanager[torrent_id].resume() def create_torrent_status(self, torrent_id, torrent_keys, plugin_keys, diff=False, update=False, all_keys=False): try: status = self.torrentmanager[torrent_id].get_status(torrent_keys, diff, update=update, all_keys=all_keys) except KeyError: import traceback traceback.print_exc() # Torrent was probaly removed meanwhile return {} # Ask the plugin manager to fill in the plugin keys if len(plugin_keys) > 0 or all_keys: status.update(self.pluginmanager.get_status(torrent_id, plugin_keys)) return status @export def get_torrent_status(self, torrent_id, keys, diff=False): torrent_keys, plugin_keys = self.torrentmanager.separate_keys(keys, [torrent_id]) return self.create_torrent_status(torrent_id, torrent_keys, plugin_keys, diff=diff, update=True, all_keys=not keys) @export def get_torrents_status(self, filter_dict, keys, diff=False): """ returns all torrents , optionally filtered by filter_dict. """ torrent_ids = self.filtermanager.filter_torrent_ids(filter_dict) d = self.torrentmanager.torrents_status_update(torrent_ids, keys, diff=diff) def add_plugin_fields(args): status_dict, plugin_keys = args # Ask the plugin manager to fill in the plugin keys if len(plugin_keys) > 0: for key in status_dict: status_dict[key].update(self.pluginmanager.get_status(key, plugin_keys)) return status_dict d.addCallback(add_plugin_fields) return d @export def get_filter_tree(self, show_zero_hits=True, hide_cat=None): """ returns {field: [(value,count)] } for use in sidebar(s) """ return self.filtermanager.get_filter_tree(show_zero_hits, hide_cat) @export def get_session_state(self): """Returns a list of torrent_ids in the session.""" # Get the torrent list from the TorrentManager return self.torrentmanager.get_torrent_list() @export def get_config(self): """Get all the preferences as a dictionary""" return self.config.config @export def get_config_value(self, key): """Get the config value for key""" return self.config.get(key) @export def get_config_values(self, keys): """Get the config values for the entered keys""" return dict((key, self.config.get(key)) for key in keys) @export def set_config(self, config): """Set the config with values from dictionary""" # Load all the values into the configuration for key in config: if self.read_only_config_keys and key in self.read_only_config_keys: continue self.config[key] = config[key] @export def get_listen_port(self): """Returns the active listen port""" return self.session.listen_port() @export def get_proxy(self): """Returns the proxy settings Returns: dict: Contains proxy settings. Notes: Proxy type names: 0: None, 1: Socks4, 2: Socks5, 3: Socks5 w Auth, 4: HTTP, 5: HTTP w Auth, 6: I2P """ settings = self.session.get_settings() proxy_type = settings['proxy_type'] proxy_hostname = settings['i2p_hostname'] if proxy_type == 6 else settings['proxy_hostname'] proxy_port = settings['i2p_port'] if proxy_type == 6 else settings['proxy_port'] proxy_dict = { 'type': proxy_type, 'hostname': proxy_hostname, 'username': settings['proxy_username'], 'password': settings['proxy_password'], 'port': proxy_port, 'proxy_hostnames': settings['proxy_hostnames'], 'proxy_peer_connections': settings['proxy_peer_connections'], 'proxy_tracker_connections': settings['proxy_tracker_connections'] } return proxy_dict @export def get_available_plugins(self): """Returns a list of plugins available in the core""" return self.pluginmanager.get_available_plugins() @export def get_enabled_plugins(self): """Returns a list of enabled plugins in the core""" return self.pluginmanager.get_enabled_plugins() @export def enable_plugin(self, plugin): return self.pluginmanager.enable_plugin(plugin) @export def disable_plugin(self, plugin): return self.pluginmanager.disable_plugin(plugin) @export def force_recheck(self, torrent_ids): """Forces a data recheck on torrent_ids""" for torrent_id in torrent_ids: self.torrentmanager[torrent_id].force_recheck() @export def set_torrent_options(self, torrent_ids, options): """Sets the torrent options for torrent_ids Args: torrent_ids (list): A list of torrent_ids to set the options for. options (dict): A dict of torrent options to set. See torrent.TorrentOptions class for valid keys. """ if 'owner' in options and not self.core.authmanager.has_account(options['owner']): raise DelugeError('Username "%s" is not known.' % options['owner']) if isinstance(torrent_ids, str if not PY2 else basestring): torrent_ids = [torrent_ids] for torrent_id in torrent_ids: self.torrentmanager[torrent_id].set_options(options) @export def set_torrent_trackers(self, torrent_id, trackers): """Sets a torrents tracker list. trackers will be [{"url", "tier"}]""" return self.torrentmanager[torrent_id].set_trackers(trackers) @deprecated @export def set_torrent_max_connections(self, torrent_id, value): """Deprecated: Use set_torrent_options with 'max_connections'""" self.set_torrent_options([torrent_id], {'max_connections': value}) @deprecated @export def set_torrent_max_upload_slots(self, torrent_id, value): """Deprecated: Use set_torrent_options with 'max_upload_slots'""" self.set_torrent_options([torrent_id], {'max_upload_slots': value}) @deprecated @export def set_torrent_max_upload_speed(self, torrent_id, value): """Deprecated: Use set_torrent_options with 'max_upload_speed'""" self.set_torrent_options([torrent_id], {'max_upload_speed': value}) @deprecated @export def set_torrent_max_download_speed(self, torrent_id, value): """Deprecated: Use set_torrent_options with 'max_download_speed'""" self.set_torrent_options([torrent_id], {'max_download_speed': value}) @deprecated @export def set_torrent_file_priorities(self, torrent_id, priorities): """Deprecated: Use set_torrent_options with 'file_priorities'""" self.set_torrent_options([torrent_id], {'file_priorities': priorities}) @deprecated @export def set_torrent_prioritize_first_last(self, torrent_id, value): """Deprecated: Use set_torrent_options with 'prioritize_first_last'""" self.set_torrent_options([torrent_id], {'prioritize_first_last_pieces': value}) @deprecated @export def set_torrent_auto_managed(self, torrent_id, value): """Deprecated: Use set_torrent_options with 'auto_managed'""" self.set_torrent_options([torrent_id], {'auto_managed': value}) @deprecated @export def set_torrent_stop_at_ratio(self, torrent_id, value): """Deprecated: Use set_torrent_options with 'stop_at_ratio'""" self.set_torrent_options([torrent_id], {'stop_at_ratio': value}) @deprecated @export def set_torrent_stop_ratio(self, torrent_id, value): """Deprecated: Use set_torrent_options with 'stop_ratio'""" self.set_torrent_options([torrent_id], {'stop_ratio': value}) @deprecated @export def set_torrent_remove_at_ratio(self, torrent_id, value): """Deprecated: Use set_torrent_options with 'remove_at_ratio'""" self.set_torrent_options([torrent_id], {'remove_at_ratio': value}) @deprecated @export def set_torrent_move_completed(self, torrent_id, value): """Deprecated: Use set_torrent_options with 'move_completed'""" self.set_torrent_options([torrent_id], {'move_completed': value}) @deprecated @export def set_torrent_move_completed_path(self, torrent_id, value): """Deprecated: Use set_torrent_options with 'move_completed_path'""" self.set_torrent_options([torrent_id], {'move_completed_path': value}) @export def get_path_size(self, path): """Returns the size of the file or folder 'path' and -1 if the path is unaccessible (non-existent or insufficient privs)""" return deluge.common.get_path_size(path) @export def create_torrent(self, path, tracker, piece_length, comment, target, webseeds, private, created_by, trackers, add_to_session): log.debug('creating torrent..') threading.Thread(target=self._create_torrent_thread, args=( path, tracker, piece_length, comment, target, webseeds, private, created_by, trackers, add_to_session)).start() def _create_torrent_thread(self, path, tracker, piece_length, comment, target, webseeds, private, created_by, trackers, add_to_session): from deluge import metafile metafile.make_meta_file( path, tracker, piece_length, comment=comment, target=target, webseeds=webseeds, private=private, created_by=created_by, trackers=trackers) log.debug('torrent created!') if add_to_session: options = {} options['download_location'] = os.path.split(path)[0] with open(target, 'rb') as _file: filedump = base64.encodestring(_file.read()) self.add_torrent_file(os.path.split(target)[1], filedump, options) @export def upload_plugin(self, filename, filedump): """This method is used to upload new plugins to the daemon. It is used when connecting to the daemon remotely and installing a new plugin on the client side. 'plugin_data' is a xmlrpc.Binary object of the file data, ie, plugin_file.read()""" try: filedump = base64.decodestring(filedump) except Exception as ex: log.error('There was an error decoding the filedump string!') log.exception(ex) return with open(os.path.join(get_config_dir(), 'plugins', filename), 'wb') as _file: _file.write(filedump) component.get('CorePluginManager').scan_for_plugins() @export def rescan_plugins(self): """ Rescans the plugin folders for new plugins """ component.get('CorePluginManager').scan_for_plugins() @export def rename_files(self, torrent_id, filenames): """ Rename files in torrent_id. Since this is an asynchronous operation by libtorrent, watch for the TorrentFileRenamedEvent to know when the files have been renamed. :param torrent_id: the torrent_id to rename files :type torrent_id: string :param filenames: a list of index, filename pairs :type filenames: ((index, filename), ...) :raises InvalidTorrentError: if torrent_id is invalid """ if torrent_id not in self.torrentmanager.torrents: raise InvalidTorrentError('torrent_id is not in session') def rename(): self.torrentmanager[torrent_id].rename_files(filenames) return task.deferLater(reactor, 0, rename) @export def rename_folder(self, torrent_id, folder, new_folder): """ Renames the 'folder' to 'new_folder' in 'torrent_id'. Watch for the TorrentFolderRenamedEvent which is emitted when the folder has been renamed successfully. :param torrent_id: the torrent to rename folder in :type torrent_id: string :param folder: the folder to rename :type folder: string :param new_folder: the new folder name :type new_folder: string :raises InvalidTorrentError: if the torrent_id is invalid """ if torrent_id not in self.torrentmanager.torrents: raise InvalidTorrentError('torrent_id is not in session') return self.torrentmanager[torrent_id].rename_folder(folder, new_folder) @export def queue_top(self, torrent_ids): log.debug('Attempting to queue %s to top', torrent_ids) # torrent_ids must be sorted in reverse before moving to preserve order for torrent_id in sorted(torrent_ids, key=self.torrentmanager.get_queue_position, reverse=True): try: # If the queue method returns True, then we should emit a signal if self.torrentmanager.queue_top(torrent_id): component.get('EventManager').emit(TorrentQueueChangedEvent()) except KeyError: log.warning('torrent_id: %s does not exist in the queue', torrent_id) @export def queue_up(self, torrent_ids): log.debug('Attempting to queue %s to up', torrent_ids) torrents = ((self.torrentmanager.get_queue_position(torrent_id), torrent_id) for torrent_id in torrent_ids) torrent_moved = True prev_queue_position = None # torrent_ids must be sorted before moving. for queue_position, torrent_id in sorted(torrents): # Move the torrent if and only if there is space (by not moving it we preserve the order) if torrent_moved or queue_position - prev_queue_position > 1: try: torrent_moved = self.torrentmanager.queue_up(torrent_id) except KeyError: log.warning('torrent_id: %s does not exist in the queue', torrent_id) # If the torrent moved, then we should emit a signal if torrent_moved: component.get('EventManager').emit(TorrentQueueChangedEvent()) else: prev_queue_position = queue_position @export def queue_down(self, torrent_ids): log.debug('Attempting to queue %s to down', torrent_ids) torrents = ((self.torrentmanager.get_queue_position(torrent_id), torrent_id) for torrent_id in torrent_ids) torrent_moved = True prev_queue_position = None # torrent_ids must be sorted before moving. for queue_position, torrent_id in sorted(torrents, reverse=True): # Move the torrent if and only if there is space (by not moving it we preserve the order) if torrent_moved or prev_queue_position - queue_position > 1: try: torrent_moved = self.torrentmanager.queue_down(torrent_id) except KeyError: log.warning('torrent_id: %s does not exist in the queue', torrent_id) # If the torrent moved, then we should emit a signal if torrent_moved: component.get('EventManager').emit(TorrentQueueChangedEvent()) else: prev_queue_position = queue_position @export def queue_bottom(self, torrent_ids): log.debug('Attempting to queue %s to bottom', torrent_ids) # torrent_ids must be sorted before moving to preserve order for torrent_id in sorted(torrent_ids, key=self.torrentmanager.get_queue_position): try: # If the queue method returns True, then we should emit a signal if self.torrentmanager.queue_bottom(torrent_id): component.get('EventManager').emit(TorrentQueueChangedEvent()) except KeyError: log.warning('torrent_id: %s does not exist in the queue', torrent_id) @export def glob(self, path): return glob.glob(path) @export def test_listen_port(self): """ Checks if the active port is open :returns: True if the port is open, False if not :rtype: bool """ d = getPage(b'http://deluge-torrent.org/test_port.php?port=%s' % self.get_listen_port(), timeout=30) def on_get_page(result): return bool(int(result)) def on_error(failure): log.warning('Error testing listen port: %s', failure) d.addCallback(on_get_page) d.addErrback(on_error) return d @export def get_free_space(self, path=None): """ Returns the number of free bytes at path :param path: the path to check free space at, if None, use the default download location :type path: string :returns: the number of free bytes at path :rtype: int :raises InvalidPathError: if the path is invalid """ if not path: path = self.config['download_location'] try: return deluge.common.free_space(path) except InvalidPathError: return -1 def _on_external_ip_event(self, external_ip): self.external_ip = external_ip @export def get_external_ip(self): """ Returns the external ip address recieved from libtorrent. """ return self.external_ip @export def get_libtorrent_version(self): """ Returns the libtorrent version. :returns: the version :rtype: string """ return lt.__version__ @export def get_completion_paths(self, args): """ Returns the available path completions for the input value. """ return path_chooser_common.get_completion_paths(args) @export(AUTH_LEVEL_ADMIN) def get_known_accounts(self): return self.authmanager.get_known_accounts() @export(AUTH_LEVEL_NONE) def get_auth_levels_mappings(self): return (AUTH_LEVELS_MAPPING, AUTH_LEVELS_MAPPING_REVERSE) @export(AUTH_LEVEL_ADMIN) def create_account(self, username, password, authlevel): return self.authmanager.create_account(username, password, authlevel) @export(AUTH_LEVEL_ADMIN) def update_account(self, username, password, authlevel): return self.authmanager.update_account(username, password, authlevel) @export(AUTH_LEVEL_ADMIN) def remove_account(self, username): return self.authmanager.remove_account(username)
class ConnectionManager(BaseMode): def __init__(self, stdscr, encoding=None): self.popup = None self.statuses = {} self.messages = deque() self.config = ConfigManager("hostlist.conf.1.2", DEFAULT_CONFIG) BaseMode.__init__(self, stdscr, encoding) self.__update_statuses() self.__update_popup() def __update_popup(self): self.popup = SelectablePopup(self, "Select Host", self.__host_selected) self.popup.add_line( "{!white,black,bold!}'Q'=quit, 'r'=refresh, 'a'=add new host, 'D'=delete host", selectable=False) for host in self.config["hosts"]: if host[0] in self.statuses: self.popup.add_line("%s:%d [Online] (%s)" % (host[1], host[2], self.statuses[host[0]]), data=host[0], foreground="green") else: self.popup.add_line("%s:%d [Offline]" % (host[1], host[2]), data=host[0], foreground="red") self.inlist = True self.refresh() def __update_statuses(self): """Updates the host status""" def on_connect(result, c, host_id): def on_info(info, c): self.statuses[host_id] = info self.__update_popup() c.disconnect() def on_info_fail(reason, c): if host_id in self.statuses: del self.statuses[host_id] c.disconnect() d = c.daemon.info() d.addCallback(on_info, c) d.addErrback(on_info_fail, c) def on_connect_failed(reason, host_id): if host_id in self.statuses: del self.statuses[host_id] for host in self.config["hosts"]: c = deluge.ui.client.Client() hadr = host[1] port = host[2] user = host[3] password = host[4] d = c.connect(hadr, port, user, password) d.addCallback(on_connect, c, host[0]) d.addErrback(on_connect_failed, host[0]) def __on_connected(self, result): component.start() self.stdscr.erase() at = AllTorrents(self.stdscr, self.encoding) component.get("ConsoleUI").set_mode(at) at.resume() def __host_selected(self, idx, data): for host in self.config["hosts"]: if host[0] == data and host[0] in self.statuses: client.connect(host[1], host[2], host[3], host[4]).addCallback(self.__on_connected) return False def __do_add(self, result): hostname = result["hostname"] try: port = int(result["port"]) except ValueError: self.report_message("Can't add host", "Invalid port. Must be an integer") return username = result["username"] password = result["password"] for host in self.config["hosts"]: if (host[1], host[2], host[3]) == (hostname, port, username): self.report_message("Can't add host", "Host already in list") return newid = hashlib.sha1(str(time.time())).hexdigest() self.config["hosts"].append( (newid, hostname, port, username, password)) self.config.save() self.__update_popup() def __add_popup(self): self.inlist = False self.popup = InputPopup( self, "Add Host (up & down arrows to navigate, esc to cancel)", close_cb=self.__do_add) self.popup.add_text_input("Hostname:", "hostname") self.popup.add_text_input("Port:", "port") self.popup.add_text_input("Username:"******"username") self.popup.add_text_input("Password:"******"password") self.refresh() def __delete_current_host(self): idx, data = self.popup.current_selection() log.debug("deleting host: %s", data) for host in self.config["hosts"]: if host[0] == data: self.config["hosts"].remove(host) break self.config.save() def report_message(self, title, message): self.messages.append((title, message)) def refresh(self): self.stdscr.erase() self.draw_statusbars() self.stdscr.noutrefresh() if self.popup == None and self.messages: title, msg = self.messages.popleft() self.popup = MessagePopup(self, title, msg) if not self.popup: self.__update_popup() self.popup.refresh() curses.doupdate() def on_resize(self, *args): BaseMode.on_resize_norefresh(self, *args) if self.popup: self.popup.handle_resize() self.stdscr.erase() self.refresh() def _doRead(self): # Read the character c = self.stdscr.getch() if c > 31 and c < 256: if chr(c) == 'q' and self.inlist: return if chr(c) == 'Q': from twisted.internet import reactor if client.connected(): def on_disconnect(result): reactor.stop() client.disconnect().addCallback(on_disconnect) else: reactor.stop() return if chr(c) == 'D' and self.inlist: self.__delete_current_host() self.__update_popup() return if chr(c) == 'r' and self.inlist: self.__update_statuses() if chr(c) == 'a' and self.inlist: self.__add_popup() return if self.popup: if self.popup.handle_read(c): self.popup = None self.refresh() return
class Core(CorePluginBase): def enable(self): self.config = ConfigManager("execute.conf", DEFAULT_CONFIG) event_manager = component.get("EventManager") self.registered_events = {} # Go through the commands list and register event handlers for command in self.config["commands"]: event = command[EXECUTE_EVENT] if event in self.registered_events: continue def create_event_handler(event): def event_handler(torrent_id, *arg): self.execute_commands(torrent_id, event, *arg) return event_handler event_handler = create_event_handler(event) event_manager.register_event_handler(EVENT_MAP[event], event_handler) self.registered_events[event] = event_handler log.debug("Execute core plugin enabled!") def execute_commands(self, torrent_id, event, *arg): torrent = component.get("TorrentManager").torrents[torrent_id] info = torrent.get_status([ "name", "save_path", "move_on_completed", "move_on_completed_path" ]) # Grab the torrent name and save path torrent_name = info["name"] if event == "complete": save_path = info["move_on_completed_path"] if info[ "move_on_completed"] else info["save_path"] elif event == "added" and arg[0]: # No futher action as from_state (arg[0]) is True return else: save_path = info["save_path"] # getProcessOutputAndValue requires args to be str if isinstance(torrent_id, unicode): torrent_id = torrent_id.encode("utf-8", "ignore") if isinstance(torrent_name, unicode): torrent_name = torrent_name.encode("utf-8", "ignore") if isinstance(save_path, unicode): save_path = save_path.encode("utf-8", "ignore") log.debug("[execute] Running commands for %s", event) def log_error(result, command): (stdout, stderr, exit_code) = result if exit_code: log.warn("[execute] command '%s' failed with exit code %d", command, exit_code) if stdout: log.warn("[execute] stdout: %s", stdout) if stderr: log.warn("[execute] stderr: %s", stderr) # Go through and execute all the commands for command in self.config["commands"]: if command[EXECUTE_EVENT] == event: command = os.path.expandvars(command[EXECUTE_COMMAND]) command = os.path.expanduser(command) log.debug("[execute] running %s", command) d = getProcessOutputAndValue( command, (torrent_id, torrent_name, save_path), env=os.environ) d.addCallback(log_error, command) def disable(self): self.config.save() event_manager = component.get("EventManager") for event, handler in self.registered_events.iteritems(): event_manager.deregister_event_handler(event, handler) log.debug("Execute core plugin disabled!") ### Exported RPC methods ### @export def add_command(self, event, command): command_id = hashlib.sha1(str(time.time())).hexdigest() self.config["commands"].append((command_id, event, command)) self.config.save() component.get("EventManager").emit( ExecuteCommandAddedEvent(command_id, event, command)) @export def get_commands(self): return self.config["commands"] @export def remove_command(self, command_id): for command in self.config["commands"]: if command[EXECUTE_ID] == command_id: self.config["commands"].remove(command) component.get("EventManager").emit( ExecuteCommandRemovedEvent(command_id)) break self.config.save() @export def save_command(self, command_id, event, cmd): for i, command in enumerate(self.config["commands"]): if command[EXECUTE_ID] == command_id: self.config["commands"][i] = (command_id, event, cmd) break self.config.save()
class GtkUI(object): def __init__(self, args): # Setup gtkbuilder/glade translation setup_translations(setup_gettext=False, setup_pygtk=True) # Setup signals def on_die(*args): log.debug('OS signal "die" caught with args: %s', args) reactor.stop() if windows_check(): from win32api import SetConsoleCtrlHandler SetConsoleCtrlHandler(on_die, True) log.debug('Win32 "die" handler registered') elif osx_check() and WINDOWING == 'quartz': import gtkosx_application self.osxapp = gtkosx_application.gtkosx_application_get() self.osxapp.connect('NSApplicationWillTerminate', on_die) log.debug('OSX quartz "die" handler registered') # Set process name again to fix gtk issue setproctitle(getproctitle()) # Attempt to register a magnet URI handler with gconf, but do not overwrite # if already set by another program. associate_magnet_links(False) # Make sure gtkui.conf has at least the defaults set self.config = ConfigManager('gtkui.conf', DEFAULT_PREFS) # Make sure the gtkui state folder has been created if not os.path.exists(os.path.join(get_config_dir(), 'gtkui_state')): os.makedirs(os.path.join(get_config_dir(), 'gtkui_state')) # Set language if self.config['language'] is not None: set_language(self.config['language']) # Start the IPC Interface before anything else.. Just in case we are # already running. self.queuedtorrents = QueuedTorrents() self.ipcinterface = IPCInterface(args.torrents) # Initialize gdk threading threads_init() # We make sure that the UI components start once we get a core URI client.set_disconnect_callback(self.__on_disconnect) self.trackericons = TrackerIcons() self.sessionproxy = SessionProxy() # Initialize various components of the gtkui self.mainwindow = MainWindow() self.menubar = MenuBar() self.toolbar = ToolBar() self.torrentview = TorrentView() self.torrentdetails = TorrentDetails() self.sidebar = SideBar() self.filtertreeview = FilterTreeView() self.preferences = Preferences() self.systemtray = SystemTray() self.statusbar = StatusBar() self.addtorrentdialog = AddTorrentDialog() if osx_check() and WINDOWING == 'quartz': def nsapp_open_file(osxapp, filename): # Ignore command name which is raised at app launch (python opening main script). if filename == sys.argv[0]: return True process_args([filename]) self.osxapp.connect('NSApplicationOpenFile', nsapp_open_file) from deluge.ui.gtkui.menubar_osx import menubar_osx menubar_osx(self, self.osxapp) self.osxapp.ready() # Initalize the plugins self.plugins = PluginManager() # Show the connection manager self.connectionmanager = ConnectionManager() # Setup RPC stats logging # daemon_bps: time, bytes_sent, bytes_recv self.daemon_bps = (0, 0, 0) self.rpc_stats = LoopingCall(self.log_rpc_stats) self.closing = False # Twisted catches signals to terminate, so have it call a pre_shutdown method. reactor.addSystemEventTrigger('before', 'gtkui_close', self.close) def gtkui_sigint_handler(num, frame): log.debug('SIGINT signal caught, firing event: gtkui_close') reactor.callLater(0, reactor.fireSystemEvent, 'gtkui_close') signal.signal(signal.SIGINT, gtkui_sigint_handler) def start(self): reactor.callWhenRunning(self._on_reactor_start) # Initialize gdk threading threads_enter() reactor.run() # Reactor no longer running so async callbacks (Deferreds) cannot be # processed after this point. threads_leave() def shutdown(self, *args, **kwargs): log.debug('GTKUI shutting down...') # Shutdown all components if client.is_standalone: return component.shutdown() @defer.inlineCallbacks def close(self): if self.closing: return self.closing = True # Make sure the config is saved. self.config.save() # Ensure columns state is saved self.torrentview.save_state() # Shut down components yield self.shutdown() # The gtk modal dialogs (e.g. Preferences) can prevent the application # quitting, so force exiting by destroying MainWindow. Must be done here # to avoid hanging when quitting with SIGINT (CTRL-C). self.mainwindow.window.destroy() reactor.stop() # Restart the application after closing if MainWindow restart attribute set. if component.get('MainWindow').restart: os.execv(sys.argv[0], sys.argv) def log_rpc_stats(self): """Log RPC statistics for thinclient mode.""" if not client.connected(): return t = time.time() recv = client.get_bytes_recv() sent = client.get_bytes_sent() delta_time = t - self.daemon_bps[0] delta_sent = sent - self.daemon_bps[1] delta_recv = recv - self.daemon_bps[2] self.daemon_bps = (t, sent, recv) sent_rate = fspeed(delta_sent / delta_time) recv_rate = fspeed(delta_recv / delta_time) log.debug('RPC: Sent %s (%s) Recv %s (%s)', fsize(sent), sent_rate, fsize(recv), recv_rate) def _on_reactor_start(self): log.debug('_on_reactor_start') self.mainwindow.first_show() if not self.config['standalone']: return self._start_thinclient() err_msg = '' try: client.start_standalone() except DaemonRunningError: err_msg = _('A Deluge daemon (deluged) is already running.\n' 'To use Standalone mode, stop local daemon and restart Deluge.') except ImportError as ex: if 'No module named libtorrent' in ex.message: err_msg = _('Only Thin Client mode is available because libtorrent is not installed.\n' 'To use Standalone mode, please install libtorrent package.') else: log.exception(ex) err_msg = _('Only Thin Client mode is available due to unknown Import Error.\n' 'To use Standalone mode, please see logs for error details.') except Exception as ex: log.exception(ex) err_msg = _('Only Thin Client mode is available due to unknown Import Error.\n' 'To use Standalone mode, please see logs for error details.') else: component.start() return def on_dialog_response(response): """User response to switching mode dialog.""" if response == RESPONSE_YES: # Turning off standalone self.config['standalone'] = False self._start_thinclient() else: # User want keep Standalone Mode so just quit. self.mainwindow.quit() # An error occurred so ask user to switch from Standalone to Thin Client mode. err_msg += '\n\n' + _('Continue in Thin Client mode?') d = YesNoDialog(_('Change User Interface Mode'), err_msg).run() d.addCallback(on_dialog_response) def _start_thinclient(self): """Start the gtkui in thinclient mode""" if log.isEnabledFor(logging.DEBUG): self.rpc_stats.start(10) # Check to see if we need to start the localhost daemon if self.config['autostart_localhost']: def on_localhost_status(status_info, port): if status_info[1] == 'Offline': log.debug('Autostarting localhost: %s', host_config[0:3]) self.connectionmanager.start_daemon(port, get_config_dir()) for host_config in self.connectionmanager.hostlist.config['hosts']: if host_config[1] in LOCALHOST: d = self.connectionmanager.hostlist.get_host_status(host_config[0]) d.addCallback(on_localhost_status, host_config[2]) break # Autoconnect to a host if self.config['autoconnect']: for host_config in self.connectionmanager.hostlist.config['hosts']: host_id, host, port, user, __ = host_config if host_id == self.config['autoconnect_host_id']: log.debug('Trying to connect to %s@%s:%s', user, host, port) self.connectionmanager._connect(host_id, try_counter=6) break if self.config['show_connection_manager_on_start']: # Dialog is blocking so call last. self.connectionmanager.show() def __on_disconnect(self): """ Called when disconnected from the daemon. We basically just stop all the components here. """ self.daemon_bps = (0, 0, 0) component.stop()
class Core(component.Component): def __init__(self, listen_interface=None, outgoing_interface=None, read_only_config_keys=None): component.Component.__init__(self, 'Core') # Start the libtorrent session. user_agent = 'Deluge/{} libtorrent/{}'.format(DELUGE_VER, LT_VERSION) peer_id = self._create_peer_id(DELUGE_VER) log.debug('Starting session (peer_id: %s, user_agent: %s)', peer_id, user_agent) settings_pack = { 'peer_fingerprint': peer_id, 'user_agent': user_agent, 'ignore_resume_timestamps': True, } self.session = lt.session(settings_pack, flags=0) # Load the settings, if available. self._load_session_state() # Enable libtorrent extensions # Allows peers to download the metadata from the swarm directly self.session.add_extension('ut_metadata') # Ban peers that sends bad data self.session.add_extension('smart_ban') # Create the components self.eventmanager = EventManager() self.preferencesmanager = PreferencesManager() self.alertmanager = AlertManager() self.pluginmanager = PluginManager(self) self.torrentmanager = TorrentManager() self.filtermanager = FilterManager(self) self.authmanager = AuthManager() # New release check information self.new_release = None # External IP Address from libtorrent self.external_ip = None self.eventmanager.register_event_handler('ExternalIPEvent', self._on_external_ip_event) # GeoIP instance with db loaded self.geoip_instance = None # These keys will be dropped from the set_config() RPC and are # configurable from the command-line. self.read_only_config_keys = read_only_config_keys log.debug('read_only_config_keys: %s', read_only_config_keys) # Get the core config self.config = ConfigManager('core.conf') self.config.save() # If there was an interface value from the command line, use it, but # store the one in the config so we can restore it on shutdown self._old_listen_interface = None if listen_interface: if deluge.common.is_ip(listen_interface): self._old_listen_interface = self.config['listen_interface'] self.config['listen_interface'] = listen_interface else: log.error( 'Invalid listen interface (must be IP Address): %s', listen_interface, ) self._old_outgoing_interface = None if outgoing_interface: self._old_outgoing_interface = self.config['outgoing_interface'] self.config['outgoing_interface'] = outgoing_interface # New release check information self.__new_release = None # Session status timer self.session_status = {k.name: 0 for k in lt.session_stats_metrics()} self._session_prev_bytes = {k: 0 for k in SESSION_RATES_MAPPING} # Initiate other session status keys. self.session_status.update(self._session_prev_bytes) hit_ratio_keys = ['write_hit_ratio', 'read_hit_ratio'] self.session_status.update({k: 0.0 for k in hit_ratio_keys}) self.session_status_timer_interval = 0.5 self.session_status_timer = task.LoopingCall( self.session.post_session_stats) self.alertmanager.register_handler('session_stats_alert', self._on_alert_session_stats) self.session_rates_timer_interval = 2 self.session_rates_timer = task.LoopingCall(self._update_session_rates) def start(self): """Starts the core""" self.session_status_timer.start(self.session_status_timer_interval) self.session_rates_timer.start(self.session_rates_timer_interval, now=False) def stop(self): log.debug('Core stopping...') if self.session_status_timer.running: self.session_status_timer.stop() if self.session_rates_timer.running: self.session_rates_timer.stop() # Save the libtorrent session state self._save_session_state() # We stored a copy of the old interface value if self._old_listen_interface is not None: self.config['listen_interface'] = self._old_listen_interface if self._old_outgoing_interface is not None: self.config['outgoing_interface'] = self._old_outgoing_interface # Make sure the config file has been saved self.config.save() def shutdown(self): pass def apply_session_setting(self, key, value): self.apply_session_settings({key: value}) def apply_session_settings(self, settings): """Apply libtorrent session settings. Args: settings (dict): A dict of lt session settings to apply. """ self.session.apply_settings(settings) @staticmethod def _create_peer_id(version): """Create a peer_id fingerprint. This creates the peer_id and modifies the release char to identify pre-release and development version. Using ``D`` for dev, daily or nightly builds, ``a, b, r`` for pre-releases and ``s`` for stable releases. Examples: ``--<client><client><major><minor><micro><release>--`` ``--DE200D--`` (development version of 2.0.0) ``--DE200s--`` (stable release of v2.0.0) ``--DE201b--`` (beta pre-release of v2.0.1) Args: version (str): The version string in PEP440 dotted notation. Returns: str: The formattted peer_id with Deluge prefix e.g. '--DE200s--' """ split = deluge.common.VersionSplit(version) # Fill list with zeros to length of 4 and use lt to create fingerprint. version_list = split.version + [0] * (4 - len(split.version)) peer_id = lt.generate_fingerprint('DE', *version_list) def substitute_chr(string, idx, char): """Fast substitute single char in string.""" return string[:idx] + char + string[idx + 1:] if split.dev: release_chr = 'D' elif split.suffix: # a (alpha), b (beta) or r (release candidate). release_chr = split.suffix[0].lower() else: release_chr = 's' peer_id = substitute_chr(peer_id, 6, release_chr) return peer_id def _save_session_state(self): """Saves the libtorrent session state""" filename = 'session.state' filepath = get_config_dir(filename) filepath_bak = filepath + '.bak' filepath_tmp = filepath + '.tmp' try: if os.path.isfile(filepath): log.debug('Creating backup of %s at: %s', filename, filepath_bak) shutil.copy2(filepath, filepath_bak) except IOError as ex: log.error('Unable to backup %s to %s: %s', filepath, filepath_bak, ex) else: log.info('Saving the %s at: %s', filename, filepath) try: with open(filepath_tmp, 'wb') as _file: _file.write(lt.bencode(self.session.save_state())) _file.flush() os.fsync(_file.fileno()) shutil.move(filepath_tmp, filepath) except (IOError, EOFError) as ex: log.error('Unable to save %s: %s', filename, ex) if os.path.isfile(filepath_bak): log.info('Restoring backup of %s from: %s', filename, filepath_bak) shutil.move(filepath_bak, filepath) def _load_session_state(self): """Loads the libtorrent session state Returns: dict: A libtorrent sesion state, empty dict if unable to load it. """ filename = 'session.state' filepath = get_config_dir(filename) filepath_bak = filepath + '.bak' for _filepath in (filepath, filepath_bak): log.debug('Opening %s for load: %s', filename, _filepath) try: with open(_filepath, 'rb') as _file: state = lt.bdecode(_file.read()) except (IOError, EOFError, RuntimeError) as ex: log.warning('Unable to load %s: %s', _filepath, ex) else: log.info('Successfully loaded %s: %s', filename, _filepath) self.session.load_state(state) def _on_alert_session_stats(self, alert): """The handler for libtorrent session stats alert""" self.session_status.update(alert.values) self._update_session_cache_hit_ratio() def _update_session_cache_hit_ratio(self): """Calculates the cache read/write hit ratios for session_status.""" blocks_written = self.session_status['disk.num_blocks_written'] blocks_read = self.session_status['disk.num_blocks_read'] if blocks_written: self.session_status['write_hit_ratio'] = ( blocks_written - self.session_status['disk.num_write_ops']) / blocks_written else: self.session_status['write_hit_ratio'] = 0.0 if blocks_read: self.session_status['read_hit_ratio'] = ( self.session_status['disk.num_blocks_cache_hits'] / blocks_read) else: self.session_status['read_hit_ratio'] = 0.0 def _update_session_rates(self): """Calculate session status rates. Uses polling interval and counter difference for session_status rates. """ for rate_key, prev_bytes in list(self._session_prev_bytes.items()): new_bytes = self.session_status[SESSION_RATES_MAPPING[rate_key]] self.session_status[rate_key] = ( new_bytes - prev_bytes) / self.session_rates_timer_interval # Store current value for next update. self._session_prev_bytes[rate_key] = new_bytes def get_new_release(self): log.debug('get_new_release') try: self.new_release = ( urlopen('http://download.deluge-torrent.org/version-2.0').read( ).decode().strip()) except URLError as ex: log.debug('Unable to get release info from website: %s', ex) else: self.check_new_release() def check_new_release(self): if self.new_release: log.debug('new_release: %s', self.new_release) if deluge.common.VersionSplit( self.new_release) > deluge.common.VersionSplit( deluge.common.get_version()): component.get('EventManager').emit( NewVersionAvailableEvent(self.new_release)) return self.new_release return False # Exported Methods @export def add_torrent_file_async(self, filename, filedump, options, save_state=True): """Adds a torrent file to the session asynchonously. Args: filename (str): The filename of the torrent. filedump (str): A base64 encoded string of torrent file contents. options (dict): The options to apply to the torrent upon adding. save_state (bool): If the state should be saved after adding the file. Returns: Deferred: The torrent ID or None. """ try: filedump = b64decode(filedump) except TypeError as ex: log.error('There was an error decoding the filedump string: %s', ex) try: d = self.torrentmanager.add_async( filedump=filedump, options=options, filename=filename, save_state=save_state, ) except RuntimeError as ex: log.error('There was an error adding the torrent file %s: %s', filename, ex) raise else: return d @export def prefetch_magnet_metadata(self, magnet, timeout=30): """Download magnet metadata without adding to Deluge session. Used by UIs to get magnet files for selection before adding to session. Args: magnet (str): The magnet uri. timeout (int): Number of seconds to wait before cancelling request. Returns: Deferred: A tuple of (torrent_id (str), metadata (dict)) for the magnet. """ def on_metadata(result, result_d): """Return result of torrent_id and metadata""" result_d.callback(result) return result d = self.torrentmanager.prefetch_metadata(magnet, timeout) # Use a seperate callback chain to handle existing prefetching magnet. result_d = defer.Deferred() d.addBoth(on_metadata, result_d) return result_d @export def add_torrent_file(self, filename, filedump, options): """Adds a torrent file to the session. Args: filename (str): The filename of the torrent. filedump (str): A base64 encoded string of the torrent file contents. options (dict): The options to apply to the torrent upon adding. Returns: str: The torrent_id or None. """ try: filedump = b64decode(filedump) except Exception as ex: log.error('There was an error decoding the filedump string: %s', ex) try: return self.torrentmanager.add(filedump=filedump, options=options, filename=filename) except RuntimeError as ex: log.error('There was an error adding the torrent file %s: %s', filename, ex) raise @export def add_torrent_files(self, torrent_files): """Adds multiple torrent files to the session asynchonously. Args: torrent_files (list of tuples): Torrent files as tuple of (filename, filedump, options). Returns: Deferred """ @defer.inlineCallbacks def add_torrents(): errors = [] last_index = len(torrent_files) - 1 for idx, torrent in enumerate(torrent_files): try: yield self.add_torrent_file_async( torrent[0], torrent[1], torrent[2], save_state=idx == last_index) except AddTorrentError as ex: log.warning('Error when adding torrent: %s', ex) errors.append(ex) defer.returnValue(errors) return task.deferLater(reactor, 0, add_torrents) @export def add_torrent_url(self, url, options, headers=None): """ Adds a torrent from a url. Deluge will attempt to fetch the torrent from url prior to adding it to the session. :param url: the url pointing to the torrent file :type url: string :param options: the options to apply to the torrent on add :type options: dict :param headers: any optional headers to send :type headers: dict :returns: a Deferred which returns the torrent_id as a str or None """ log.info('Attempting to add url %s', url) def on_download_success(filename): # We got the file, so add it to the session with open(filename, 'rb') as _file: data = _file.read() try: os.remove(filename) except OSError as ex: log.warning('Could not remove temp file: %s', ex) return self.add_torrent_file(filename, b64encode(data), options) def on_download_fail(failure): # Log the error and pass the failure onto the client log.error('Failed to add torrent from url %s', url) return failure tmp_fd, tmp_file = tempfile.mkstemp(prefix='deluge_url.', suffix='.torrent') os.close(tmp_fd) d = download_file(url, tmp_file, headers=headers, force_filename=True) d.addCallbacks(on_download_success, on_download_fail) return d @export def add_torrent_magnet(self, uri, options): """ Adds a torrent from a magnet link. :param uri: the magnet link :type uri: string :param options: the options to apply to the torrent on add :type options: dict :returns: the torrent_id :rtype: string """ log.debug('Attempting to add by magnet uri: %s', uri) return self.torrentmanager.add(magnet=uri, options=options) @export def remove_torrent(self, torrent_id, remove_data): """Removes a single torrent from the session. Args: torrent_id (str): The torrent ID to remove. remove_data (bool): If True, also remove the downloaded data. Returns: bool: True if removed successfully. Raises: InvalidTorrentError: If the torrent ID does not exist in the session. """ log.debug('Removing torrent %s from the core.', torrent_id) return self.torrentmanager.remove(torrent_id, remove_data) @export def remove_torrents(self, torrent_ids, remove_data): """Remove multiple torrents from the session. Args: torrent_ids (list): The torrent IDs to remove. remove_data (bool): If True, also remove the downloaded data. Returns: list: An empty list if no errors occurred otherwise the list contains tuples of strings, a torrent ID and an error message. For example: [('<torrent_id>', 'Error removing torrent')] """ log.info('Removing %d torrents from core.', len(torrent_ids)) def do_remove_torrents(): errors = [] for torrent_id in torrent_ids: try: self.torrentmanager.remove(torrent_id, remove_data=remove_data, save_state=False) except InvalidTorrentError as ex: errors.append((torrent_id, str(ex))) # Save the session state self.torrentmanager.save_state() if errors: log.warning('Failed to remove %d of %d torrents.', len(errors), len(torrent_ids)) return errors return task.deferLater(reactor, 0, do_remove_torrents) @export def get_session_status(self, keys): """Gets the session status values for 'keys', these keys are taking from libtorrent's session status. See: http://www.rasterbar.com/products/libtorrent/manual.html#status :param keys: the keys for which we want values :type keys: list :returns: a dictionary of {key: value, ...} :rtype: dict """ if not keys: return self.session_status status = {} for key in keys: try: status[key] = self.session_status[key] except KeyError: if key in DEPR_SESSION_STATUS_KEYS: new_key = DEPR_SESSION_STATUS_KEYS[key] log.debug( 'Deprecated session status key %s, please use %s', key, new_key) status[key] = self.session_status[new_key] else: log.warning('Session status key not valid: %s', key) return status @export def force_reannounce(self, torrent_ids): log.debug('Forcing reannouncment to: %s', torrent_ids) for torrent_id in torrent_ids: self.torrentmanager[torrent_id].force_reannounce() @export def pause_torrent(self, torrent_id): """Pauses a torrent""" log.debug('Pausing: %s', torrent_id) if not isinstance(torrent_id, string_types): self.pause_torrents(torrent_id) else: self.torrentmanager[torrent_id].pause() @export def pause_torrents(self, torrent_ids=None): """Pauses a list of torrents""" if not torrent_ids: torrent_ids = self.torrentmanager.get_torrent_list() for torrent_id in torrent_ids: self.pause_torrent(torrent_id) @export def connect_peer(self, torrent_id, ip, port): log.debug('adding peer %s to %s', ip, torrent_id) if not self.torrentmanager[torrent_id].connect_peer(ip, port): log.warning('Error adding peer %s:%s to %s', ip, port, torrent_id) @export def move_storage(self, torrent_ids, dest): log.debug('Moving storage %s to %s', torrent_ids, dest) for torrent_id in torrent_ids: if not self.torrentmanager[torrent_id].move_storage(dest): log.warning('Error moving torrent %s to %s', torrent_id, dest) @export def pause_session(self): """Pause the entire session""" if not self.session.is_paused(): self.session.pause() component.get('EventManager').emit(SessionPausedEvent()) @export def resume_session(self): """Resume the entire session""" if self.session.is_paused(): self.session.resume() for torrent_id in self.torrentmanager.torrents: self.torrentmanager[torrent_id].update_state() component.get('EventManager').emit(SessionResumedEvent()) @export def is_session_paused(self): """Returns the activity of the session""" return self.session.is_paused() @export def resume_torrent(self, torrent_id): """Resumes a torrent""" log.debug('Resuming: %s', torrent_id) if not isinstance(torrent_id, string_types): self.resume_torrents(torrent_id) else: self.torrentmanager[torrent_id].resume() @export def resume_torrents(self, torrent_ids=None): """Resumes a list of torrents""" if not torrent_ids: torrent_ids = self.torrentmanager.get_torrent_list() for torrent_id in torrent_ids: self.resume_torrent(torrent_id) def create_torrent_status( self, torrent_id, torrent_keys, plugin_keys, diff=False, update=False, all_keys=False, ): try: status = self.torrentmanager[torrent_id].get_status( torrent_keys, diff, update=update, all_keys=all_keys) except KeyError: import traceback traceback.print_exc() # Torrent was probaly removed meanwhile return {} # Ask the plugin manager to fill in the plugin keys if len(plugin_keys) > 0 or all_keys: status.update( self.pluginmanager.get_status(torrent_id, plugin_keys)) return status @export def get_torrent_status(self, torrent_id, keys, diff=False): torrent_keys, plugin_keys = self.torrentmanager.separate_keys( keys, [torrent_id]) return self.create_torrent_status( torrent_id, torrent_keys, plugin_keys, diff=diff, update=True, all_keys=not keys, ) @export def get_torrents_status(self, filter_dict, keys, diff=False): """ returns all torrents , optionally filtered by filter_dict. """ torrent_ids = self.filtermanager.filter_torrent_ids(filter_dict) d = self.torrentmanager.torrents_status_update(torrent_ids, keys, diff=diff) def add_plugin_fields(args): status_dict, plugin_keys = args # Ask the plugin manager to fill in the plugin keys if len(plugin_keys) > 0: for key in status_dict: status_dict[key].update( self.pluginmanager.get_status(key, plugin_keys)) return status_dict d.addCallback(add_plugin_fields) return d @export def get_filter_tree(self, show_zero_hits=True, hide_cat=None): """ returns {field: [(value,count)] } for use in sidebar(s) """ return self.filtermanager.get_filter_tree(show_zero_hits, hide_cat) @export def get_session_state(self): """Returns a list of torrent_ids in the session.""" # Get the torrent list from the TorrentManager return self.torrentmanager.get_torrent_list() @export def get_config(self): """Get all the preferences as a dictionary""" return self.config.config @export def get_config_value(self, key): """Get the config value for key""" return self.config.get(key) @export def get_config_values(self, keys): """Get the config values for the entered keys""" return {key: self.config.get(key) for key in keys} @export def set_config(self, config): """Set the config with values from dictionary""" # Load all the values into the configuration for key in config: if self.read_only_config_keys and key in self.read_only_config_keys: continue self.config[key] = config[key] @export def get_listen_port(self): """Returns the active listen port""" return self.session.listen_port() @export def get_proxy(self): """Returns the proxy settings Returns: dict: Contains proxy settings. Notes: Proxy type names: 0: None, 1: Socks4, 2: Socks5, 3: Socks5 w Auth, 4: HTTP, 5: HTTP w Auth, 6: I2P """ settings = self.session.get_settings() proxy_type = settings['proxy_type'] proxy_hostname = (settings['i2p_hostname'] if proxy_type == 6 else settings['proxy_hostname']) proxy_port = settings['i2p_port'] if proxy_type == 6 else settings[ 'proxy_port'] proxy_dict = { 'type': proxy_type, 'hostname': proxy_hostname, 'username': settings['proxy_username'], 'password': settings['proxy_password'], 'port': proxy_port, 'proxy_hostnames': settings['proxy_hostnames'], 'proxy_peer_connections': settings['proxy_peer_connections'], 'proxy_tracker_connections': settings['proxy_tracker_connections'], } return proxy_dict @export def get_available_plugins(self): """Returns a list of plugins available in the core""" return self.pluginmanager.get_available_plugins() @export def get_enabled_plugins(self): """Returns a list of enabled plugins in the core""" return self.pluginmanager.get_enabled_plugins() @export def enable_plugin(self, plugin): return self.pluginmanager.enable_plugin(plugin) @export def disable_plugin(self, plugin): return self.pluginmanager.disable_plugin(plugin) @export def force_recheck(self, torrent_ids): """Forces a data recheck on torrent_ids""" for torrent_id in torrent_ids: self.torrentmanager[torrent_id].force_recheck() @export def set_torrent_options(self, torrent_ids, options): """Sets the torrent options for torrent_ids Args: torrent_ids (list): A list of torrent_ids to set the options for. options (dict): A dict of torrent options to set. See torrent.TorrentOptions class for valid keys. """ if 'owner' in options and not self.authmanager.has_account( options['owner']): raise DelugeError('Username "%s" is not known.' % options['owner']) if isinstance(torrent_ids, string_types): torrent_ids = [torrent_ids] for torrent_id in torrent_ids: self.torrentmanager[torrent_id].set_options(options) @export def set_torrent_trackers(self, torrent_id, trackers): """Sets a torrents tracker list. trackers will be [{"url", "tier"}]""" return self.torrentmanager[torrent_id].set_trackers(trackers) @deprecated @export def set_torrent_max_connections(self, torrent_id, value): """Deprecated: Use set_torrent_options with 'max_connections'""" self.set_torrent_options([torrent_id], {'max_connections': value}) @deprecated @export def set_torrent_max_upload_slots(self, torrent_id, value): """Deprecated: Use set_torrent_options with 'max_upload_slots'""" self.set_torrent_options([torrent_id], {'max_upload_slots': value}) @deprecated @export def set_torrent_max_upload_speed(self, torrent_id, value): """Deprecated: Use set_torrent_options with 'max_upload_speed'""" self.set_torrent_options([torrent_id], {'max_upload_speed': value}) @deprecated @export def set_torrent_max_download_speed(self, torrent_id, value): """Deprecated: Use set_torrent_options with 'max_download_speed'""" self.set_torrent_options([torrent_id], {'max_download_speed': value}) @deprecated @export def set_torrent_file_priorities(self, torrent_id, priorities): """Deprecated: Use set_torrent_options with 'file_priorities'""" self.set_torrent_options([torrent_id], {'file_priorities': priorities}) @deprecated @export def set_torrent_prioritize_first_last(self, torrent_id, value): """Deprecated: Use set_torrent_options with 'prioritize_first_last'""" self.set_torrent_options([torrent_id], {'prioritize_first_last_pieces': value}) @deprecated @export def set_torrent_auto_managed(self, torrent_id, value): """Deprecated: Use set_torrent_options with 'auto_managed'""" self.set_torrent_options([torrent_id], {'auto_managed': value}) @deprecated @export def set_torrent_stop_at_ratio(self, torrent_id, value): """Deprecated: Use set_torrent_options with 'stop_at_ratio'""" self.set_torrent_options([torrent_id], {'stop_at_ratio': value}) @deprecated @export def set_torrent_stop_ratio(self, torrent_id, value): """Deprecated: Use set_torrent_options with 'stop_ratio'""" self.set_torrent_options([torrent_id], {'stop_ratio': value}) @deprecated @export def set_torrent_remove_at_ratio(self, torrent_id, value): """Deprecated: Use set_torrent_options with 'remove_at_ratio'""" self.set_torrent_options([torrent_id], {'remove_at_ratio': value}) @deprecated @export def set_torrent_move_completed(self, torrent_id, value): """Deprecated: Use set_torrent_options with 'move_completed'""" self.set_torrent_options([torrent_id], {'move_completed': value}) @deprecated @export def set_torrent_move_completed_path(self, torrent_id, value): """Deprecated: Use set_torrent_options with 'move_completed_path'""" self.set_torrent_options([torrent_id], {'move_completed_path': value}) @export def get_path_size(self, path): """Returns the size of the file or folder 'path' and -1 if the path is unaccessible (non-existent or insufficient privs)""" return deluge.common.get_path_size(path) @export def create_torrent( self, path, tracker, piece_length, comment, target, webseeds, private, created_by, trackers, add_to_session, ): log.debug('creating torrent..') threading.Thread( target=self._create_torrent_thread, args=( path, tracker, piece_length, comment, target, webseeds, private, created_by, trackers, add_to_session, ), ).start() def _create_torrent_thread( self, path, tracker, piece_length, comment, target, webseeds, private, created_by, trackers, add_to_session, ): from deluge import metafile metafile.make_meta_file( path, tracker, piece_length, comment=comment, target=target, webseeds=webseeds, private=private, created_by=created_by, trackers=trackers, ) log.debug('torrent created!') if add_to_session: options = {} options['download_location'] = os.path.split(path)[0] with open(target, 'rb') as _file: filedump = b64encode(_file.read()) self.add_torrent_file( os.path.split(target)[1], filedump, options) @export def upload_plugin(self, filename, filedump): """This method is used to upload new plugins to the daemon. It is used when connecting to the daemon remotely and installing a new plugin on the client side. 'plugin_data' is a xmlrpc.Binary object of the file data, ie, plugin_file.read()""" try: filedump = b64decode(filedump) except TypeError as ex: log.error('There was an error decoding the filedump string!') log.exception(ex) return with open(os.path.join(get_config_dir(), 'plugins', filename), 'wb') as _file: _file.write(filedump) component.get('CorePluginManager').scan_for_plugins() @export def rescan_plugins(self): """ Rescans the plugin folders for new plugins """ component.get('CorePluginManager').scan_for_plugins() @export def rename_files(self, torrent_id, filenames): """ Rename files in torrent_id. Since this is an asynchronous operation by libtorrent, watch for the TorrentFileRenamedEvent to know when the files have been renamed. :param torrent_id: the torrent_id to rename files :type torrent_id: string :param filenames: a list of index, filename pairs :type filenames: ((index, filename), ...) :raises InvalidTorrentError: if torrent_id is invalid """ if torrent_id not in self.torrentmanager.torrents: raise InvalidTorrentError('torrent_id is not in session') def rename(): self.torrentmanager[torrent_id].rename_files(filenames) return task.deferLater(reactor, 0, rename) @export def rename_folder(self, torrent_id, folder, new_folder): """ Renames the 'folder' to 'new_folder' in 'torrent_id'. Watch for the TorrentFolderRenamedEvent which is emitted when the folder has been renamed successfully. :param torrent_id: the torrent to rename folder in :type torrent_id: string :param folder: the folder to rename :type folder: string :param new_folder: the new folder name :type new_folder: string :raises InvalidTorrentError: if the torrent_id is invalid """ if torrent_id not in self.torrentmanager.torrents: raise InvalidTorrentError('torrent_id is not in session') return self.torrentmanager[torrent_id].rename_folder( folder, new_folder) @export def queue_top(self, torrent_ids): log.debug('Attempting to queue %s to top', torrent_ids) # torrent_ids must be sorted in reverse before moving to preserve order for torrent_id in sorted(torrent_ids, key=self.torrentmanager.get_queue_position, reverse=True): try: # If the queue method returns True, then we should emit a signal if self.torrentmanager.queue_top(torrent_id): component.get('EventManager').emit( TorrentQueueChangedEvent()) except KeyError: log.warning('torrent_id: %s does not exist in the queue', torrent_id) @export def queue_up(self, torrent_ids): log.debug('Attempting to queue %s to up', torrent_ids) torrents = ((self.torrentmanager.get_queue_position(torrent_id), torrent_id) for torrent_id in torrent_ids) torrent_moved = True prev_queue_position = None # torrent_ids must be sorted before moving. for queue_position, torrent_id in sorted(torrents): # Move the torrent if and only if there is space (by not moving it we preserve the order) if torrent_moved or queue_position - prev_queue_position > 1: try: torrent_moved = self.torrentmanager.queue_up(torrent_id) except KeyError: log.warning('torrent_id: %s does not exist in the queue', torrent_id) # If the torrent moved, then we should emit a signal if torrent_moved: component.get('EventManager').emit(TorrentQueueChangedEvent()) else: prev_queue_position = queue_position @export def queue_down(self, torrent_ids): log.debug('Attempting to queue %s to down', torrent_ids) torrents = ((self.torrentmanager.get_queue_position(torrent_id), torrent_id) for torrent_id in torrent_ids) torrent_moved = True prev_queue_position = None # torrent_ids must be sorted before moving. for queue_position, torrent_id in sorted(torrents, reverse=True): # Move the torrent if and only if there is space (by not moving it we preserve the order) if torrent_moved or prev_queue_position - queue_position > 1: try: torrent_moved = self.torrentmanager.queue_down(torrent_id) except KeyError: log.warning('torrent_id: %s does not exist in the queue', torrent_id) # If the torrent moved, then we should emit a signal if torrent_moved: component.get('EventManager').emit(TorrentQueueChangedEvent()) else: prev_queue_position = queue_position @export def queue_bottom(self, torrent_ids): log.debug('Attempting to queue %s to bottom', torrent_ids) # torrent_ids must be sorted before moving to preserve order for torrent_id in sorted(torrent_ids, key=self.torrentmanager.get_queue_position): try: # If the queue method returns True, then we should emit a signal if self.torrentmanager.queue_bottom(torrent_id): component.get('EventManager').emit( TorrentQueueChangedEvent()) except KeyError: log.warning('torrent_id: %s does not exist in the queue', torrent_id) @export def glob(self, path): return glob.glob(path) @export def test_listen_port(self): """ Checks if the active port is open :returns: True if the port is open, False if not :rtype: bool """ port = self.get_listen_port() url = 'https://deluge-torrent.org/test_port.php?port=%s' % port agent = Agent(reactor, connectTimeout=30) d = agent.request(b'GET', url.encode()) def on_get_page(body): return bool(int(body)) def on_error(failure): log.warning('Error testing listen port: %s', failure) d.addCallback(readBody).addCallback(on_get_page) d.addErrback(on_error) return d @export def get_free_space(self, path=None): """ Returns the number of free bytes at path :param path: the path to check free space at, if None, use the default download location :type path: string :returns: the number of free bytes at path :rtype: int :raises InvalidPathError: if the path is invalid """ if not path: path = self.config['download_location'] try: return deluge.common.free_space(path) except InvalidPathError: return -1 def _on_external_ip_event(self, external_ip): self.external_ip = external_ip @export def get_external_ip(self): """ Returns the external ip address recieved from libtorrent. """ return self.external_ip @export def get_libtorrent_version(self): """ Returns the libtorrent version. :returns: the version :rtype: string """ return LT_VERSION @export def get_completion_paths(self, args): """ Returns the available path completions for the input value. """ return path_chooser_common.get_completion_paths(args) @export(AUTH_LEVEL_ADMIN) def get_known_accounts(self): return self.authmanager.get_known_accounts() @export(AUTH_LEVEL_NONE) def get_auth_levels_mappings(self): return (AUTH_LEVELS_MAPPING, AUTH_LEVELS_MAPPING_REVERSE) @export(AUTH_LEVEL_ADMIN) def create_account(self, username, password, authlevel): return self.authmanager.create_account(username, password, authlevel) @export(AUTH_LEVEL_ADMIN) def update_account(self, username, password, authlevel): return self.authmanager.update_account(username, password, authlevel) @export(AUTH_LEVEL_ADMIN) def remove_account(self, username): return self.authmanager.remove_account(username)
class Core(CorePluginBase): def enable(self): self.config = ConfigManager('execute.conf', DEFAULT_CONFIG) event_manager = component.get('EventManager') self.registered_events = {} self.preremoved_cache = {} # Go through the commands list and register event handlers for command in self.config['commands']: event = command[EXECUTE_EVENT] if event in self.registered_events: continue def create_event_handler(event): def event_handler(torrent_id, *arg): self.execute_commands(torrent_id, event, *arg) return event_handler event_handler = create_event_handler(event) event_manager.register_event_handler(EVENT_MAP[event], event_handler) if event == 'removed': event_manager.register_event_handler('PreTorrentRemovedEvent', self.on_preremoved) self.registered_events[event] = event_handler log.debug('Execute core plugin enabled!') def on_preremoved(self, torrent_id): # Get and store the torrent info before it is removed torrent = component.get('TorrentManager').torrents[torrent_id] info = torrent.get_status(['name', 'download_location']) self.preremoved_cache[torrent_id] = [torrent_id, info['name'], info['download_location']] def execute_commands(self, torrent_id, event, *arg): if event == 'added' and arg[0]: # No futher action as from_state (arg[0]) is True return elif event == 'removed': torrent_id, torrent_name, download_location = self.preremoved_cache.pop(torrent_id) else: torrent = component.get('TorrentManager').torrents[torrent_id] info = torrent.get_status(['name', 'download_location']) # Grab the torrent name and download location # getProcessOutputAndValue requires args to be str torrent_name = info['name'] download_location = info['download_location'] log.debug('Running commands for %s', event) def log_error(result, command): (stdout, stderr, exit_code) = result if exit_code: log.warn('Command "%s" failed with exit code %d', command, exit_code) if stdout: log.warn('stdout: %s', stdout) if stderr: log.warn('stderr: %s', stderr) # Go through and execute all the commands for command in self.config['commands']: if command[EXECUTE_EVENT] == event: command = os.path.expandvars(command[EXECUTE_COMMAND]) command = os.path.expanduser(command) cmd_args = [torrent_id.encode('utf8'), torrent_name.encode('utf8'), download_location.encode('utf8')] if windows_check(): # Escape ampersand on windows (see #2784) cmd_args = [cmd_arg.replace('&', '^^^&') for cmd_arg in cmd_args] if os.path.isfile(command) and os.access(command, os.X_OK): log.debug('Running %s with args: %s', command, cmd_args) d = getProcessOutputAndValue(command, cmd_args, env=os.environ) d.addCallback(log_error, command) else: log.error('Execute script not found or not executable') def disable(self): self.config.save() event_manager = component.get('EventManager') for event, handler in self.registered_events.items(): event_manager.deregister_event_handler(event, handler) log.debug('Execute core plugin disabled!') # Exported RPC methods # @export def add_command(self, event, command): command_id = hashlib.sha1(str(time.time())).hexdigest() self.config['commands'].append((command_id, event, command)) self.config.save() component.get('EventManager').emit(ExecuteCommandAddedEvent(command_id, event, command)) @export def get_commands(self): return self.config['commands'] @export def remove_command(self, command_id): for command in self.config['commands']: if command[EXECUTE_ID] == command_id: self.config['commands'].remove(command) component.get('EventManager').emit(ExecuteCommandRemovedEvent(command_id)) break self.config.save() @export def save_command(self, command_id, event, cmd): for i, command in enumerate(self.config['commands']): if command[EXECUTE_ID] == command_id: self.config['commands'][i] = (command_id, event, cmd) break self.config.save()
class Core(CorePluginBase): def enable(self): self.config = ConfigManager('execute.conf', DEFAULT_CONFIG) event_manager = component.get('EventManager') self.registered_events = {} self.preremoved_cache = {} # Go through the commands list and register event handlers for command in self.config['commands']: event = command[EXECUTE_EVENT] if event in self.registered_events: continue def create_event_handler(event): def event_handler(torrent_id, *arg): self.execute_commands(torrent_id, event, *arg) return event_handler event_handler = create_event_handler(event) event_manager.register_event_handler(EVENT_MAP[event], event_handler) if event == 'removed': event_manager.register_event_handler('PreTorrentRemovedEvent', self.on_preremoved) self.registered_events[event] = event_handler log.debug('Execute core plugin enabled!') def on_preremoved(self, torrent_id): # Get and store the torrent info before it is removed torrent = component.get('TorrentManager').torrents[torrent_id] info = torrent.get_status(['name', 'download_location']) self.preremoved_cache[torrent_id] = [ torrent_id, info['name'], info['download_location'], ] def execute_commands(self, torrent_id, event, *arg): if event == 'added' and arg[0]: # No futher action as from_state (arg[0]) is True return elif event == 'removed': torrent_id, torrent_name, download_location = self.preremoved_cache.pop( torrent_id) else: torrent = component.get('TorrentManager').torrents[torrent_id] info = torrent.get_status(['name', 'download_location']) # Grab the torrent name and download location # getProcessOutputAndValue requires args to be str torrent_name = info['name'] download_location = info['download_location'] log.debug('Running commands for %s', event) def log_error(result, command): (stdout, stderr, exit_code) = result if exit_code: log.warning('Command "%s" failed with exit code %d', command, exit_code) if stdout: log.warning('stdout: %s', stdout) if stderr: log.warning('stderr: %s', stderr) # Go through and execute all the commands for command in self.config['commands']: if command[EXECUTE_EVENT] == event: command = os.path.expandvars(command[EXECUTE_COMMAND]) command = os.path.expanduser(command) cmd_args = [ torrent_id.encode('utf8'), torrent_name.encode('utf8'), download_location.encode('utf8'), ] if windows_check(): # Escape ampersand on windows (see #2784) cmd_args = [ cmd_arg.replace('&', '^^^&') for cmd_arg in cmd_args ] if os.path.isfile(command) and os.access(command, os.X_OK): log.debug('Running %s with args: %s', command, cmd_args) d = getProcessOutputAndValue(command, cmd_args, env=os.environ) d.addCallback(log_error, command) else: log.error('Execute script not found or not executable') def disable(self): self.config.save() event_manager = component.get('EventManager') for event, handler in self.registered_events.items(): event_manager.deregister_event_handler(event, handler) log.debug('Execute core plugin disabled!') # Exported RPC methods # @export def add_command(self, event, command): command_id = hashlib.sha1(str(time.time()).encode()).hexdigest() self.config['commands'].append((command_id, event, command)) self.config.save() component.get('EventManager').emit( ExecuteCommandAddedEvent(command_id, event, command)) @export def get_commands(self): return self.config['commands'] @export def remove_command(self, command_id): for command in self.config['commands']: if command[EXECUTE_ID] == command_id: self.config['commands'].remove(command) component.get('EventManager').emit( ExecuteCommandRemovedEvent(command_id)) break self.config.save() @export def save_command(self, command_id, event, cmd): for i, command in enumerate(self.config['commands']): if command[EXECUTE_ID] == command_id: self.config['commands'][i] = (command_id, event, cmd) break self.config.save()
class Core(CorePluginBase): def enable(self): self.config = ConfigManager("execute.conf", DEFAULT_CONFIG) event_manager = component.get("EventManager") self.torrent_manager = component.get("TorrentManager") self.registered_events = {} self.preremoved_cache = {} # Go through the commands list and register event handlers for command in self.config["commands"]: event = command[EXECUTE_EVENT] if event in self.registered_events: continue def create_event_handler(event): def event_handler(torrent_id): self.execute_commands(torrent_id, event) return event_handler event_handler = create_event_handler(event) event_manager.register_event_handler(EVENT_MAP[event], event_handler) if event == "removed": event_manager.register_event_handler("PreTorrentRemovedEvent", self.on_preremoved) self.registered_events[event] = event_handler log.debug("Execute core plugin enabled!") def on_preremoved(self, torrent_id): # Get and store the torrent info before it is removed torrent = component.get("TorrentManager").torrents[torrent_id] info = torrent.get_status(["name", "save_path"]) self.preremoved_cache[torrent_id] = [ utf8_encoded(torrent_id), utf8_encoded(info["name"]), utf8_encoded(info["save_path"]) ] def execute_commands(self, torrent_id, event): if event == "added" and not self.torrent_manager.session_started: return elif event == "removed": torrent_id, torrent_name, save_path = self.preremoved_cache.pop( torrent_id) else: torrent = component.get("TorrentManager").torrents[torrent_id] info = torrent.get_status(["name", "save_path"]) # getProcessOutputAndValue requires args to be str torrent_id = utf8_encoded(torrent_id) torrent_name = utf8_encoded(info["name"]) save_path = utf8_encoded(info["save_path"]) log.debug("[execute] Running commands for %s", event) def log_error(result, command): (stdout, stderr, exit_code) = result if exit_code: log.warn("[execute] command '%s' failed with exit code %d", command, exit_code) if stdout: log.warn("[execute] stdout: %s", stdout) if stderr: log.warn("[execute] stderr: %s", stderr) # Go through and execute all the commands for command in self.config["commands"]: if command[EXECUTE_EVENT] == event: command = os.path.expandvars(command[EXECUTE_COMMAND]) command = os.path.expanduser(command) cmd_args = [torrent_id, torrent_name, save_path] if windows_check: # Escape ampersand on windows (see #2784) cmd_args = [arg.replace("&", "^^^&") for arg in cmd_args] if os.path.isfile(command) and os.access(command, os.X_OK): log.debug("[execute] Running %s with args: %s", command, cmd_args) d = getProcessOutputAndValue(command, cmd_args, env=os.environ) d.addCallback(log_error, command) else: log.error( "[execute] Execute script not found or not executable") def disable(self): self.config.save() event_manager = component.get("EventManager") for event, handler in self.registered_events.iteritems(): event_manager.deregister_event_handler(event, handler) log.debug("Execute core plugin disabled!") ### Exported RPC methods ### @export def add_command(self, event, command): command_id = hashlib.sha1(str(time.time())).hexdigest() self.config["commands"].append((command_id, event, command)) self.config.save() component.get("EventManager").emit( ExecuteCommandAddedEvent(command_id, event, command)) @export def get_commands(self): return self.config["commands"] @export def remove_command(self, command_id): for command in self.config["commands"]: if command[EXECUTE_ID] == command_id: self.config["commands"].remove(command) component.get("EventManager").emit( ExecuteCommandRemovedEvent(command_id)) break self.config.save() @export def save_command(self, command_id, event, cmd): for i, command in enumerate(self.config["commands"]): if command[EXECUTE_ID] == command_id: self.config["commands"][i] = (command_id, event, cmd) break self.config.save()
class GtkUI(object): def __init__(self, args): # Setup gtkbuilder/glade translation setup_translations(setup_gettext=False, setup_pygtk=True) # Setup signals def on_die(*args): log.debug('OS signal "die" caught with args: %s', args) reactor.stop() if windows_check(): from win32api import SetConsoleCtrlHandler SetConsoleCtrlHandler(on_die, True) log.debug('Win32 "die" handler registered') elif osx_check() and WINDOWING == 'quartz': import gtkosx_application self.osxapp = gtkosx_application.gtkosx_application_get() self.osxapp.connect('NSApplicationWillTerminate', on_die) log.debug('OSX quartz "die" handler registered') # Set process name again to fix gtk issue setproctitle(getproctitle()) # Attempt to register a magnet URI handler with gconf, but do not overwrite # if already set by another program. associate_magnet_links(False) # Make sure gtkui.conf has at least the defaults set self.config = ConfigManager('gtkui.conf', DEFAULT_PREFS) # Make sure the gtkui state folder has been created if not os.path.exists(os.path.join(get_config_dir(), 'gtkui_state')): os.makedirs(os.path.join(get_config_dir(), 'gtkui_state')) # Set language if self.config['language'] is not None: set_language(self.config['language']) # Start the IPC Interface before anything else.. Just in case we are # already running. self.queuedtorrents = QueuedTorrents() self.ipcinterface = IPCInterface(args.torrents) # Initialize gdk threading threads_init() # We make sure that the UI components start once we get a core URI client.set_disconnect_callback(self.__on_disconnect) self.trackericons = TrackerIcons() self.sessionproxy = SessionProxy() # Initialize various components of the gtkui self.mainwindow = MainWindow() self.menubar = MenuBar() self.toolbar = ToolBar() self.torrentview = TorrentView() self.torrentdetails = TorrentDetails() self.sidebar = SideBar() self.filtertreeview = FilterTreeView() self.preferences = Preferences() self.systemtray = SystemTray() self.statusbar = StatusBar() self.addtorrentdialog = AddTorrentDialog() if osx_check() and WINDOWING == 'quartz': def nsapp_open_file(osxapp, filename): # Ignore command name which is raised at app launch (python opening main script). if filename == sys.argv[0]: return True process_args([filename]) self.osxapp.connect('NSApplicationOpenFile', nsapp_open_file) from deluge.ui.gtkui.menubar_osx import menubar_osx menubar_osx(self, self.osxapp) self.osxapp.ready() # Initalize the plugins self.plugins = PluginManager() # Show the connection manager self.connectionmanager = ConnectionManager() # Setup RPC stats logging # daemon_bps: time, bytes_sent, bytes_recv self.daemon_bps = (0, 0, 0) self.rpc_stats = LoopingCall(self.log_rpc_stats) self.closing = False # Twisted catches signals to terminate, so have it call a pre_shutdown method. reactor.addSystemEventTrigger('before', 'gtkui_close', self.close) def gtkui_sigint_handler(num, frame): log.debug('SIGINT signal caught, firing event: gtkui_close') reactor.callLater(0, reactor.fireSystemEvent, 'gtkui_close') signal.signal(signal.SIGINT, gtkui_sigint_handler) def start(self): reactor.callWhenRunning(self._on_reactor_start) # Initialize gdk threading threads_enter() reactor.run() # Reactor no longer running so async callbacks (Deferreds) cannot be # processed after this point. threads_leave() def shutdown(self, *args, **kwargs): log.debug('GTKUI shutting down...') # Shutdown all components if client.is_standalone: return component.shutdown() @defer.inlineCallbacks def close(self): if self.closing: return self.closing = True # Make sure the config is saved. self.config.save() # Ensure columns state is saved self.torrentview.save_state() # Shut down components yield self.shutdown() # The gtk modal dialogs (e.g. Preferences) can prevent the application # quitting, so force exiting by destroying MainWindow. Must be done here # to avoid hanging when quitting with SIGINT (CTRL-C). self.mainwindow.window.destroy() reactor.stop() # Restart the application after closing if MainWindow restart attribute set. if component.get('MainWindow').restart: os.execv(sys.argv[0], sys.argv) def log_rpc_stats(self): """Log RPC statistics for thinclient mode.""" if not client.connected(): return t = time.time() recv = client.get_bytes_recv() sent = client.get_bytes_sent() delta_time = t - self.daemon_bps[0] delta_sent = sent - self.daemon_bps[1] delta_recv = recv - self.daemon_bps[2] self.daemon_bps = (t, sent, recv) sent_rate = fspeed(delta_sent / delta_time) recv_rate = fspeed(delta_recv / delta_time) log.debug( 'RPC: Sent %s (%s) Recv %s (%s)', fsize(sent), sent_rate, fsize(recv), recv_rate, ) def _on_reactor_start(self): log.debug('_on_reactor_start') self.mainwindow.first_show() if not self.config['standalone']: return self._start_thinclient() err_msg = '' try: client.start_standalone() except DaemonRunningError: err_msg = _( 'A Deluge daemon (deluged) is already running.\n' 'To use Standalone mode, stop local daemon and restart Deluge.' ) except ImportError as ex: if 'No module named libtorrent' in ex.message: err_msg = _( 'Only Thin Client mode is available because libtorrent is not installed.\n' 'To use Standalone mode, please install libtorrent package.' ) else: log.exception(ex) err_msg = _( 'Only Thin Client mode is available due to unknown Import Error.\n' 'To use Standalone mode, please see logs for error details.' ) except Exception as ex: log.exception(ex) err_msg = _( 'Only Thin Client mode is available due to unknown Import Error.\n' 'To use Standalone mode, please see logs for error details.' ) else: component.start() return def on_dialog_response(response): """User response to switching mode dialog.""" if response == RESPONSE_YES: # Turning off standalone self.config['standalone'] = False self._start_thinclient() else: # User want keep Standalone Mode so just quit. self.mainwindow.quit() # An error occurred so ask user to switch from Standalone to Thin Client mode. err_msg += '\n\n' + _('Continue in Thin Client mode?') d = YesNoDialog(_('Change User Interface Mode'), err_msg).run() d.addCallback(on_dialog_response) def _start_thinclient(self): """Start the gtkui in thinclient mode""" if log.isEnabledFor(logging.DEBUG): self.rpc_stats.start(10) # Check to see if we need to start the localhost daemon if self.config['autostart_localhost']: def on_localhost_status(status_info, port): if status_info[1] == 'Offline': log.debug('Autostarting localhost: %s', host_config[0:3]) self.connectionmanager.start_daemon(port, get_config_dir()) for host_config in self.connectionmanager.hostlist.config['hosts']: if host_config[1] in LOCALHOST: d = self.connectionmanager.hostlist.get_host_status(host_config[0]) d.addCallback(on_localhost_status, host_config[2]) break # Autoconnect to a host if self.config['autoconnect']: for host_config in self.connectionmanager.hostlist.config['hosts']: host_id, host, port, user, __ = host_config if host_id == self.config['autoconnect_host_id']: log.debug('Trying to connect to %s@%s:%s', user, host, port) self.connectionmanager._connect(host_id, try_counter=6) break if self.config['show_connection_manager_on_start']: # Dialog is blocking so call last. self.connectionmanager.show() def __on_disconnect(self): """ Called when disconnected from the daemon. We basically just stop all the components here. """ self.daemon_bps = (0, 0, 0) component.stop()
class ConnectionManager(component.Component): def __init__(self): component.Component.__init__(self, "ConnectionManager") self.gtkui_config = ConfigManager("gtkui.conf") self.config = ConfigManager("hostlist.conf.1.2", DEFAULT_CONFIG) self.running = False # Component overrides def start(self): pass def stop(self): # Close this dialog when we are shutting down if self.running: self.connection_manager.response(gtk.RESPONSE_CLOSE) def shutdown(self): pass # Public methods def show(self): """ Show the ConnectionManager dialog. """ # Get the glade file for the connection manager self.glade = gtk.glade.XML( pkg_resources.resource_filename("deluge.ui.gtkui", "glade/connection_manager.glade")) self.window = component.get("MainWindow") # Setup the ConnectionManager dialog self.connection_manager = self.glade.get_widget("connection_manager") self.connection_manager.set_transient_for(self.window.window) self.connection_manager.set_icon(common.get_deluge_icon()) self.glade.get_widget("image1").set_from_pixbuf(common.get_logo(32)) self.hostlist = self.glade.get_widget("hostlist") # Create status pixbufs if not HOSTLIST_PIXBUFS: for stock_id in (gtk.STOCK_NO, gtk.STOCK_YES, gtk.STOCK_CONNECT): HOSTLIST_PIXBUFS.append(self.connection_manager.render_icon(stock_id, gtk.ICON_SIZE_MENU)) # Create the host list gtkliststore # id-hash, hostname, port, status, username, password, version self.liststore = gtk.ListStore(str, str, int, str, str, str, str) # Setup host list treeview self.hostlist.set_model(self.liststore) render = gtk.CellRendererPixbuf() column = gtk.TreeViewColumn(_("Status"), render) column.set_cell_data_func(render, cell_render_status, 3) self.hostlist.append_column(column) render = gtk.CellRendererText() column = gtk.TreeViewColumn(_("Host"), render, text=HOSTLIST_COL_HOST) column.set_cell_data_func(render, cell_render_host, (1, 2, 4)) column.set_expand(True) self.hostlist.append_column(column) render = gtk.CellRendererText() column = gtk.TreeViewColumn(_("Version"), render, text=HOSTLIST_COL_VERSION) self.hostlist.append_column(column) # Load any saved host entries self.__load_hostlist() self.__load_options() # Select the first host if possible if len(self.liststore) > 0: self.hostlist.get_selection().select_path("0") # Connect the signals to the handlers self.glade.signal_autoconnect(self) self.hostlist.get_selection().connect("changed", self.on_hostlist_selection_changed) self.__update_list() self.running = True response = self.connection_manager.run() self.running = False # Save the toggle options self.__save_options() self.__save_hostlist() self.connection_manager.destroy() del self.glade del self.window del self.connection_manager del self.liststore del self.hostlist def add_host(self, host, port, username="", password=""): """ Adds a host to the list. :param host: str, the hostname :param port: int, the port :param username: str, the username to login as :param password: str, the password to login with """ # Check to see if there is already an entry for this host and return # if thats the case for entry in self.liststore: if [entry[HOSTLIST_COL_HOST], entry[HOSTLIST_COL_PORT], entry[HOSTLIST_COL_USER]] == [host, port, username]: raise Exception("Host already in list!") # Host isn't in the list, so lets add it row = self.liststore.append() import time import hashlib self.liststore[row][HOSTLIST_COL_ID] = hashlib.sha1(str(time.time())).hexdigest() self.liststore[row][HOSTLIST_COL_HOST] = host self.liststore[row][HOSTLIST_COL_PORT] = port self.liststore[row][HOSTLIST_COL_USER] = username self.liststore[row][HOSTLIST_COL_PASS] = password self.liststore[row][HOSTLIST_COL_STATUS] = "Offline" # Save the host list to file self.__save_hostlist() # Update the status of the hosts self.__update_list() # Private methods def __save_hostlist(self): """ Save the current hostlist to the config file. """ # Grab the hosts from the liststore self.config["hosts"] = [] for row in self.liststore: self.config["hosts"].append((row[HOSTLIST_COL_ID], row[HOSTLIST_COL_HOST], row[HOSTLIST_COL_PORT], row[HOSTLIST_COL_USER], row[HOSTLIST_COL_PASS])) self.config.save() def __load_hostlist(self): """ Load saved host entries """ for host in self.config["hosts"]: new_row = self.liststore.append() self.liststore[new_row][HOSTLIST_COL_ID] = host[0] self.liststore[new_row][HOSTLIST_COL_HOST] = host[1] self.liststore[new_row][HOSTLIST_COL_PORT] = host[2] self.liststore[new_row][HOSTLIST_COL_USER] = host[3] self.liststore[new_row][HOSTLIST_COL_PASS] = host[4] self.liststore[new_row][HOSTLIST_COL_STATUS] = "Offline" def __get_host_row(self, host_id): """ Returns the row in the liststore for `:param:host_id` or None """ for row in self.liststore: if host_id == row[HOSTLIST_COL_ID]: return row return None def __update_list(self): """Updates the host status""" if not hasattr(self, "liststore"): # This callback was probably fired after the window closed return def on_connect(result, c, host_id): # Return if the deferred callback was done after the dialog was closed if not self.running: return row = self.__get_host_row(host_id) def on_info(info, c): if not self.running: return if row: row[HOSTLIST_COL_STATUS] = "Online" row[HOSTLIST_COL_VERSION] = info self.__update_buttons() c.disconnect() def on_info_fail(reason, c): if not self.running: return if row: row[HOSTLIST_COL_STATUS] = "Offline" self.__update_buttons() c.disconnect() d = c.daemon.info() d.addCallback(on_info, c) d.addErrback(on_info_fail, c) def on_connect_failed(reason, host_id): if not self.running: return row = self.__get_host_row(host_id) if row: row[HOSTLIST_COL_STATUS] = "Offline" self.__update_buttons() for row in self.liststore: host_id = row[HOSTLIST_COL_ID] host = row[HOSTLIST_COL_HOST] port = row[HOSTLIST_COL_PORT] user = row[HOSTLIST_COL_USER] password = row[HOSTLIST_COL_PASS] if client.connected() and \ (host, port, "localclient" if not user and host in ("127.0.0.1", "localhost") else user) == client.connection_info(): def on_info(info): if not self.running: return row[HOSTLIST_COL_VERSION] = info self.__update_buttons() row[HOSTLIST_COL_STATUS] = "Connected" client.daemon.info().addCallback(on_info) continue # Create a new Client instance c = deluge.ui.client.Client() d = c.connect(host, port, user, password) d.addCallback(on_connect, c, host_id) d.addErrback(on_connect_failed, host_id) def __load_options(self): """ Set the widgets to show the correct options from the config. """ self.glade.get_widget("chk_autoconnect").set_active(self.gtkui_config["autoconnect"]) self.glade.get_widget("chk_autostart").set_active(self.gtkui_config["autostart_localhost"]) self.glade.get_widget("chk_donotshow").set_active(not self.gtkui_config["show_connection_manager_on_start"]) def __save_options(self): """ Set options in gtkui config from the toggle buttons. """ self.gtkui_config["autoconnect"] = self.glade.get_widget("chk_autoconnect").get_active() self.gtkui_config["autostart_localhost"] = self.glade.get_widget("chk_autostart").get_active() self.gtkui_config["show_connection_manager_on_start"] = not self.glade.get_widget("chk_donotshow").get_active() def __update_buttons(self): """ Updates the buttons states. """ if len(self.liststore) == 0: # There is nothing in the list self.glade.get_widget("button_startdaemon").set_sensitive(True) self.glade.get_widget("button_connect").set_sensitive(False) self.glade.get_widget("button_removehost").set_sensitive(False) self.glade.get_widget("image_startdaemon").set_from_stock( gtk.STOCK_EXECUTE, gtk.ICON_SIZE_MENU) self.glade.get_widget("label_startdaemon").set_text("_Start Daemon") model, row = self.hostlist.get_selection().get_selected() if not row: return # Get some values about the selected host status = model[row][HOSTLIST_COL_STATUS] host = model[row][HOSTLIST_COL_HOST] log.debug("Status: %s", status) # Check to see if we have a localhost entry selected localhost = False if host in ("127.0.0.1", "localhost"): localhost = True # Make sure buttons are sensitive at start self.glade.get_widget("button_startdaemon").set_sensitive(True) self.glade.get_widget("button_connect").set_sensitive(True) self.glade.get_widget("button_removehost").set_sensitive(True) # See if this is the currently connected host if status == "Connected": # Display a disconnect button if we're connected to this host self.glade.get_widget("button_connect").set_label("gtk-disconnect") self.glade.get_widget("button_removehost").set_sensitive(False) else: self.glade.get_widget("button_connect").set_label("gtk-connect") if status == "Offline" and not localhost: self.glade.get_widget("button_connect").set_sensitive(False) # Check to see if the host is online if status == "Connected" or status == "Online": self.glade.get_widget("image_startdaemon").set_from_stock( gtk.STOCK_STOP, gtk.ICON_SIZE_MENU) self.glade.get_widget("label_startdaemon").set_text( _("_Stop Daemon")) # Update the start daemon button if the selected host is localhost if localhost and status == "Offline": # The localhost is not online self.glade.get_widget("image_startdaemon").set_from_stock( gtk.STOCK_EXECUTE, gtk.ICON_SIZE_MENU) self.glade.get_widget("label_startdaemon").set_text( _("_Start Daemon")) if not localhost: # An offline host self.glade.get_widget("button_startdaemon").set_sensitive(False) # Make sure label is displayed correctly using mnemonics self.glade.get_widget("label_startdaemon").set_use_underline( True) def start_daemon(self, port, config): """ Attempts to start a daemon process and will show an ErrorDialog if unable to. """ try: return client.start_daemon(port, config) except OSError, e: from errno import ENOENT if e.errno == ENOENT: dialogs.ErrorDialog( _("Unable to start daemon!"), _("Deluge cannot find the 'deluged' executable, it is likely \ that you forgot to install the deluged package or it's not in your PATH.")).run() else: raise e except Exception, e: import traceback import sys tb = sys.exc_info() dialogs.ErrorDialog( _("Unable to start daemon!"), _("Please examine the details for more information."), details=traceback.format_exc(tb[2])).run()
class AllTorrents(BaseMode, component.Component): def __init__(self, stdscr, encoding=None): self.torrent_names = None self.numtorrents = -1 self._cached_rows = {} self.cursel = 1 self.curoff = 1 # TODO: this should really be 0 indexed self.column_string = "" self.popup = None self.messages = deque() self.marked = [] self.last_mark = -1 self._sorted_ids = None self._go_top = False self._curr_filter = None self.entering_search = False self.search_string = None self.search_state = SEARCH_EMPTY self.coreconfig = component.get("ConsoleUI").coreconfig self.legacy_mode = None self.__status_dict = {} self.__torrent_info_id = None BaseMode.__init__(self, stdscr, encoding) component.Component.__init__(self, "AllTorrents", 1, depend=["SessionProxy"]) curses.curs_set(0) self.stdscr.notimeout(0) self.update_config() component.start(["AllTorrents"]) self._info_fields = [ ("Name",None,("name",)), ("State", None, ("state",)), ("Down Speed", format_utils.format_speed, ("download_payload_rate",)), ("Up Speed", format_utils.format_speed, ("upload_payload_rate",)), ("Progress", format_utils.format_progress, ("progress",)), ("ETA", deluge.common.ftime, ("eta",)), ("Path", None, ("save_path",)), ("Downloaded",deluge.common.fsize,("all_time_download",)), ("Uploaded", deluge.common.fsize,("total_uploaded",)), ("Share Ratio", format_utils.format_float, ("ratio",)), ("Seeders",format_utils.format_seeds_peers,("num_seeds","total_seeds")), ("Peers",format_utils.format_seeds_peers,("num_peers","total_peers")), ("Active Time",deluge.common.ftime,("active_time",)), ("Seeding Time",deluge.common.ftime,("seeding_time",)), ("Date Added",deluge.common.fdate,("time_added",)), ("Availability", format_utils.format_float, ("distributed_copies",)), ("Pieces", format_utils.format_pieces, ("num_pieces","piece_length")), ] self.__status_keys = ["name","state","download_payload_rate","upload_payload_rate", "progress","eta","all_time_download","total_uploaded", "ratio", "num_seeds","total_seeds","num_peers","total_peers", "active_time", "seeding_time","time_added","distributed_copies", "num_pieces", "piece_length","save_path"] self.legacy_mode = Legacy(self.stdscr, self.encoding) if self.config["first_run"]: self.popup = Popup(self,"Welcome to Deluge" ,init_lines=self.__help_lines, height_req=0.75, width_req=65) self.config["first_run"] = False self.config.save() # component start/update def start(self): component.get("SessionProxy").get_torrents_status(self.__status_dict, self.__status_fields).addCallback(self.set_state,False) def update(self): component.get("SessionProxy").get_torrents_status(self.__status_dict, self.__status_fields).addCallback(self.set_state,True) if self.__torrent_info_id: component.get("SessionProxy").get_torrent_status(self.__torrent_info_id, self.__status_keys).addCallback(self._on_torrent_status) def update_config(self): self.config = ConfigManager("console.conf",DEFAULT_PREFS) s_primary = self.config["sort_primary"] s_secondary = self.config["sort_secondary"] self.__cols_to_show = [ pref for pref in column_pref_names if ("show_%s" % pref) not in self.config or self.config["show_%s"%pref] ] self.__columns = [prefs_to_names[col] for col in self.__cols_to_show] self.__status_fields = column.get_required_fields(self.__columns) # we always need these, even if we're not displaying them for rf in ["state", "name", "queue", "progress"]: if rf not in self.__status_fields: self.__status_fields.append(rf) # same with sort keys if s_primary and (s_primary not in self.__status_fields): self.__status_fields.append(s_primary) if s_secondary and (s_secondary not in self.__status_fields): self.__status_fields.append(s_secondary) self.__update_columns() def resume(self): component.start(["AllTorrents"]) self.refresh() def __update_columns(self): self.column_widths = [self.config["%s_width"%c] for c in self.__cols_to_show] req = sum(filter(lambda x:x >= 0,self.column_widths)) if (req > self.cols): # can't satisfy requests, just spread out evenly cw = int(self.cols/len(self.__columns)) for i in range(0,len(self.column_widths)): self.column_widths[i] = cw else: rem = self.cols - req var_cols = len(filter(lambda x: x < 0,self.column_widths)) if (var_cols > 0): vw = int(rem/var_cols) for i in range(0, len(self.column_widths)): if (self.column_widths[i] < 0): self.column_widths[i] = vw self.column_string = "{!header!}" try: primary_sort_col_name = prefs_to_names[self.config["sort_primary"]] except: primary_sort_col_name = "" for i, column in enumerate(self.__columns): ccol = column width = self.column_widths[i] #Trim the column if it's too long to fit if len(ccol) > width: ccol = ccol[:width - 1] # Padding ccol += " " * (width - len(ccol)) # Highlight the primary sort column if column == primary_sort_col_name: if i != len(self.__columns) - 1: ccol = "{!black,green,bold!}%s{!header!}" % ccol else: ccol = ("{!black,green,bold!}%s" % ccol)[:-1] self.column_string += ccol def set_state(self, state, refresh): self.curstate = state # cache in case we change sort order newnames = [] self._cached_rows = {} self._sorted_ids = self._sort_torrents(self.curstate) for torrent_id in self._sorted_ids: ts = self.curstate[torrent_id] newnames.append(ts["name"]) self.numtorrents = len(state) self.torrent_names = newnames if refresh: self.refresh() def get_torrent_name(self, torrent_id): for p,i in enumerate(self._sorted_ids): if torrent_id == i: return self.torrent_names[p] return None def _scroll_up(self, by): prevoff = self.curoff self.cursel = max(self.cursel - by,1) if ((self.cursel - 1) < self.curoff): self.curoff = max(self.cursel - 1,1) return prevoff != self.curoff def _scroll_down(self, by): prevoff = self.curoff self.cursel = min(self.cursel + by,self.numtorrents) if ((self.curoff + self.rows - 5) < self.cursel): self.curoff = self.cursel - self.rows + 5 return prevoff != self.curoff def current_torrent_id(self): if self._sorted_ids: return self._sorted_ids[self.cursel-1] else: return None def _selected_torrent_ids(self): ret = [] for i in self.marked: ret.append(self._sorted_ids[i-1]) return ret def _on_torrent_status(self, state): if (self.popup): self.popup.clear() name = state["name"] off = int((self.cols/4)-(len(name)/2)) self.popup.set_title(name) for i,f in enumerate(self._info_fields): if f[1] != None: args = [] try: for key in f[2]: args.append(state[key]) except: log.debug("Could not get info field: %s",e) continue info = f[1](*args) else: info = state[f[2][0]] nl = len(f[0])+4 if (nl+len(info))>self.popup.width: self.popup.add_line("{!info!}%s: {!input!}%s"%(f[0],info[:(self.popup.width - nl)])) info = info[(self.popup.width - nl):] n = self.popup.width-3 chunks = [info[i:i+n] for i in xrange(0, len(info), n)] for c in chunks: self.popup.add_line(" %s"%c) else: self.popup.add_line("{!info!}%s: {!input!}%s"%(f[0],info)) self.refresh() else: self.__torrent_info_id = None def on_resize(self, *args): BaseMode.on_resize_norefresh(self, *args) if self.popup: self.popup.handle_resize() self.update() self.__update_columns() self.refresh([]) def _queue_sort(self, v1, v2): if v1 == v2: return 0 if v2 < 0: return -1 if v1 < 0: return 1 if v1 > v2: return 1 if v2 > v1: return -1 def _sort_torrents(self, state): "sorts by primary and secondary sort fields" if not state: return {} s_primary = self.config["sort_primary"] s_secondary = self.config["sort_secondary"] result = state #Sort first by secondary sort field and then primary sort field # so it all works out cmp_func = self._queue_sort sg = state.get def sort_by_field(state, result, field): if field in column_names_to_state_keys: field = column_names_to_state_keys[field] reverse = field in reverse_sort_fields #Get first element so we can check if it has given field # and if it's a string first_element = state[state.keys()[0]] if field in first_element: is_string = isinstance( first_element[field], basestring) sort_key = lambda s:sg(s)[field] sort_key2 = lambda s:sg(s)[field].lower() #If it's a string, sort case-insensitively but preserve A>a order if is_string: result = sorted(result, cmp_func, sort_key, reverse) result = sorted(result, cmp_func, sort_key2, reverse) else: result = sorted(result, cmp_func, sort_key, reverse) if field == "eta": result = sorted(result, key=lambda s: state.get(s)["eta"] == 0) return result #Just in case primary and secondary fields are empty and/or # both are too ambiguous, also sort by queue position first if "queue" not in [s_secondary, s_primary]: result = sort_by_field(state, result, "queue") if s_secondary != s_primary: result = sort_by_field(state, result, s_secondary) result = sort_by_field(state, result, s_primary) if self.config["separate_complete"]: result = sorted(result, cmp_func, lambda s: state.get(s)["progress"] == 100.0) return result def _format_queue(self, qnum): if (qnum >= 0): return "%d"%(qnum+1) else: return "" def show_addtorrents_screen(self): def dodeets(arg): if arg and True in arg[0]: self.stdscr.erase() component.get("ConsoleUI").set_mode(AddTorrents(self,self.stdscr, self.config, self.encoding)) else: self.messages.append(("Error","An error occured trying to display add torrents screen")) component.stop(["AllTorrents"]).addCallback(dodeets) def show_torrent_details(self,tid): def dodeets(arg): if arg and True in arg[0]: self.stdscr.erase() component.get("ConsoleUI").set_mode(TorrentDetail(self,tid,self.stdscr, self.config, self.encoding)) else: self.messages.append(("Error","An error occured trying to display torrent details")) component.stop(["AllTorrents"]).addCallback(dodeets) def show_preferences(self): def _on_get_config(config): client.core.get_listen_port().addCallback(_on_get_listen_port,config) def _on_get_listen_port(port,config): client.core.get_cache_status().addCallback(_on_get_cache_status,port,config) def _on_get_cache_status(status,port,config): def doprefs(arg): if arg and True in arg[0]: self.stdscr.erase() component.get("ConsoleUI").set_mode(Preferences(self,config,self.config,port,status,self.stdscr,self.encoding)) else: self.messages.append(("Error","An error occured trying to display preferences")) component.stop(["AllTorrents"]).addCallback(doprefs) client.core.get_config().addCallback(_on_get_config) def __show_events(self): def doevents(arg): if arg and True in arg[0]: self.stdscr.erase() component.get("ConsoleUI").set_mode(EventView(self,self.stdscr,self.encoding)) else: self.messages.append(("Error","An error occured trying to display events")) component.stop(["AllTorrents"]).addCallback(doevents) def __legacy_mode(self): def dolegacy(arg): if arg and True in arg[0]: self.stdscr.erase() component.get("ConsoleUI").set_mode(self.legacy_mode) self.legacy_mode.refresh() curses.curs_set(2) else: self.messages.append(("Error","An error occured trying to switch to legacy mode")) component.stop(["AllTorrents"]).addCallback(dolegacy) def _torrent_filter(self, idx, data): if data==FILTER.ALL: self.__status_dict = {} self._curr_filter = None elif data==FILTER.ACTIVE: self.__status_dict = {"state":"Active"} self._curr_filter = "Active" elif data==FILTER.DOWNLOADING: self.__status_dict = {"state":"Downloading"} self._curr_filter = "Downloading" elif data==FILTER.SEEDING: self.__status_dict = {"state":"Seeding"} self._curr_filter = "Seeding" elif data==FILTER.PAUSED: self.__status_dict = {"state":"Paused"} self._curr_filter = "Paused" elif data==FILTER.CHECKING: self.__status_dict = {"state":"Checking"} self._curr_filter = "Checking" elif data==FILTER.ERROR: self.__status_dict = {"state":"Error"} self._curr_filter = "Error" elif data==FILTER.QUEUED: self.__status_dict = {"state":"Queued"} self._curr_filter = "Queued" self._go_top = True return True def _show_torrent_filter_popup(self): self.popup = SelectablePopup(self,"Filter Torrents", self._torrent_filter) self.popup.add_line("_All",data=FILTER.ALL) self.popup.add_line("Ac_tive",data=FILTER.ACTIVE) self.popup.add_line("_Downloading",data=FILTER.DOWNLOADING,foreground="green") self.popup.add_line("_Seeding",data=FILTER.SEEDING,foreground="cyan") self.popup.add_line("_Paused",data=FILTER.PAUSED) self.popup.add_line("_Error",data=FILTER.ERROR,foreground="red") self.popup.add_line("_Checking",data=FILTER.CHECKING,foreground="blue") self.popup.add_line("Q_ueued",data=FILTER.QUEUED,foreground="yellow") def _report_add_status(self, succ_cnt, fail_cnt, fail_msgs): if fail_cnt == 0: self.report_message("Torrents Added","{!success!}Successfully added %d torrent(s)"%succ_cnt) else: msg = ("{!error!}Failed to add the following %d torrent(s):\n {!input!}"%fail_cnt)+"\n ".join(fail_msgs) if succ_cnt != 0: msg += "\n \n{!success!}Successfully added %d torrent(s)"%succ_cnt self.report_message("Torrent Add Report",msg) def _show_torrent_add_popup(self): def do_add_from_url(result): def fail_cb(msg, url): log.debug("failed to add torrent: %s: %s" % (url, msg)) error_msg = "{!input!} * %s: {!error!}%s" % (url, msg) self._report_add_status(0, 1, [error_msg] ) def success_cb(tid, url): if tid: log.debug("added torrent: %s (%s)"%(url, tid)) self._report_add_status(1, 0, []) else: fail_cb("Already in session (probably)", url) url = result["url"] if not url: return t_options = { "download_location": result["path"], "add_paused": result["add_paused"] } if deluge.common.is_magnet(url): client.core.add_torrent_magnet(url, t_options).addCallback(success_cb, url).addErrback(fail_cb, url) elif deluge.common.is_url(url): client.core.add_torrent_url(url, t_options).addCallback(success_cb, url).addErrback(fail_cb, url) else: self.messages.append(("Error","{!error!}Invalid URL or magnet link: %s" % url)) return log.debug("Adding Torrent(s): %s (dl path: %s) (paused: %d)", url, result["path"], result["add_paused"]) def show_add_url_popup(): try: dl = self.coreconfig["download_location"] except KeyError: dl = "" ap = 1 try: if self.coreconfig["add_paused"]: ap = 0 except KeyError: pass self.popup = InputPopup(self,"Add Torrent (Esc to cancel)", close_cb=do_add_from_url) self.popup.add_text_input("Enter torrent URL or Magnet link:", "url") self.popup.add_text_input("Enter save path:", "path", dl) self.popup.add_select_input("Add Paused:", "add_paused", ["Yes", "No"], [True, False], ap) def option_chosen(index, data): self.popup = None if not data: return if data == 1: self.show_addtorrents_screen() elif data == 2: show_add_url_popup() self.popup = SelectablePopup(self,"Add torrent", option_chosen) self.popup.add_line("From _File(s)", data=1) self.popup.add_line("From _URL or Magnet", data=2) self.popup.add_line("_Cancel", data=0) def _do_set_column_visibility(self, data): for key, value in data.items(): self.config[key] = value self.config.save() self.update_config() self.__update_columns() self.refresh([]) def _show_visible_columns_popup(self): title = "Visible columns (Enter to exit)" self.popup = InputPopup(self, title, close_cb=self._do_set_column_visibility, immediate_action=True, height_req= len(column_pref_names) + 1, width_req= max([len(col) for col in column_pref_names + [title]]) + 8 ) for col in column_pref_names: name = prefs_to_names[col] prop = "show_%s" % col if prop not in self.config: continue state = self.config[prop] self.popup.add_checked_input(name, prop, state) def report_message(self,title,message): self.messages.append((title,message)) def clear_marks(self): self.marked = [] self.last_mark = -1 def set_popup(self,pu): self.popup = pu self.refresh() def refresh(self,lines=None): #log.error("ref") #import traceback #traceback.print_stack() # Something has requested we scroll to the top of the list if self._go_top: self.cursel = 1 self.curoff = 1 self._go_top = False # show a message popup if there's anything queued if self.popup == None and self.messages: title,msg = self.messages.popleft() self.popup = MessagePopup(self,title,msg, width_req=1.0) if not lines: if component.get("ConsoleUI").screen != self: return self.stdscr.erase() # Update the status bars if self._curr_filter == None: self.add_string(0,self.statusbars.topbar) else: self.add_string(0,"%s {!filterstatus!}Current filter: %s"%(self.statusbars.topbar,self._curr_filter)) self.add_string(1,self.column_string) if self.entering_search: string = { SEARCH_EMPTY: "{!black,white!}Search torrents: %s{!black,white!}", SEARCH_SUCCESS: "{!black,white!}Search torrents: {!black,green!}%s{!black,white!}", SEARCH_FAILING: "{!black,white!}Search torrents: {!black,red!}%s{!black,white!}", SEARCH_START_REACHED: "{!black,white!}Search torrents: {!black,yellow!}%s{!black,white!} (start reached)", SEARCH_END_REACHED: "{!black,white!}Search torrents: {!black,yellow!}%s{!black,white!} (end reached)" }[self.search_state] % self.search_string self.add_string(self.rows - 1, string) else: #This will quite likely fail when switching modes try: rf = format_utils.remove_formatting string = self.statusbars.bottombar hstr = "Press {!magenta,blue,bold!}[h]{!status!} for help" string += " " * ( self.cols - len(rf(string)) - len(rf(hstr))) + hstr self.add_string(self.rows - 1, string) except: pass # add all the torrents if self.numtorrents == 0: msg = "No torrents match filter".center(self.cols) self.add_string(3, "{!info!}%s"%msg) elif self.numtorrents > 0: tidx = self.curoff currow = 2 #Because dots are slow sorted_ids = self._sorted_ids curstate = self.curstate gcv = column.get_column_value fr = format_utils.format_row cols = self.__columns colw = self.column_widths cr = self._cached_rows def draw_row(index): if index not in cr: ts = curstate[sorted_ids[index]] cr[index] = (fr([gcv(name,ts) for name in cols],colw),ts["state"]) return cr[index] if lines: todraw = [] for l in lines: if l < tidx - 1: continue if l >= tidx - 1 + self.rows - 3: break if l >= self.numtorrents: break todraw.append(draw_row(l)) lines.reverse() else: todraw = [] for i in range(tidx-1, tidx-1 + self.rows - 3): if i >= self.numtorrents: break todraw += [draw_row(i)] for row in todraw: # default style fg = "white" bg = "black" attr = None if lines: tidx = lines.pop()+1 currow = tidx-self.curoff+2 if tidx in self.marked: bg = "blue" attr = "bold" if tidx == self.cursel: bg = "white" attr = "bold" if tidx in self.marked: fg = "blue" else: fg = "black" if row[1] == "Downloading": fg = "green" elif row[1] == "Seeding": fg = "cyan" elif row[1] == "Error": fg = "red" elif row[1] == "Queued": fg = "yellow" elif row[1] == "Checking": fg = "blue" if self.entering_search and len(self.search_string) > 1: lcase_name = self.torrent_names[tidx-1].lower() sstring_lower = self.search_string.lower() if lcase_name.find(sstring_lower) != -1: if tidx == self.cursel: pass elif tidx in self.marked: bg = "magenta" else: bg = "green" if fg == "green": fg = "black" attr = "bold" if attr: colorstr = "{!%s,%s,%s!}"%(fg,bg,attr) else: colorstr = "{!%s,%s!}"%(fg,bg) try: self.add_string(currow,"%s%s"%(colorstr,row[0]),trim=False) except: #Yeah, this should be fixed in some better way pass tidx += 1 currow += 1 if (currow > (self.rows - 2)): break else: self.add_string(1, "Waiting for torrents from core...") #self.stdscr.redrawwin() if self.entering_search: curses.curs_set(2) self.stdscr.move(self.rows-1, len(self.search_string)+17) else: curses.curs_set(0) if component.get("ConsoleUI").screen != self: return self.stdscr.noutrefresh() if self.popup: self.popup.refresh() curses.doupdate() def _mark_unmark(self,idx): if idx in self.marked: self.marked.remove(idx) self.last_mark = -1 else: self.marked.append(idx) self.last_mark = idx def __search_match_count(self): match_count = 0 search_string = self.search_string.lower() for n in self.torrent_names: n = n.lower() if n.find(search_string) != -1: match_count += 1 return match_count def __do_search(self, direction="first", skip=0): """ Performs a search on visible torrent and sets cursor to the match :param string: direction, the direction of search, can be first, last, next or previous :returns: Nothing """ if direction == "first": search_space = enumerate(self.torrent_names) elif direction == "last": search_space = enumerate(self.torrent_names) search_space = list(search_space) search_space = reversed(search_space) elif direction == "next": search_space = enumerate(self.torrent_names) search_space = list(search_space) search_space = search_space[self.cursel:] elif direction == "previous": search_space = enumerate(self.torrent_names) search_space = list(search_space)[:self.cursel-1] search_space = reversed(search_space) search_string = self.search_string.lower() for i,n in search_space: n = n.lower() if n.find(search_string) != -1: if skip > 0: skip -= 1 continue self.cursel = (i+1) if ((self.curoff + self.rows - 5) < self.cursel): self.curoff = self.cursel - self.rows + 5 elif ((self.curoff +1) > self.cursel): self.curoff = max(1, self.cursel - 1) self.search_state = SEARCH_SUCCESS return if direction in ["first", "last"]: self.search_state = SEARCH_FAILING elif direction == "next": self.search_state = SEARCH_END_REACHED elif direction == "previous": self.search_state = SEARCH_START_REACHED def __update_search(self, c): cname = self.torrent_names[self.cursel-1] if c == curses.KEY_BACKSPACE or c == 127: if self.search_string: self.search_string = self.search_string[:-1] if cname.lower().find(self.search_string.lower()) != -1: self.search_state = SEARCH_SUCCESS else: self.entering_search = False self.search_state = SEARCH_EMPTY self.refresh([]) elif c == curses.KEY_DC: self.search_string = "" self.search_state = SEARCH_SUCCESS self.refresh([]) elif c == curses.KEY_UP: self.__do_search("previous") self.refresh([]) elif c == curses.KEY_DOWN: self.__do_search("next") self.refresh([]) elif c == curses.KEY_LEFT: self.entering_search = False self.search_state = SEARCH_EMPTY self.refresh([]) elif c == ord('/'): self.entering_search = False self.search_state = SEARCH_EMPTY self.refresh([]) elif c == curses.KEY_RIGHT: tid = self.current_torrent_id() self.show_torrent_details(tid) elif c == curses.KEY_HOME: self.__do_search("first") self.refresh([]) elif c == curses.KEY_END: self.__do_search("last") self.refresh([]) elif c in [10, curses.KEY_ENTER]: self.last_mark = -1 tid = self.current_torrent_id() torrent_actions_popup(self, [tid] ,details=True) elif c == 27: self.search_string = "" self.search_state = SEARCH_EMPTY self.refresh([]) elif c > 31 and c < 256: old_search_string = self.search_string stroke = chr(c) uchar = "" while not uchar: try: uchar = stroke.decode(self.encoding) except UnicodeDecodeError: c = self.stdscr.getch() stroke += chr(c) if uchar: self.search_string += uchar still_matching = ( cname.lower().find(self.search_string.lower()) == cname.lower().find(old_search_string.lower()) and cname.lower().find(self.search_string.lower()) != -1 ) if self.search_string and not still_matching: self.__do_search() elif self.search_string: self.search_state = SEARCH_SUCCESS self.refresh([]) if not self.search_string: self.search_state = SEARCH_EMPTY self.refresh([]) def _doRead(self): # Read the character effected_lines = None c = self.stdscr.getch() if self.popup: if self.popup.handle_read(c): self.popup = None self.refresh() return if c > 31 and c < 256: if chr(c) == 'Q': from twisted.internet import reactor if client.connected(): def on_disconnect(result): reactor.stop() client.disconnect().addCallback(on_disconnect) else: reactor.stop() return if self.numtorrents < 0: return elif self.entering_search: self.__update_search(c) return if c == curses.KEY_UP: if self.cursel == 1: return if not self._scroll_up(1): effected_lines = [self.cursel-1,self.cursel] elif c == curses.KEY_PPAGE: self._scroll_up(int(self.rows/2)) elif c == curses.KEY_DOWN: if self.cursel >= self.numtorrents: return if not self._scroll_down(1): effected_lines = [self.cursel-2,self.cursel-1] elif c == curses.KEY_NPAGE: self._scroll_down(int(self.rows/2)) elif c == curses.KEY_HOME: self._scroll_up(self.cursel) elif c == curses.KEY_END: self._scroll_down(self.numtorrents-self.cursel) elif c == curses.KEY_DC: if self.cursel not in self.marked: self.marked.append(self.cursel) self.last_mark = self.cursel torrent_actions_popup(self,self._selected_torrent_ids(), action=ACTION.REMOVE) elif c == curses.KEY_RIGHT: # We enter a new mode for the selected torrent here tid = self.current_torrent_id() if tid: self.show_torrent_details(tid) return # Enter Key elif (c == curses.KEY_ENTER or c == 10) and self.numtorrents: if self.cursel not in self.marked: self.marked.append(self.cursel) self.last_mark = self.cursel torrent_actions_popup(self,self._selected_torrent_ids(),details=True) return else: if c > 31 and c < 256: if chr(c) == '/': self.search_string = "" self.entering_search = True elif chr(c) == 'n' and self.search_string: self.__do_search("next") elif chr(c) == 'j': if not self._scroll_up(1): effected_lines = [self.cursel-1,self.cursel] elif chr(c) == 'k': if not self._scroll_down(1): effected_lines = [self.cursel-2,self.cursel-1] elif chr(c) == 'i': cid = self.current_torrent_id() if cid: def cb(): self.__torrent_info_id = None self.popup = Popup(self,"Info",close_cb=cb, height_req=20) self.popup.add_line("Getting torrent info...") self.__torrent_info_id = cid elif chr(c) == 'm': self._mark_unmark(self.cursel) effected_lines = [self.cursel-1] elif chr(c) == 'M': if self.last_mark >= 0: if (self.cursel+1) > self.last_mark: mrange = range(self.last_mark,self.cursel+1) else: mrange = range(self.cursel-1,self.last_mark) self.marked.extend(mrange[1:]) effected_lines = mrange else: self._mark_unmark(self.cursel) effected_lines = [self.cursel-1] elif chr(c) == 'c': self.marked = [] self.last_mark = -1 elif chr(c) == 'a': self._show_torrent_add_popup() elif chr(c) == 'v': self._show_visible_columns_popup() elif chr(c) == 'o': if not self.marked: self.marked = [self.cursel] self.last_mark = self.cursel else: self.last_mark = -1 torrent_actions_popup(self, self._selected_torrent_ids(), action=ACTION.TORRENT_OPTIONS) elif chr(c) == '<': i = len(self.__cols_to_show) try: i = self.__cols_to_show.index(self.config["sort_primary"]) - 1 except: pass i = max(0, i) i = min(len(self.__cols_to_show) - 1, i) self.config["sort_primary"] = self.__cols_to_show[i] self.config.save() self.update_config() self.__update_columns() self.refresh([]) elif chr(c) == '>': i = 0 try: i = self.__cols_to_show.index(self.config["sort_primary"]) + 1 except: pass i = min(len(self.__cols_to_show) - 1, i) i = max(0, i) self.config["sort_primary"] = self.__cols_to_show[i] self.config.save() self.update_config() self.__update_columns() self.refresh([]) elif chr(c) == 'f': self._show_torrent_filter_popup() elif chr(c) == 'h': self.popup = MessagePopup(self, "Help", HELP_STR, width_req=0.75) elif chr(c) == 'p': self.show_preferences() return elif chr(c) == 'e': self.__show_events() return elif chr(c) == 'l': self.__legacy_mode() return self.refresh(effected_lines)