diff --git a/deluge/ui/gtkui/details_tab.py b/deluge/ui/gtkui/details_tab.py index e863b51b7..f0b1fb099 100644 --- a/deluge/ui/gtkui/details_tab.py +++ b/deluge/ui/gtkui/details_tab.py @@ -81,7 +81,8 @@ class DetailsTab(Tab): status_keys = ["name", "total_size", "num_files", "tracker", "save_path", "message", "hash", "comment"] - client.core.get_torrent_status(selected, status_keys).addCallback(self._on_get_torrent_status) + session = component.get("SessionProxy") + session.get_torrent_status(selected, status_keys).addCallback(self._on_get_torrent_status) def _on_get_torrent_status(self, status): # Check to see if we got valid data from the core diff --git a/deluge/ui/gtkui/edittrackersdialog.py b/deluge/ui/gtkui/edittrackersdialog.py index f16e4bc1a..d710cc6c6 100644 --- a/deluge/ui/gtkui/edittrackersdialog.py +++ b/deluge/ui/gtkui/edittrackersdialog.py @@ -94,8 +94,8 @@ class EditTrackersDialog: return # Get the trackers for this torrent - - client.core.get_torrent_status(self.torrent_id, ["trackers"]).addCallback(self._on_get_torrent_status) + session = component.get("SessionProxy") + session.get_torrent_status(self.torrent_id, ["trackers"]).addCallback(self._on_get_torrent_status) client.force_call() def _on_get_torrent_status(self, status): diff --git a/deluge/ui/gtkui/files_tab.py b/deluge/ui/gtkui/files_tab.py index 2ab531337..f81820bad 100644 --- a/deluge/ui/gtkui/files_tab.py +++ b/deluge/ui/gtkui/files_tab.py @@ -315,7 +315,7 @@ class FilesTab(Tab): log.debug("Getting file list from core..") status_keys += ["files"] - client.core.get_torrent_status(self.torrent_id, status_keys).addCallback(self._on_get_torrent_status) + component.get("SessionProxy").get_torrent_status(self.torrent_id, status_keys).addCallback(self._on_get_torrent_status) def clear(self): self.treestore.clear() @@ -323,7 +323,7 @@ class FilesTab(Tab): def _on_row_activated(self, tree, path, view_column): if client.is_localhost: - client.core.get_torrent_status(self.torrent_id, ["save_path", "files"]).addCallback(self._on_open_file) + component.get("SessionProxy").get_torrent_status(self.torrent_id, ["save_path", "files"]).addCallback(self._on_open_file) def get_file_path(self, row, path=""): if not row: diff --git a/deluge/ui/gtkui/gtkui.py b/deluge/ui/gtkui/gtkui.py index 182ec8a02..1a0995d38 100644 --- a/deluge/ui/gtkui/gtkui.py +++ b/deluge/ui/gtkui/gtkui.py @@ -80,6 +80,7 @@ from ipcinterface import IPCInterface from deluge.ui.tracker_icons import TrackerIcons from queuedtorrents import QueuedTorrents from addtorrentdialog import AddTorrentDialog +from deluge.ui.sessionproxy import SessionProxy import dialogs import common @@ -207,6 +208,7 @@ class GtkUI(object): 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() @@ -229,7 +231,7 @@ class GtkUI(object): from twisted.internet.task import LoopingCall rpc_stats = LoopingCall(self.print_rpc_stats) rpc_stats.start(10) - + reactor.callWhenRunning(self._on_reactor_start) # Start the gtk main loop gtk.gdk.threads_enter() @@ -266,7 +268,7 @@ class GtkUI(object): sent = client.get_bytes_sent() except AttributeError: return - + log.debug("sent: %s recv: %s", deluge.common.fsize(sent), deluge.common.fsize(recv)) t = time.time() delta_time = t - self.daemon_bps[0] @@ -277,7 +279,7 @@ class GtkUI(object): recv_rate = deluge.common.fspeed(float(delta_recv) / float(delta_time)) log.debug("sent rate: %s recv rate: %s", sent_rate, recv_rate) self.daemon_bps = (t, sent, recv) - + def _on_reactor_start(self): log.debug("_on_reactor_start") self.mainwindow.first_show() diff --git a/deluge/ui/gtkui/menubar.py b/deluge/ui/gtkui/menubar.py index 565b6d211..1fe549afa 100644 --- a/deluge/ui/gtkui/menubar.py +++ b/deluge/ui/gtkui/menubar.py @@ -314,7 +314,7 @@ class MenuBar(component.Component): def _on_torrent_status(status): deluge.common.open_file(status["save_path"]) for torrent_id in component.get("TorrentView").get_selected_torrents(): - client.core.get_torrent_status(torrent_id, ["save_path"]).addCallback(_on_torrent_status) + component.get("SessionProxy").get_torrent_status(torrent_id, ["save_path"]).addCallback(_on_torrent_status) def on_menuitem_move_activate(self, data=None): log.debug("on_menuitem_move_activate") @@ -337,8 +337,7 @@ class MenuBar(component.Component): component.get("TorrentView").get_selected_torrents(), result) chooser.destroy() else: - client.core.get_torrent_status(component.get("TorrentView").get_selected_torrent(), ["save_path"]).addCallback(self.show_move_storage_dialog) - client.force_call(False) + component.get("SessionProxy").get_torrent_status(component.get("TorrentView").get_selected_torrent(), ["save_path"]).addCallback(self.show_move_storage_dialog) def show_move_storage_dialog(self, status): log.debug("show_move_storage_dialog") @@ -461,12 +460,11 @@ class MenuBar(component.Component): "separatormenuitem", "menuitem_connectionmanager" ] - + if value: attr = "hide" else: attr = "show" - + for item in items: getattr(self.window.main_glade.get_widget(item), attr)() - diff --git a/deluge/ui/gtkui/notification.py b/deluge/ui/gtkui/notification.py index 7d5d34723..e5af16861 100644 --- a/deluge/ui/gtkui/notification.py +++ b/deluge/ui/gtkui/notification.py @@ -53,7 +53,7 @@ class Notification: self.get_torrent_status(torrent_id) def get_torrent_status(self, torrent_id): - client.core.get_torrent_status(torrent_id, ["name", "num_files", "total_payload_download"]).addCallback(self._on_get_torrent_status) + component.get("SessionProxy").get_torrent_status(torrent_id, ["name", "num_files", "total_payload_download"]).addCallback(self._on_get_torrent_status) def _on_get_torrent_status(self, status): if status is None: diff --git a/deluge/ui/gtkui/options_tab.py b/deluge/ui/gtkui/options_tab.py index fb0cf3a91..6ea769171 100644 --- a/deluge/ui/gtkui/options_tab.py +++ b/deluge/ui/gtkui/options_tab.py @@ -98,7 +98,7 @@ class OptionsTab(Tab): if torrent_id != self.prev_torrent_id: self.prev_status = None - client.core.get_torrent_status(torrent_id, + component.get("SessionProxy").get_torrent_status(torrent_id, ["max_download_speed", "max_upload_speed", "max_connections", diff --git a/deluge/ui/gtkui/peers_tab.py b/deluge/ui/gtkui/peers_tab.py index debbecc54..9e4b92dda 100644 --- a/deluge/ui/gtkui/peers_tab.py +++ b/deluge/ui/gtkui/peers_tab.py @@ -257,7 +257,7 @@ class PeersTab(Tab): self.peers = {} self.torrent_id = torrent_id - client.core.get_torrent_status(torrent_id, ["peers"]).addCallback(self._on_get_torrent_status) + component.get("SessionProxy").get_torrent_status(torrent_id, ["peers"]).addCallback(self._on_get_torrent_status) def get_flag_pixbuf(self, country): if country == " ": diff --git a/deluge/ui/gtkui/status_tab.py b/deluge/ui/gtkui/status_tab.py index 5043a6eda..6feed8ed9 100644 --- a/deluge/ui/gtkui/status_tab.py +++ b/deluge/ui/gtkui/status_tab.py @@ -116,7 +116,7 @@ class StatusTab(Tab): "max_upload_speed", "max_download_speed", "active_time", "seeding_time", "seed_rank", "is_auto_managed", "time_added"] - client.core.get_torrent_status( + component.get("SessionProxy").get_torrent_status( selected, status_keys).addCallback(self._on_get_torrent_status) def _on_get_torrent_status(self, status): diff --git a/deluge/ui/gtkui/torrentview.py b/deluge/ui/gtkui/torrentview.py index 33fa1ba9b..be3f3d37b 100644 --- a/deluge/ui/gtkui/torrentview.py +++ b/deluge/ui/gtkui/torrentview.py @@ -164,7 +164,7 @@ def seed_peer_column_sort(model, iter1, iter2, data): class TorrentView(listview.ListView, component.Component): """TorrentView handles the listing of torrents.""" def __init__(self): - component.Component.__init__(self, "TorrentView", interval=2) + component.Component.__init__(self, "TorrentView", interval=2, depend=["SessionProxy"]) self.window = component.get("MainWindow") # Call the ListView constructor listview.ListView.__init__(self, @@ -256,10 +256,9 @@ class TorrentView(listview.ListView, component.Component): """Start the torrentview""" # We need to get the core session state to know which torrents are in # the session so we can add them to our list. - client.core.get_session_state().addCallback(self._on_session_state) + component.get("SessionProxy").get_torrents_status({}, []).addCallback(self._on_session_state) def _on_session_state(self, state): - log.debug("on_session_state: %s", state) self.treeview.freeze_child_notify() model = self.treeview.get_model() for torrent_id in state: @@ -268,7 +267,10 @@ class TorrentView(listview.ListView, component.Component): self.treeview.set_model(model) self.treeview.thaw_child_notify() self.got_state = True - self.update() + # Update the view right away with our status + self.status = state + self.set_columns_to_update() + self.update_view() def stop(self): """Stops the torrentview""" @@ -294,10 +296,8 @@ class TorrentView(listview.ListView, component.Component): self.filter = dict(filter_dict) #copied version of filter_dict. self.update() - def send_status_request(self, columns=None): - # Store the 'status_fields' we need to send to core + def set_columns_to_update(self, columns=None): status_keys = [] - # Store the actual columns we will be updating self.columns_to_update = [] if columns is None: @@ -317,6 +317,11 @@ class TorrentView(listview.ListView, component.Component): # Remove duplicate keys self.columns_to_update = list(set(self.columns_to_update)) + return status_keys + + def send_status_request(self, columns=None): + # Store the 'status_fields' we need to send to core + status_keys = self.set_columns_to_update(columns) # If there is nothing in status_keys then we must not continue if status_keys is []: @@ -327,8 +332,8 @@ class TorrentView(listview.ListView, component.Component): # Request the statuses for all these torrent_ids, this is async so we # will deal with the return in a signal callback. - client.core.get_torrents_status( - self.filter, status_keys, True).addCallback(self._on_get_torrents_status) + component.get("SessionProxy").get_torrents_status( + self.filter, status_keys).addCallback(self._on_get_torrents_status) def update(self): if self.got_state: diff --git a/deluge/ui/sessionproxy.py b/deluge/ui/sessionproxy.py new file mode 100644 index 000000000..c241c285e --- /dev/null +++ b/deluge/ui/sessionproxy.py @@ -0,0 +1,227 @@ +# +# sessionproxy.py +# +# Copyright (C) 2010 Andrew Resch +# +# Deluge is free software. +# +# You may redistribute it and/or modify it under the terms of the +# GNU General Public License, as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) +# any later version. +# +# deluge is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# +# + +from twisted.internet.defer import maybeDeferred, succeed + +import deluge.component as component +from deluge.ui.client import client +from deluge.log import LOG as log +import time + +class SessionProxy(component.Component): + """ + The SessionProxy component is used to cache session information client-side + to reduce the number of RPCs needed to provide a rich user interface. + + On start-up it will query the Core for a full status of all the torrents in + the session. After that point, it will query the Core for only changes in + the status of the torrents and will try to satisfy client requests from the + cache. + + """ + def __init__(self): + log.debug("SessionProxy init..") + component.Component.__init__(self, "SessionProxy", interval=5) + + # Set the cache time in seconds + # This is how long data will be valid before refetching from the core + self.cache_time = 1.5 + + # Hold the torrents' status.. {torrent_id: [time, {status_dict}], ...} + self.torrents = {} + + # Hold the time awhen the last state filter was used and the torrent_ids returned + # [(filter_dict, time, (torrent_id, ...)), ...] + self.state_filter_queries = [] + + client.register_event_handler("TorrentStateChangedEvent", self.on_torrent_state_changed) + client.register_event_handler("TorrentRemovedEvent", self.on_torrent_removed) + + def start(self): + def on_torrent_status(status): + # Save the time we got the torrent status + t = time.time() + for key, value in status.items(): + self.torrents[key] = [t, value] + + return client.core.get_torrents_status({}, [], True).addCallback(on_torrent_status) + + def stop(self): + self.torrents = {} + self.state_filter_queries = {} + + def update(self): + # Clean-up any stale filter queries + sfq = self.state_filter_queries + t = time.time() + self.state_filter_queries = [x for x in sfq if (t - x[1]) < self.cache_time] + + def create_status_dict(self, torrent_ids, keys): + """ + Creates a status dict from the cache. + + :param torrent_ids: the torrent_ids + :type torrent_ids: list of strings + :param keys: the status keys + :type keys: list of strings + + :returns: a dict with the status information for the *torrent_ids* + :rtype: dict + + """ + sd = {} + for torrent_id in torrent_ids: + sd[torrent_id] = dict([(x, y) for x, y in self.torrents[torrent_id][1].iteritems() if x in keys]) + return sd + + def get_torrent_status(self, torrent_id, keys): + """ + Get a status dict for one torrent. + + :param torrent_id: the torrent_id + :type torrent_id: string + :param keys: the status keys + :type keys: list of strings + + :returns: a dict of status information + :rtype: dict + + """ + if torrent_id in self.torrents: + if time.time() - self.torrents[torrent_id][0] < self.cache_time: + return succeed(self.create_status_dict([torrent_id], keys)[torrent_id]) + else: + d = client.core.get_torrent_status(torrent_id, keys, True) + def on_status(result, torrent_id): + self.torrents[torrent_id][0] = time.time() + self.torrents[torrent_id][1].update(result) + return self.torrents[torrent_id][1] + return d.addCallback(on_status, torrent_id) + else: + d = client.core.get_torrent_status(torrent_id, keys, True) + def on_status(result): + if result: + self.torrents[torrent_id] = (time.time(), result) + return result + return d.addCallback(on_status) + + def get_torrents_status(self, filter_dict, keys): + """ + Get a dict of torrent statuses. + + The filter can take 2 keys, *state* and *id*. The state filter can be + one of the torrent states or the special one *Active*. The *id* key is + simply a list of torrent_ids. + + :param filter_dict: the filter used for this query + :type filter_dict: dict + :param keys: the status keys + :type keys: list of strings + + :returns: a dict of torrent_ids and their status dicts + :rtype: dict + + """ + # Helper functions and callbacks --------------------------------------- + def on_status(result, torrent_ids, keys): + # Update the internal torrent status dict with the update values + t = time.time() + for key, value in result.items(): + self.torrents[key][0] = t + self.torrents[key][1].update(value) + + # Create the status dict + if not torrent_ids: + torrent_ids = self.torrents.keys() + + return self.create_status_dict(torrent_ids, keys) + + def find_torrents_to_fetch(torrent_ids): + to_fetch = [] + t = time.time() + for key in torrent_ids: + torrent = self.torrents[key] + if t - torrent[0] > self.cache_time: + to_fetch.append(key) + + return to_fetch + #----------------------------------------------------------------------- + + if not filter_dict: + # This means we want all the torrents status + # We get a list of any torrent_ids with expired status dicts + to_fetch = find_torrents_to_fetch(self.torrents.keys()) + if to_fetch: + d = client.core.get_torrents_status({"id": to_fetch}, keys, True) + return d.addCallback(on_status, [], keys) + + # Don't need to fetch anything + return maybeDeferred(self.create_status_dict, self.torrents.keys(), keys) + + if "state" in filter_dict: + # We check if a similar query has already been done within our cache time + # and return results for those torrent_ids + for sfq in self.state_filter_queries: + if sfq[0] == filter_dict and (time.time() - sfq[1]) < self.cache_time: + # We found a winner! + if "id" in filter_dict: + filter_fict["id"].extend(sfq[2]) + else: + filter_dict["id"] = sfq[2] + del filter_dict["state"] + break + + if "state" in filter_dict: + # If we got there, then there is no suitable cached filter query, so + # we'll just query the core + d = client.core.get_torrents_status(filer_dict, keys. True) + def on_filter_status(result, keys): + return on_status(result, result.keys(), keys) + return d.addCallback(on_filter_status, keys) + + # At this point we should have a filter with just "id" in it + to_fetch = find_torrents_to_fetch(filter_dict["id"]) + if to_fetch: + d = client.core.get_torrents_status({"id": to_fetch}, keys, True) + return d.addCallback(on_status, filter_dict["id"], keys) + else: + # Don't need to fetch anything, so just return data from the cache + return maybeDeferred(self.create_status_dict, filter_dict["id"], keys) + + def on_torrent_state_changed(self, torrent_id, state): + self.torrents[torrent_id][1]["state"] = state + + def on_torrent_removed(self, torrent_id): + del self.torrents[torrent_id]