Ejemplo n.º 1
0
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()
Ejemplo n.º 2
0
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 ''
Ejemplo n.º 3
0
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
Ejemplo n.º 4
0
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 ""
Ejemplo n.º 5
0
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()
Ejemplo n.º 6
0
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()
Ejemplo n.º 7
0
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
Ejemplo n.º 8
0
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()
Ejemplo n.º 9
0
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
Ejemplo n.º 10
0
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()
Ejemplo n.º 11
0
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)
Ejemplo n.º 12
0
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
Ejemplo n.º 13
0
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()
Ejemplo n.º 14
0
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()
Ejemplo n.º 15
0
Archivo: core.py Proyecto: zluca/deluge
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)
Ejemplo n.º 16
0
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()
Ejemplo n.º 17
0
Archivo: core.py Proyecto: zluca/deluge
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()
Ejemplo n.º 18
0
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()
Ejemplo n.º 19
0
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()
Ejemplo n.º 20
0
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()
Ejemplo n.º 21
0
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)