From 16fbf27b905c1b320a9606587e4d37e14514d4f9 Mon Sep 17 00:00:00 2001 From: Calum Lind Date: Mon, 6 Jun 2011 11:52:08 +0100 Subject: Stats plugin update --- .../plugins/Stats/deluge/plugins/stats/common.py | 3 +- deluge/plugins/Stats/deluge/plugins/stats/core.py | 151 ++++++++--- .../Stats/deluge/plugins/stats/data/config.glade | 265 ++++++++++++++++++- .../Stats/deluge/plugins/stats/data/tabs.glade | 167 +++++++----- deluge/plugins/Stats/deluge/plugins/stats/graph.py | 281 ++++++++++++++------- deluge/plugins/Stats/deluge/plugins/stats/gtkui.py | 234 +++++++++++++---- .../Stats/deluge/plugins/stats/template/graph.html | 12 + deluge/plugins/Stats/deluge/plugins/stats/webui.py | 16 +- deluge/plugins/Stats/setup.py | 28 +- 9 files changed, 873 insertions(+), 284 deletions(-) create mode 100644 deluge/plugins/Stats/deluge/plugins/stats/template/graph.html diff --git a/deluge/plugins/Stats/deluge/plugins/stats/common.py b/deluge/plugins/Stats/deluge/plugins/stats/common.py index 674d6f4b3..c5fd57250 100644 --- a/deluge/plugins/Stats/deluge/plugins/stats/common.py +++ b/deluge/plugins/Stats/deluge/plugins/stats/common.py @@ -36,5 +36,4 @@ import pkg_resources import os.path def get_resource(filename): - return pkg_resources.resource_filename("deluge.plugins.stats", - os.path.join("data", filename)) + return pkg_resources.resource_filename("deluge.plugins.stats", os.path.join("data", filename)) diff --git a/deluge/plugins/Stats/deluge/plugins/stats/core.py b/deluge/plugins/Stats/deluge/plugins/stats/core.py index 8499d7de0..d1e977608 100644 --- a/deluge/plugins/Stats/deluge/plugins/stats/core.py +++ b/deluge/plugins/Stats/deluge/plugins/stats/core.py @@ -22,18 +22,7 @@ # 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. -# +# 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 @@ -44,21 +33,19 @@ # 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 -import logging from twisted.internet.task import LoopingCall import time import deluge +from deluge.log import LOG as log from deluge.plugins.pluginbase import CorePluginBase from deluge import component from deluge import configmanager from deluge.core.rpcserver import export -log = logging.getLogger(__name__) - DEFAULT_PREFS = { "test": "NiNiNi", - "update_interval": 2, #2 seconds. + "update_interval": 1, #2 seconds. "length": 150, # 2 seconds * 150 --> 5 minutes. } @@ -70,24 +57,55 @@ DEFAULT_TOTALS = { "stats": {} } +def get_key(config, key): + try: + return config[key] + except KeyError: + return None + +def mean(items): + try: + return sum(items)/ len(items) + except Exception: + return 0 + class Core(CorePluginBase): totals = {} #class var to catch only updating this once per session in enable. def enable(self): + log.debug("Stats plugin enabled") self.core = component.get("Core") self.stats ={} + self.count = {} + self.intervals = [1, 5, 30, 300] + + self.last_update = {} + t = time.time() + for i in self.intervals: + self.stats[i] = {} + self.last_update[i] = t + self.count[i] = 0 + self.config = configmanager.ConfigManager("stats.conf", DEFAULT_PREFS) self.saved_stats = configmanager.ConfigManager("stats.totals", DEFAULT_TOTALS) if self.totals == {}: self.totals.update(self.saved_stats.config) - self.stats = self.saved_stats["stats"] or {} + self.length = self.config["length"] + + #self.stats = get_key(self.saved_stats, "stats") or {} + self.stats_keys = [] + self.add_stats( + 'upload_rate', + 'download_rate', + 'num_connections', + 'dht_nodes', + 'dht_cache_nodes', + 'dht_torrents', + 'num_peers', + ) - self.stats_keys = [ - "payload_download_rate", - "payload_upload_rate" - ] self.update_stats() self.update_timer = LoopingCall(self.update_stats) @@ -104,40 +122,94 @@ class Core(CorePluginBase): except: pass + def add_stats(self, *stats): + for stat in stats: + if stat not in self.stats_keys: + self.stats_keys.append(stat) + for i in self.intervals: + if stat not in self.stats[i]: + self.stats[i][stat] = [] + def update_stats(self): try: - status = self.core.get_session_status(self.stats_keys) - for key, value in status.items(): - if key not in self.stats: - self.stats[key] = [] - self.stats[key].insert(0, value) - - for stat_list in self.stats.values(): - if len(stat_list) > self.config["length"]: + #Get all possible stats! + stats = {} + for key in self.stats_keys: + #try all keys we have, very inefficient but saves having to + #work out where a key comes from... + try: + stats.update(self.core.get_session_status([key])) + except AttributeError: + pass + stats["num_connections"] = self.core.get_num_connections() + stats.update(self.core.get_config_values(["max_download", + "max_upload", + "max_num_connections"])) + # status = self.core.session.status() + # for stat in dir(status): + # if not stat.startswith('_') and stat not in stats: + # stats[stat] = getattr(status, stat, None) + + update_time = time.time() + self.last_update[1] = update_time + + #extract the ones we are interested in + #adding them to the 1s array + for stat, stat_list in self.stats[1].iteritems(): + if stat in stats: + stat_list.insert(0, int(stats[stat])) + else: + stat_list.insert(0, 0) + if len(stat_list) > self.length: stat_list.pop() - self.last_update = time.time() + + def update_interval(interval, base, multiplier): + self.count[interval] = self.count[interval] + 1 + if self.count[interval] >= interval: + self.last_update[interval] = update_time + self.count[interval] = 0 + current_stats = self.stats[interval] + for stat, stat_list in self.stats[base].iteritems(): + try: + avg = mean(stat_list[0:multiplier]) + except ValueError: + avg = 0 + current_stats[stat].insert(0, avg) + if len(current_stats[stat]) > self.length: + current_stats[stat].pop() + + update_interval(5, 1, 5) + update_interval(30, 5, 6) + update_interval(300, 30, 10) except Exception, e: - log.exception(e) + log.error("Stats update error %s" % e) + return True def save_stats(self): try: self.saved_stats["stats"] = self.stats self.saved_stats.config.update(self.get_totals()) self.saved_stats.save() - except Exception,e: - log.exception(e) + except Exception, e: + log.error("Stats save error", e) return True # export: @export - def get_stats(self, keys): + def get_stats(self, keys, interval): + if interval not in self.intervals: + return None + stats_dict = {} for key in keys: - if key in self.stats: - stats_dict[key] = self.stats[key] - stats_dict["_last_update"] = self.last_update + if key in self.stats[interval]: + stats_dict[key] = self.stats[interval][key] + + stats_dict["_last_update"] = self.last_update[interval] + stats_dict["_length"] = self.config["length"] + stats_dict["_update_interval"] = interval return stats_dict @export @@ -169,3 +241,8 @@ class Core(CorePluginBase): def get_config(self): "returns the config dictionary" return self.config.config + + @export + def get_intervals(self): + "Returns the available resolutions" + return self.intervals diff --git a/deluge/plugins/Stats/deluge/plugins/stats/data/config.glade b/deluge/plugins/Stats/deluge/plugins/stats/data/config.glade index e39b5204b..858acd767 100644 --- a/deluge/plugins/Stats/deluge/plugins/stats/data/config.glade +++ b/deluge/plugins/Stats/deluge/plugins/stats/data/config.glade @@ -1,24 +1,263 @@ - - - + + + - + True + vertical - + True - Test config value: - - - - - True - True + 0 + none + + + True + 15 + + + True + 10 + 2 + 15 + + + True + True + True + #000000000000 + + + 1 + 2 + 1 + 2 + GTK_EXPAND + + + + + True + 0 + Download color: + + + 1 + 2 + + + + + True + 0 + Upload color: + + + 2 + 3 + + + + + True + True + True + #000000000000 + + + 1 + 2 + 2 + 3 + GTK_EXPAND + + + + + True + 0 + <b>Connections Graph</b> + True + + + 2 + 3 + 4 + + + + + True + 0 + <b>Bandwidth Graph</b> + True + + + 2 + + + + + True + True + True + #000000000000 + + + 1 + 2 + 4 + 5 + GTK_EXPAND + + + + + True + 0 + DHT nodes: + + + 4 + 5 + + + + + True + True + True + #000000000000 + + + 1 + 2 + 5 + 6 + GTK_EXPAND + + + + + True + 0 + Cached DHT nodes: + + + 5 + 6 + + + + + True + 0 + DHT torrents: + + + 6 + 7 + + + + + True + 0 + Connections: + + + 7 + 8 + + + + + True + True + True + #000000000000 + + + 1 + 2 + 6 + 7 + GTK_EXPAND + + + + + True + True + True + #000000000000 + + + 1 + 2 + 7 + 8 + GTK_EXPAND + + + + + True + 0 + <b>Seeds / Peers</b> + True + + + 2 + 8 + 9 + + + + + True + True + True + #000000000000 + + + 1 + 2 + 9 + 10 + GTK_EXPAND + + + + + True + 0 + Peers: + + + 9 + 10 + + + + + + + + + True + <b>Graph Colors</b> + True + + + label_item + + - 1 + 0 diff --git a/deluge/plugins/Stats/deluge/plugins/stats/data/tabs.glade b/deluge/plugins/Stats/deluge/plugins/stats/data/tabs.glade index c3616a6a5..8732caeaf 100644 --- a/deluge/plugins/Stats/deluge/plugins/stats/data/tabs.glade +++ b/deluge/plugins/Stats/deluge/plugins/stats/data/tabs.glade @@ -1,7 +1,7 @@ - - - + + + @@ -14,6 +14,9 @@ True gtk-page-setup + + 0 + @@ -25,70 +28,120 @@ + + 0 + True True - GTK_POLICY_AUTOMATIC - GTK_POLICY_AUTOMATIC + automatic + automatic - + True - True - GTK_POS_LEFT - - - True - - - - - True - Bandwidth - - - tab - False - - - - - True - - - 1 - - - - - True - Connections - - - tab - 1 - False - - - - - True - - - 2 - - + queue + none - + True - Seeds/Peers + vertical + + + True + + + True + Resolution + + + False + False + 0 + + + + + True + + + False + False + 1 + + + + + False + False + 0 + + + + + True + True + left + + + True + + + + + True + Bandwidth + + + False + tab + + + + + True + + + 1 + + + + + True + Connections + + + 1 + False + tab + + + + + True + + + 2 + + + + + True + Seeds/Peers + + + 2 + False + tab + + + + + 1 + + - - tab - 2 - False - diff --git a/deluge/plugins/Stats/deluge/plugins/stats/graph.py b/deluge/plugins/Stats/deluge/plugins/stats/graph.py index 7f6a0548e..b5a265501 100644 --- a/deluge/plugins/Stats/deluge/plugins/stats/graph.py +++ b/deluge/plugins/Stats/deluge/plugins/stats/graph.py @@ -1,6 +1,7 @@ # # graph.py # +# Copyright (C) 2009 Ian Martin # Copyright (C) 2008 Damien Churchill # Copyright (C) 2008 Martijn Voncken # Copyright (C) Marcos Pinto 2007 @@ -21,7 +22,7 @@ # along with deluge. If not, write to: # The Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor -# Boston, MA 02110-1301, USA. +# 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 @@ -32,23 +33,14 @@ # 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. -# -# -# 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 """ port of old plugin by markybob. """ import time +import math import cairo -import logging +from deluge.log import LOG as log from deluge.ui.client import client black = (0, 0, 0) @@ -60,13 +52,18 @@ green = (0, 1.0, 0) blue = (0, 0, 1.0) orange = (1.0, 0.74, 0) -log = logging.getLogger(__name__) - def default_formatter(value): return str(value) +def size_formatter_scale(value): + scale = 1.0 + for i in range(0,3): + scale = scale * 1024.0 + if value / scale < 1024: + return scale + def change_opacity(color, opactiy): - """A method to assist in changing the opacity of a color in order to draw the + """A method to assist in changing the opactiy of a color inorder to draw the fills. """ color = list(color) @@ -83,6 +80,7 @@ class Graph: self.length = 150 self.stat_info = {} self.line_size = 2 + self.dash_length = [10] self.mean_selected = True self.legend_selected = True self.max_selected = True @@ -105,125 +103,198 @@ class Graph: def set_stats(self, stats): self.last_update = stats["_last_update"] - log.debug("Last update: %s" % self.last_update) del stats["_last_update"] + self.length = stats["_length"] + del stats["_length"] + self.interval = stats["_update_interval"] + del stats["_update_interval"] self.stats = stats + return + + # def set_config(self, config): + # self.length = config["length"] + # self.interval = config["update_interval"] - def set_config(self, config): - self.length = config["length"] - self.interval = config["update_interval"] + def set_interval(self, interval): + self.interval = interval def draw_to_context(self, context, width, height): self.ctx = context self.width, self.height = width, height - try: - self.draw_rect(white, 0, 0, self.width, self.height) - self.draw_x_axis() - self.draw_left_axis() - - if self.legend_selected: - self.draw_legend() - except cairo.Error, e: - log.exception(e) + self.draw_rect(white, 0, 0, self.width, self.height) + self.draw_graph() return self.ctx def draw(self, width, height): - self.width = width - self.height = height - - self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.width, self.height) - self.ctx = cairo.Context(self.surface) - self.draw_rect(white, 0, 0, self.width, self.height) - self.draw_x_axis() - self.draw_left_axis() + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + ctx = cairo.Context(surface) + self.draw_to_context(ctx, width, height) + return surface - if self.legend_selected: - self.draw_legend() - return self.surface - def draw_x_axis(self): - duration = float(self.length * self.interval) + def draw_x_axis(self, bounds): + (left, top, right, bottom) = bounds + duration = self.length * self.interval start = self.last_update - duration - ratio = (self.width - 40) / duration - seconds_to_minute = 60 - time.localtime(start)[5] + ratio = (right - left) / float(duration) + + if duration < 1800 * 10: + #try rounding to nearest 1min, 5mins, 10mins, 30mins + for step in [60, 300, 600, 1800]: + if duration / step < 10: + x_step = step + break + else: + # if there wasnt anything useful find a nice fitting hourly divisor + x_step = ((duration / 5) /3600 )* 3600 - for i in xrange(0, 5): - text = time.strftime('%H:%M', time.localtime(start + seconds_to_minute + (60*i))) - x = int(ratio * (seconds_to_minute + (60*i))) - self.draw_text(text, x + 46, self.height - 20) - x = x + 59.5 - self.draw_dotted_line(gray, x, 20, x, self.height - 20) + #this doesnt allow for dst and timezones... + seconds_to_step = math.ceil(start/float(x_step)) * x_step - start - y = self.height - 22.5 - self.draw_dotted_line(gray, 60, y, int(self.width), y) + for i in xrange(0, duration/x_step + 1): + text = time.strftime('%H:%M', time.localtime(start + seconds_to_step + i*x_step)) + # + 0.5 to allign x to nearest pixel + x = int(ratio * (seconds_to_step + i*x_step) + left) + 0.5 + self.draw_x_text(text, x, bottom) + self.draw_dotted_line(gray, x, top-0.5, x, bottom+0.5) - def draw_left_axis(self): + self.draw_line(gray, left, bottom+0.5, right, bottom+0.5) + + def draw_graph(self): + font_extents = self.ctx.font_extents() + x_axis_space = font_extents[2] + 2 + self.line_size / 2.0 + plot_height = self.height - x_axis_space + #lets say we need 2n-1*font height pixels to plot the y ticks + tick_limit = (plot_height / font_extents[3] )# / 2.0 + + max_value = 0 + for stat in self.stat_info: + if self.stat_info[stat]['axis'] == 'left': + try: + l_max = max(self.stats[stat]) + except ValueError: + l_max = 0 + if l_max > max_value: + max_value = l_max + if max_value < self.left_axis['min']: + max_value = self.left_axis['min'] + + y_ticks = self.intervalise(max_value, tick_limit) + max_value = y_ticks[-1] + #find the width of the y_ticks + y_tick_text = [self.left_axis['formatter'](tick) for tick in y_ticks] + def space_required(text): + te = self.ctx.text_extents(text) + return math.ceil(te[4] - te[0]) + y_tick_width = max((space_required(text) for text in y_tick_text)) + + top = font_extents[2] / 2.0 + #bounds(left, top, right, bottom) + bounds = (y_tick_width + 4, top + 2, self.width, self.height - x_axis_space) + + self.draw_x_axis(bounds) + self.draw_left_axis(bounds, y_ticks, y_tick_text) + + def intervalise(self, x, limit=None): + """Given a value x create an array of tick points to got with the graph + The number of ticks returned can be constrained by limit, minimum of 3 + """ + #Limit is the number of ticks which is 1 + the number of steps as we + #count the 0 tick in limit + if limit is not None: + if limit <3: + limit = 2 + else: + limit = limit -1 + scale = 1 + if 'formatter_scale' in self.left_axis: + scale = self.left_axis['formatter_scale'](x) + x = x / float(scale) + + #Find the largest power of 10 less than x + log = math.log10(x) + intbit = math.floor(log) + + interval = math.pow(10, intbit) + steps = int(math.ceil(x / interval)) + + if steps <= 1 and (limit is None or limit >= 10*steps): + interval = interval * 0.1 + steps = steps * 10 + elif steps <= 2 and (limit is None or limit >= 5*steps): + interval = interval * 0.2 + steps = steps * 5 + elif steps <=5 and (limit is None or limit >= 2*steps): + interval = interval * 0.5 + steps = steps * 2 + + if limit is not None and steps > limit: + multi = steps / float(limit) + if multi > 2: + interval = interval * 5 + else: + interval = interval * 2 + + intervals = [i * interval * scale for i in xrange(1+int(math.ceil(x/ interval)))] + return intervals + + def draw_left_axis(self, bounds, y_ticks, y_tick_text): + (left, top, right, bottom) = bounds stats = {} - max_values = [] for stat in self.stat_info: if self.stat_info[stat]['axis'] == 'left': stats[stat] = self.stat_info[stat] stats[stat]['values'] = self.stats[stat] stats[stat]['fill_color'] = change_opacity(stats[stat]['color'], 0.5) stats[stat]['color'] = change_opacity(stats[stat]['color'], 0.8) - stats[stat]['max_value'] = max(self.stats[stat]) - max_values.append(stats[stat]['max_value']) - if len(max_values) > 1: - max_value = max(*max_values) - else: - max_value = max_values[0] - if max_value < self.left_axis['min']: - max_value = self.left_axis['min'] - - height = self.height - self.line_size - 22 - #max_value = float(round(max_value, len(str(max_value)) * -1)) - max_value = float(max_value) + height = bottom - top + max_value = y_ticks[-1] ratio = height / max_value - for i in xrange(1, 6): - y = int(ratio * ((max_value / 5) * i)) - 0.5 - if i < 5: - self.draw_dotted_line(gray, 60, y, self.width, y) - text = self.left_axis['formatter']((max_value / 5) * (5 - i)) - self.draw_text(text, 0, y - 6) - self.draw_dotted_line(gray, 60.5, 20, 60.5, self.height - 20) + for i, y_val in enumerate(y_ticks): + y = int(bottom - y_val * ratio ) - 0.5 + if i != 0: + self.draw_dotted_line(gray, left, y, right, y) + self.draw_y_text(y_tick_text[i], left, y) + self.draw_line(gray, left, top, left, bottom) for stat, info in stats.iteritems(): - self.draw_value_poly(info['values'], info['color'], max_value) - self.draw_value_poly(info['values'], info['fill_color'], max_value, info['fill']) + if len(info['values']) > 0: + self.draw_value_poly(info['values'], info['color'], max_value, bounds) + self.draw_value_poly(info['values'], info['fill_color'], max_value, bounds, info['fill']) def draw_legend(self): pass - def trace_path(self, values, max_value): - height = self.height - 24 - width = self.width + + def trace_path(self, values, max_value, bounds): + (left, top, right, bottom) = bounds + ratio = (bottom - top) / max_value line_width = self.line_size self.ctx.set_line_width(line_width) - self.ctx.move_to(width, height) + self.ctx.move_to(right, bottom) - self.ctx.line_to(width, - int(height - ((height - 28) * values[0] / max_value))) + self.ctx.line_to(right, int(bottom - values[0] * ratio )) - x = width - step = (width - 60) / float(self.length) + x = right + step = (right - left) / float(self.length -1) for i, value in enumerate(values): if i == self.length - 1: - x = 62 - self.ctx.line_to(x, - int(height - 1 - ((height - 28) * value / max_value)) - ) + x = left + + self.ctx.line_to(x, int(bottom - value * ratio)) x -= step self.ctx.line_to( - int(width + 62 - (((len(values) - 1) * width) / (self.length - 1))), - height) + int(right - (len(values) - 1) * step), + bottom) self.ctx.close_path() - def draw_value_poly(self, values, color, max_value, fill=False): - self.trace_path(values, max_value) + + def draw_value_poly(self, values, color, max_value, bounds, fill=False): + self.trace_path(values, max_value, bounds) self.ctx.set_source_rgba(*color) if fill: @@ -231,9 +302,26 @@ class Graph: else: self.ctx.stroke() - def draw_text(self, text, x, y): - self.ctx.set_font_size(9) - self.ctx.move_to(x, y + 9) + def draw_x_text(self, text, x, y): + """Draws text below and horizontally centered about x,y""" + fe = self.ctx.font_extents() + te = self.ctx.text_extents(text) + height = fe[2] + x_bearing = te[0] + width = te[2] + self.ctx.move_to(int(x - width/2.0 + x_bearing), int(y + height)) + self.ctx.set_source_rgba(*self.black) + self.ctx.show_text(text) + + def draw_y_text(self, text, x, y): + """Draws text left of and vertically centered about x,y""" + fe = self.ctx.font_extents() + te = self.ctx.text_extents(text) + descent = fe[1] + ascent = fe[0] + x_bearing = te[0] + width = te[4] + self.ctx.move_to(int(x - width - x_bearing - 2), int(y + (ascent - descent)/2.0)) self.ctx.set_source_rgba(*self.black) self.ctx.show_text(text) @@ -252,13 +340,12 @@ class Graph: def draw_dotted_line(self, color, x1, y1, x2, y2): self.ctx.set_source_rgba(*color) self.ctx.set_line_width(1) + dash, offset = self.ctx.get_dash() + self.ctx.set_dash(self.dash_length, 0) self.ctx.move_to(x1, y1) self.ctx.line_to(x2, y2) - #self.ctx.stroke_preserve() - #self.ctx.set_source_rgba(*white) - #self.ctx.set_dash((1, 1), 4) self.ctx.stroke() - #self.ctx.set_dash((1, 1), 0) + self.ctx.set_dash(dash, offset) if __name__ == "__main__": import test diff --git a/deluge/plugins/Stats/deluge/plugins/stats/gtkui.py b/deluge/plugins/Stats/deluge/plugins/stats/gtkui.py index d3bec7353..607fc406f 100644 --- a/deluge/plugins/Stats/deluge/plugins/stats/gtkui.py +++ b/deluge/plugins/Stats/deluge/plugins/stats/gtkui.py @@ -1,6 +1,7 @@ # # gtkui.py # +# Copyright (C) 2009 Ian Martin # Copyright (C) 2008 Martijn Voncken # # Basic plugin template created by: @@ -23,18 +24,7 @@ # 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. -# +# 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 @@ -45,89 +35,211 @@ # 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 -import os import gtk -import logging +import gobject from gtk.glade import XML -from twisted.internet import defer - -# Relative imports -from . import common -from . import graph - +import graph +import deluge from deluge import component +from deluge.log import LOG as log from deluge.common import fspeed from deluge.ui.client import client from deluge.ui.gtkui.torrentdetails import Tab from deluge.plugins.pluginbase import GtkPluginBase -log = logging.getLogger(__name__) +import common + +DEFAULT_CONF = { 'version': 1, + 'colors' :{ + 'bandwidth_graph': {'upload_rate': str(gtk.gdk.Color("blue")), + 'download_rate': str(gtk.gdk.Color("green")), + }, + 'connections_graph': { 'dht_nodes': str(gtk.gdk.Color("orange")), + 'dht_cache_nodes': str(gtk.gdk.Color("blue")), + 'dht_torrents': str(gtk.gdk.Color("green")), + 'num_connections': str(gtk.gdk.Color("darkred")), + }, + 'seeds_graph': { 'num_peers': str(gtk.gdk.Color("blue")), + }, + } + } + +def neat_time(column, cell, model, iter): + """Render seconds as seconds or minutes with label""" + seconds = model.get_value(iter, 0) + if seconds >60: + text = "%d %s" % (seconds / 60, _("minutes")) + elif seconds == 60: + text = _("1 minute") + elif seconds == 1: + text = _("1 second") + else: + text = "%d %s" % (seconds, _("seconds")) + cell.set_property('text', text) + return + +def int_str(number): + return (str(int(number))) + +def gtk_to_graph_color(color): + """Turns a gtk.gdk.Color into a tuple with range 0-1 as used by the graph""" + MAX = float(65535) + gtk_color = gtk.gdk.Color(color) + red = gtk_color.red / MAX + green = gtk_color.green / MAX + blue = gtk_color.blue / MAX + return (red, green, blue) + class GraphsTab(Tab): - def __init__(self, glade): + def __init__(self, glade, colors): Tab.__init__(self) - self._name = 'Graphs' self.glade = glade self.window = self.glade.get_widget('graph_tab') - self._child_widget = self.window self.notebook = self.glade.get_widget('graph_notebook') self.label = self.glade.get_widget('graph_label') + + self._name = 'Graphs' + self._child_widget = self.window self._tab_label = self.label + + self.colors = colors + self.bandwidth_graph = self.glade.get_widget('bandwidth_graph') - self.bandwidth_graph.connect('expose_event', self.expose) + self.bandwidth_graph.connect('expose_event', self.graph_expose) + + self.connections_graph = self.glade.get_widget('connections_graph') + self.connections_graph.connect('expose_event', self.graph_expose) + + self.seeds_graph = self.glade.get_widget('seeds_graph') + self.seeds_graph.connect('expose_event', self.graph_expose) + + self.notebook.connect('switch-page', self._on_notebook_switch_page) + + self.selected_interval = 1 #should come from config or similar + self.select_bandwidth_graph() + self.window.unparent() self.label.unparent() - self.graph_widget = self.bandwidth_graph - self.graph = graph.Graph() - self.graph.add_stat('payload_download_rate', label='Download Rate', color=graph.green) - self.graph.add_stat('payload_upload_rate', label='Upload Rate', color=graph.blue) - self.graph.set_left_axis(formatter=fspeed, min=10240) + self.intervals = None + self.intervals_combo = self.glade.get_widget('combo_intervals') + cell = gtk.CellRendererText() + self.intervals_combo.pack_start(cell, True) + self.intervals_combo.set_cell_data_func(cell, neat_time) + self.intervals_combo.connect("changed", self._on_selected_interval_changed) + self.update_intervals() + - def expose(self, widget, event): - """Redraw""" + def graph_expose(self, widget, event): context = self.graph_widget.window.cairo_create() # set a clip region context.rectangle(event.area.x, event.area.y, event.area.width, event.area.height) context.clip() - - width, height = self.graph_widget.allocation.width, self.graph_widget.allocation.height - self.graph.draw_to_context(context, width, height) + self.graph.draw_to_context(context, + self.graph_widget.allocation.width, + self.graph_widget.allocation.height) #Do not propagate the event return False def update(self): - log.debug("getstat keys: %s", self.graph.stat_info.keys()) - d1 = client.stats.get_stats(self.graph.stat_info.keys()) + d1 = client.stats.get_stats(self.graph.stat_info.keys(), self.selected_interval) d1.addCallback(self.graph.set_stats) - d2 = client.stats.get_config() - d2.addCallback(self.graph.set_config) - dl = defer.DeferredList([d1, d2]) - - def _on_update(result): - width, height = self.graph_widget.allocation.width, self.graph_widget.allocation.height - rect = gtk.gdk.Rectangle(0, 0, width, height) - self.graph_widget.window.invalidate_rect(rect, True) - - dl.addCallback(_on_update) + def _update_complete(result): + self.graph_widget.queue_draw() + d1.addCallback(_update_complete) + return True def clear(self): pass + def update_intervals(self): + client.stats.get_intervals().addCallback(self._on_intervals_changed) + def select_bandwidth_graph(self): + log.debug("Selecting bandwidth graph") + self.graph_widget = self.bandwidth_graph + self.graph = graph.Graph() + colors = self.colors['bandwidth_graph'] + self.graph.add_stat('download_rate', label='Download Rate', + color=gtk_to_graph_color(colors['download_rate'])) + self.graph.add_stat('upload_rate', label='Upload Rate', + color=gtk_to_graph_color(colors['upload_rate'])) + self.graph.set_left_axis(formatter=fspeed, min=10240, + formatter_scale=graph.size_formatter_scale) + + def select_connections_graph(self): + log.debug("Selecting connections graph") + self.graph_widget = self.connections_graph + g = graph.Graph() + self.graph = g + colors = self.colors['connections_graph'] + g.add_stat('dht_nodes', color=gtk_to_graph_color(colors['dht_nodes'])) + g.add_stat('dht_cache_nodes', color=gtk_to_graph_color(colors['dht_cache_nodes'])) + g.add_stat('dht_torrents', color=gtk_to_graph_color(colors['dht_torrents'])) + g.add_stat('num_connections', color=gtk_to_graph_color(colors['num_connections'])) + g.set_left_axis(formatter=int_str, min=10) + + def select_seeds_graph(self): + log.debug("Selecting connections graph") + self.graph_widget = self.seeds_graph + self.graph = graph.Graph() + colors = self.colors['seeds_graph'] + self.graph.add_stat('num_peers', color=gtk_to_graph_color(colors['num_peers'])) + self.graph.set_left_axis(formatter=int_str, min=10) + + def set_colors(self, colors): + self.colors = colors + # Fake switch page to update the graph colors (HACKY) + self._on_notebook_switch_page(self.notebook, + None, #This is unused + self.notebook.get_current_page()) + + def _on_intervals_changed(self, intervals): + liststore = gtk.ListStore(int) + for inter in intervals: + liststore.append([inter]) + self.intervals_combo.set_model(liststore) + try: + current = intervals.index(self.selected_interval) + except: + current = 0 + #should select the value saved in config + self.intervals_combo.set_active(current) + + def _on_selected_interval_changed(self, combobox): + model = combobox.get_model() + iter = combobox.get_active_iter() + self.selected_interval = model.get_value(iter, 0) + self.update() + return True + + def _on_notebook_switch_page(self, notebook, page, page_num): + p = notebook.get_nth_page(page_num) + if p is self.bandwidth_graph: + self.select_bandwidth_graph() + self.update() + elif p is self.connections_graph: + self.select_connections_graph() + self.update() + elif p is self.seeds_graph: + self.select_seeds_graph() + self.update() + return True class GtkUI(GtkPluginBase): def enable(self): log.debug("Stats plugin enable called") + self.config = deluge.configmanager.ConfigManager("stats.gtkui.conf", DEFAULT_CONF) self.glade = XML(common.get_resource("config.glade")) component.get("Preferences").add_page("Stats", self.glade.get_widget("prefs_box")) component.get("PluginManager").register_hook("on_apply_prefs", self.on_apply_prefs) component.get("PluginManager").register_hook("on_show_prefs", self.on_show_prefs) self.on_show_prefs() - self.graphs_tab = GraphsTab(XML(common.get_resource("tabs.glade"))) + self.graphs_tab = GraphsTab(XML(common.get_resource("tabs.glade")), self.config['colors']) self.torrent_details = component.get('TorrentDetails') self.torrent_details.add_tab(self.graphs_tab) @@ -139,15 +251,31 @@ class GtkUI(GtkPluginBase): def on_apply_prefs(self): log.debug("applying prefs for Stats") - config = { - "test":self.glade.get_widget("txt_test").get_text() - } + gtkconf = {} + for graph, colors in self.config['colors'].items(): + gtkconf[graph] = {} + for value, color in colors.items(): + try: + color_btn = self.glade.get_widget("%s_%s_color" % (graph, value)) + gtkconf[graph][value] = str(color_btn.get_color()) + except: + gtkconf[graph][value] = DEFAULT_CONF['colors'][graph][value] + self.config['colors'] = gtkconf + self.graphs_tab.set_colors(self.config['colors']) + + config = { } client.stats.set_config(config) def on_show_prefs(self): + for graph, colors in self.config['colors'].items(): + for value, color in colors.items(): + try: + color_btn = self.glade.get_widget("%s_%s_color" % (graph, value)) + color_btn.set_color(gtk.gdk.Color(color)) + except: + log.debug("Unable to set %s %s %s" % (graph, value, color)) client.stats.get_config().addCallback(self.cb_get_config) def cb_get_config(self, config): "callback for on show_prefs" - self.glade.get_widget("txt_test").set_text(config["test"]) - + pass diff --git a/deluge/plugins/Stats/deluge/plugins/stats/template/graph.html b/deluge/plugins/Stats/deluge/plugins/stats/template/graph.html new file mode 100644 index 000000000..2ff803bcb --- /dev/null +++ b/deluge/plugins/Stats/deluge/plugins/stats/template/graph.html @@ -0,0 +1,12 @@ +$:render.header(_("Network Graph"), 'graph') +$:render.admin_toolbar('graph') + +
+ +
+
+
+ + + +$:render.footer() diff --git a/deluge/plugins/Stats/deluge/plugins/stats/webui.py b/deluge/plugins/Stats/deluge/plugins/stats/webui.py index e48d4d654..6ec261655 100644 --- a/deluge/plugins/Stats/deluge/plugins/stats/webui.py +++ b/deluge/plugins/Stats/deluge/plugins/stats/webui.py @@ -1,7 +1,11 @@ # # webui.py # -# Copyright (C) 2009 Damien Churchill +# Copyright (C) 2008 Martijn Voncken +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken +# Copyright (C) 2007, 2008 Andrew Resch # # Deluge is free software. # @@ -33,20 +37,18 @@ # # -import logging +from deluge.log import LOG as log from deluge.ui.client import client from deluge import component from deluge.plugins.pluginbase import WebPluginBase from common import get_resource -log = logging.getLogger(__name__) - class WebUI(WebPluginBase): - + scripts = [get_resource("stats.js")] - - # The enable and disable methods are not strictly required on the WebUI + + # The enable and disable methods are not scrictly required on the WebUI # plugins. They are only here if you need to register images/stylesheets # with the webserver. def enable(self): diff --git a/deluge/plugins/Stats/setup.py b/deluge/plugins/Stats/setup.py index f84f76f8e..c5cf13ce5 100644 --- a/deluge/plugins/Stats/setup.py +++ b/deluge/plugins/Stats/setup.py @@ -1,6 +1,6 @@ # # setup.py -# +# Copyright (C) 2009 Ian Martin # Copyright (C) 2008 Martijn Voncken # # Basic plugin template created by: @@ -23,18 +23,7 @@ # 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. -# +# 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 @@ -48,13 +37,16 @@ from setuptools import setup, find_packages __plugin_name__ = "Stats" -__author__ = "Martijn Voncken" -__author_email__ = "mvoncken@gmail.com" -__version__ = "0.1" +__author__ = "Ian Martin" +__author_email__ = "ianmartin@cantab.net" +__version__ = "0.3.2" __url__ = "http://deluge-torrent.org" __license__ = "GPLv3" -__description__ = "" -__long_description__ = """""" +__description__ = "Display stats graphs" +__long_description__ = """ +Records lots of extra stats +and produces time series +graphs""" __pkg_data__ = {"deluge.plugins."+__plugin_name__.lower(): ["template/*", "data/*"]} setup( -- cgit