diff --git a/TODO b/TODO index c6faf383f..289f8dec3 100644 --- a/TODO +++ b/TODO @@ -19,4 +19,6 @@ * Add command line option to change config dir.. --config * Add method for plugins to add labels * Add context menus for labels.. ie. setting options for all torrents in label -* Create asynchronous batch torrent status method +* Implement caching in core +* Use the batch torrent status info as a cache for other torrent status requests +* Don't save fastresume files on exit for finished or paused torrents diff --git a/deluge/core/core.py b/deluge/core/core.py index 9cfbc24f9..22653a128 100644 --- a/deluge/core/core.py +++ b/deluge/core/core.py @@ -35,7 +35,7 @@ import gettext import locale import pkg_resources import sys -import pickle +import cPickle as pickle import shutil import os @@ -54,7 +54,7 @@ from deluge.core.pluginmanager import PluginManager from deluge.core.alertmanager import AlertManager from deluge.core.signalmanager import SignalManager from deluge.log import LOG as log - + DEFAULT_PREFS = { "config_location": deluge.common.get_config_dir(), "daemon_port": 58846, @@ -332,10 +332,8 @@ class Core( log.debug("Resuming torrent %s", torrent_id) if self.torrents.resume(torrent_id): self.torrent_resumed(torrent_id) - + def export_get_torrent_status(self, torrent_id, keys): - # Convert the array of strings to a python list of strings - keys = deluge.common.pythonize(keys) # Build the status dictionary try: status = self.torrents[torrent_id].get_status(keys) @@ -347,8 +345,33 @@ class Core( leftover_fields = list(set(keys) - set(status.keys())) if len(leftover_fields) > 0: status.update(self.plugins.get_status(torrent_id, leftover_fields)) - return xmlrpclib.Binary(pickle.dumps(status)) + return pickle.dumps(status) + def export_get_torrents_status(self, torrent_ids, keys): + """Returns dictionary of statuses for torrent_ids""" + # This is an async command, so we want to return right away + gobject.idle_add(self._get_torrents_status, torrent_ids, keys) + + def _get_torrents_status(self, torrent_ids, keys): + status_dict = {}.fromkeys(torrent_ids) + + # Get the torrent status for each torrent_id + for torrent_id in torrent_ids: + try: + status = self.torrents[torrent_id].get_status(keys) + except KeyError: + return None + # Get the leftover fields and ask the plugin manager to fill them + leftover_fields = list(set(keys) - set(status.keys())) + if len(leftover_fields) > 0: + status.update( + self.plugins.get_status(torrent_id, leftover_fields)) + + status_dict[torrent_id] = status + # Emit the torrent_status signal to the clients + self.torrent_status(pickle.dumps(status_dict)) + return False + def export_get_session_state(self): """Returns a list of torrent_ids in the session.""" # Get the torrent list from the TorrentManager @@ -449,6 +472,10 @@ class Core( """Emitted when all torrents have been resumed""" log.debug("torrent_all_resumed signal emitted") self.signals.emit("torrent_all_resumed", torrent_id) + + def torrent_status(self, status): + """Emitted when the torrent statuses are ready to be sent""" + self.signals.emit("torrent_status", status) # Config set functions def _on_set_torrentfiles_location(self, key, value): diff --git a/deluge/core/signalmanager.py b/deluge/core/signalmanager.py index 5ba3a0aa1..d47e01fad 100644 --- a/deluge/core/signalmanager.py +++ b/deluge/core/signalmanager.py @@ -32,6 +32,7 @@ # statement from all source files in the program, then also delete it here. import xmlrpclib +import socket import gobject @@ -67,6 +68,6 @@ class SignalManager(component.Component): def _emit(self, client, signal, data): try: client.emit_signal(signal, data) - except: - log.warning("Unable to emit signal to client %s", client) + except (socket.error, Exception), e: + log.warning("Unable to emit signal to client %s: %s", client, e) diff --git a/deluge/ui/client.py b/deluge/ui/client.py index bbbf4d272..48c076fa8 100644 --- a/deluge/ui/client.py +++ b/deluge/ui/client.py @@ -326,8 +326,19 @@ def get_torrent_status(torrent_id, keys): if status == None: return {} - return pickle.loads(status.data) + return pickle.loads(status) +def get_torrents_status(torrent_ids, keys): + """Builds a dictionary of torrent_ids status. Expects 2 lists. This is + asynchronous so the return value will be sent as the signal + 'torrent_status'""" + try: + get_core().get_torrents_status(torrent_ids, keys) + except (AttributeError, socket.error): + set_core_uri(None) + + return None + @cache def get_session_state(): try: diff --git a/deluge/ui/gtkui/signals.py b/deluge/ui/gtkui/signals.py index aaed607a3..33022d3c4 100644 --- a/deluge/ui/gtkui/signals.py +++ b/deluge/ui/gtkui/signals.py @@ -56,7 +56,9 @@ class Signals(component.Component): self.receiver.connect_to_signal("torrent_all_paused", self.torrent_all_paused) self.receiver.connect_to_signal("torrent_all_resumed", - self.torrent_all_resumed) + self.torrent_all_resumed) + self.receiver.connect_to_signal("torrent_status", + self.torrent_status) def stop(self): self.receiver.shutdown() @@ -94,3 +96,6 @@ class Signals(component.Component): log.debug("torrent_all_resumed signal received..") component.get("TorrentView").update() component.get("ToolBar").update_buttons() + + def torrent_status(self, status): + component.get("TorrentView").on_torrent_status_signal(status) diff --git a/deluge/ui/gtkui/torrentview.py b/deluge/ui/gtkui/torrentview.py index 77360b09a..81b48cfcb 100644 --- a/deluge/ui/gtkui/torrentview.py +++ b/deluge/ui/gtkui/torrentview.py @@ -38,6 +38,7 @@ pygtk.require('2.0') import gtk, gtk.glade import gettext import gobject +import cPickle as pickle import deluge.common import deluge.component as component @@ -119,6 +120,8 @@ class TorrentView(listview.ListView, component.Component): # Try to load the state file if available self.load_state("torrentview.state") + self.status_signal_received = True + # Register the columns menu with the listview so it gets updated # accordingly. self.register_checklist_menu( @@ -230,27 +233,18 @@ class TorrentView(listview.ListView, component.Component): model.set_value(row, filter_column, True) else: model.set_value(row, filter_column, False) - + self.liststore.foreach(foreachrow, self.filter) - if self.liststore != None: - self.liststore.foreach(self.update_row, columns) - - def update_row(self, model=None, path=None, row=None, columns=None): - """Updates the column values for 'row'. If columns is None it will - update all visible columns.""" - # Check to see if this row is visible and return if not - if not model.get_value(row, self.columns["filter"].column_indices[0]): + # We will only send another status request if we have received the + # previous. This is to prevent things from going out of sync. + if not self.status_signal_received: return - - # Get the torrent_id from the liststore - torrent_id = model.get_value(row, - self.columns["torrent_id"].column_indices[0]) - + # Store the 'status_fields' we need to send to core status_keys = [] # Store the actual columns we will be updating - columns_to_update = [] + self.columns_to_update = [] if columns is None: # We need to iterate through all columns @@ -265,10 +259,10 @@ class TorrentView(listview.ListView, component.Component): and self.columns[column].status_field is not None: for field in self.columns[column].status_field: status_keys.append(field) - columns_to_update.append(column) + self.columns_to_update.append(column) # Remove duplicate keys - columns_to_update = list(set(columns_to_update)) + self.columns_to_update = list(set(self.columns_to_update)) # If there is nothing in status_keys then we must not continue if status_keys is []: @@ -276,32 +270,63 @@ class TorrentView(listview.ListView, component.Component): # Remove duplicates from status_key list status_keys = list(set(status_keys)) - status = client.get_torrent_status(torrent_id, - status_keys) - - # Set values for each column in the row - for column in columns_to_update: - column_index = self.get_column_index(column) - if type(column_index) is not list: - # We only have a single list store column we need to update - try: - model.set_value(row, - column_index, - status[self.columns[column].status_field[0]]) - except (TypeError, KeyError), e: - log.warning("Unable to update column %s: %s", - column, e) - else: - # We have more than 1 liststore column to update - for index in column_index: - # Only update the column if the status field exists - try: - model.set_value(row, - index, - status[self.columns[column].status_field[ - column_index.index(index)]]) - except: - pass + + # Create list of torrent_ids in need of status updates + torrent_ids = [] + row = self.liststore.get_iter_first() + while row != None: + # Only add this torrent_id if it's not filtered + if self.liststore.get_value( + row, self.columns["filter"].column_indices[0]) == True: + torrent_ids.append(self.liststore.get_value( + row, self.columns["torrent_id"].column_indices[0])) + row = self.liststore.iter_next(row) + + if torrent_ids == []: + return + + # Request the statuses for all these torrent_ids, this is async so we + # will deal with the return in a signal callback. + self.status_signal_received = False + client.get_torrents_status(torrent_ids, status_keys) + + def on_torrent_status_signal(self, status): + """Callback function for get_torrents_status(). 'status' should be a + dictionary of {torrent_id: {key, value}}.""" + status = pickle.loads(status) + row = self.liststore.get_iter_first() + while row != None: + torrent_id = self.liststore.get_value( + row, self.columns["torrent_id"].column_indices[0]) + if torrent_id in status.keys(): + # Set values for each column in the row + for column in self.columns_to_update: + column_index = self.get_column_index(column) + if type(column_index) is not list: + # We only have a single list store column we need to + # update + try: + self.liststore.set_value(row, + column_index, + status[torrent_id][ + self.columns[column].status_field[0]]) + except (TypeError, KeyError), e: + log.warning("Unable to update column %s: %s", + column, e) + else: + # We have more than 1 liststore column to update + for index in column_index: + # Only update the column if the status field exists + try: + self.liststore.set_value(row, + index, + status[torrent_id][ + self.columns[column].status_field[ + column_index.index(index)]]) + except: + pass + row = self.liststore.iter_next(row) + self.status_signal_received = True def add_row(self, torrent_id): """Adds a new torrent row to the treeview""" @@ -312,8 +337,6 @@ class TorrentView(listview.ListView, component.Component): row, self.columns["torrent_id"].column_indices[0], torrent_id) - # Update the new row so - self.update_row(model=self.liststore, row=row) def remove_row(self, torrent_id): """Removes a row with torrent_id""" diff --git a/deluge/ui/signalreceiver.py b/deluge/ui/signalreceiver.py index faeacbcc2..60689d474 100644 --- a/deluge/ui/signalreceiver.py +++ b/deluge/ui/signalreceiver.py @@ -79,7 +79,6 @@ class SignalReceiver( self.register_function(self.emit_signal) # Register the signal receiver with the core - # FIXME: send actual URI not localhost core = client.get_core() core.register_client(str(port)) @@ -115,7 +114,6 @@ class SignalReceiver( def emit_signal(self, signal, data): """Exported method used by the core to emit a signal to the client""" - log.debug("Received signal %s with data %s from core..", signal, data) try: if data != None: for callback in self.signals[signal]: diff --git a/setup.py b/setup.py index c9a29caa4..51a3a913c 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ _extra_compile_args = [ "-O2" ] -removals = ["-g", "-p", "-Wstrict-prototypes"] +removals = ["-Wstrict-prototypes"] if python_version == '2.5': cv_opt = sysconfig.get_config_vars()["CFLAGS"]