diff --git a/deluge/plugins/scheduler/scheduler/__init__.py b/deluge/plugins/scheduler/scheduler/__init__.py new file mode 100644 index 000000000..f32865f92 --- /dev/null +++ b/deluge/plugins/scheduler/scheduler/__init__.py @@ -0,0 +1,57 @@ +# +# __init__.py +# +# Copyright (C) 2009 Andrew Resch +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken +# Copyright (C) 2007-2009 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 deluge.plugins.init import PluginInitBase + +class CorePlugin(PluginInitBase): + def __init__(self, plugin_name): + from core import Core as _plugin_cls + self._plugin_cls = _plugin_cls + super(CorePlugin, self).__init__(plugin_name) + +class GtkUIPlugin(PluginInitBase): + def __init__(self, plugin_name): + from gtkui import GtkUI as _plugin_cls + self._plugin_cls = _plugin_cls + super(GtkUIPlugin, self).__init__(plugin_name) + +class WebUIPlugin(PluginInitBase): + def __init__(self, plugin_name): + from webui import WebUI as _plugin_cls + self._plugin_cls = _plugin_cls + super(WebUIPlugin, self).__init__(plugin_name) diff --git a/deluge/plugins/scheduler/scheduler/common.py b/deluge/plugins/scheduler/scheduler/common.py new file mode 100644 index 000000000..b2b184b23 --- /dev/null +++ b/deluge/plugins/scheduler/scheduler/common.py @@ -0,0 +1,41 @@ +# +# common.py +# +# Copyright (C) 2009 Andrew Resch +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken +# Copyright (C) 2007-2009 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. +# + +def get_resource(filename): + import pkg_resources, os + return pkg_resources.resource_filename("scheduler", os.path.join("data", filename)) diff --git a/deluge/plugins/scheduler/scheduler/core.py b/deluge/plugins/scheduler/scheduler/core.py new file mode 100644 index 000000000..ad0f5fc25 --- /dev/null +++ b/deluge/plugins/scheduler/scheduler/core.py @@ -0,0 +1,159 @@ +# +# core.py +# +# Copyright (C) 2009 Andrew Resch +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken +# Copyright (C) 2007-2009 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. +# + +import time + +from deluge.log import LOG as log +from deluge.plugins.pluginbase import CorePluginBase +import deluge.component as component +import deluge.configmanager +from deluge.core.rpcserver import export +from deluge.event import DelugeEvent + +from twisted.internet import reactor + +DEFAULT_PREFS = { + "low_down": -1.0, + "low_up": -1.0, + "low_active": -1, + "button_state": [[1] * 7 for dummy in xrange(24)] +} + +STATES = { + 0: "Green", + 1: "Yellow", + 2: "Red" +} + +class SchedulerEvent(DelugeEvent): + """ + Emitted when a schedule state changes. + """ + def __init__(self, colour): + """ + :param colour: str, the current scheduler state + """ + self._args = [colour] + +class Core(CorePluginBase): + def enable(self): + # Create the defaults with the core config + core_config = component.get("Core").config + DEFAULT_PREFS["low_down"] = core_config["max_download_speed"] + DEFAULT_PREFS["low_up"] = core_config["max_upload_speed"] + DEFAULT_PREFS["low_active"] = core_config["max_active_limit"] + + self.config = deluge.configmanager.ConfigManager("scheduler.conf", DEFAULT_PREFS) + + self.state = self.get_state() + + # Apply the scheduling rules + self.do_schedule(False) + + # Schedule the next do_schedule() call for on the next hour + now = time.localtime(time.time()) + secs_to_next_hour = ((60 - now[4]) * 60) + (60 - now[5]) + self.timer = reactor.callLater(secs_to_next_hour, self.do_schedule) + + def disable(self): + try: + self.timer.cancel() + except: + pass + + def update(self): + pass + + + def do_schedule(self, timer=True): + """ + This is where we apply schedule rules. + """ + + state = self.get_state() + + if state == "Green": + # This is Green (Normal) so we just make sure we've applied the + # global defaults + core_config = deluge.configmanager.ConfigManager("core.conf") + core_config.apply_set_functions("max_download_speed") + core_config.apply_set_functions("max_upload_speed") + core_config.apply_set_functions("max_active_limit") + # Resume the session if necessary + component.get("Core").session.resume() + elif state == "Yellow": + # This is Yellow (Slow), so use the settings provided from the user + session = component.get("Core").session + session.set_download_rate_limit(int(self.config["low_down"] * 1024)) + session.set_upload_rate_limit(int(self.config["low_up"] * 1024)) + settings = session.settings() + settings.active_limit = self.config["low_active"] + session.set_settings(settings) + # Resume the session if necessary + component.get("Core").session.resume() + elif state == "Red": + # This is Red (Stop), so pause the libtorrent session + component.get("Core").session.pause() + + if state != self.state: + # The state has changed since last update so we need to emit an event + self.state = state + component.get("EventManager").emit(SchedulerEvent(self.state)) + + if timer: + # Call this again in 1 hour + self.timer = reactor.callLater(3600, self.do_schedule) + + @export() + def set_config(self, config): + "sets the config dictionary" + for key in config.keys(): + self.config[key] = config[key] + self.config.save() + self.do_schedule(False) + + @export() + def get_config(self): + "returns the config dictionary" + return self.config.config + + @export() + def get_state(self): + now = time.localtime(time.time()) + level = self.config["button_state"][now[3]][now[6]] + return STATES[level] diff --git a/deluge/plugins/scheduler/scheduler/data/green.png b/deluge/plugins/scheduler/scheduler/data/green.png new file mode 100644 index 000000000..3118c7430 Binary files /dev/null and b/deluge/plugins/scheduler/scheduler/data/green.png differ diff --git a/deluge/plugins/scheduler/scheduler/data/red.png b/deluge/plugins/scheduler/scheduler/data/red.png new file mode 100644 index 000000000..90f532609 Binary files /dev/null and b/deluge/plugins/scheduler/scheduler/data/red.png differ diff --git a/deluge/plugins/scheduler/scheduler/data/yellow.png b/deluge/plugins/scheduler/scheduler/data/yellow.png new file mode 100644 index 000000000..11146b996 Binary files /dev/null and b/deluge/plugins/scheduler/scheduler/data/yellow.png differ diff --git a/deluge/plugins/scheduler/scheduler/gtkui.py b/deluge/plugins/scheduler/scheduler/gtkui.py new file mode 100644 index 000000000..c0521853d --- /dev/null +++ b/deluge/plugins/scheduler/scheduler/gtkui.py @@ -0,0 +1,270 @@ +# +# gtkui.py +# +# Copyright (C) 2009 Andrew Resch +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken +# Copyright (C) 2007-2009 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. +# + +import gtk + +from deluge.log import LOG as log +from deluge.ui.client import client +from deluge.plugins.pluginbase import GtkPluginBase +import deluge.component as component +import deluge.common + +from common import get_resource + +DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] + +class SchedulerSelectWidget(gtk.DrawingArea): + def __init__(self, hover): + gtk.DrawingArea.__init__(self) + self.set_events(gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK | gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.LEAVE_NOTIFY_MASK) + + self.connect("expose_event", self.expose) + self.connect("button_press_event", self.mouse_down) + self.connect("button_release_event", self.mouse_up) + self.connect("motion_notify_event", self.mouse_hover) + self.connect("leave_notify_event", self.mouse_leave) + + self.colors = [[115.0/255, 210.0/255, 22.0/255], [237.0/255, 212.0/255, 0.0/255], [204.0/255, 0.0/255, 0.0/255]] + self.button_state = [[0] * 7 for dummy in xrange(24)] + + self.start_point = [0,0] + self.hover_point = [-1,-1] + self.hover_label = hover + self.hover_days = DAYS + self.mouse_press = False + self.set_size_request(350,150) + + def set_button_state(self, state): + self.button_state = [] + for s in state: + self.button_state.append(list(s)) + log.debug(self.button_state) + + #redraw the whole thing + def expose(self, widget, event): + self.context = self.window.cairo_create() + self.context.rectangle(event.area.x, event.area.y, event.area.width, event.area.height) + self.context.clip() + + width = self.window.get_size()[0] + height = self.window.get_size()[1] + + for y in xrange(7): + for x in xrange(24): + self.context.set_source_rgba(self.colors[self.button_state[x][y]][0], self.colors[self.button_state[x][y]][1], self.colors[self.button_state[x][y]][2], 0.7) + self.context.rectangle(width*(6*x/145.0+1/145.0), height*(6*y/43.0+1/43.0), 5*width/145.0, 5*height/43.0) + self.context.fill_preserve() + self.context.set_source_rgba(0.5, 0.5, 0.5, 0.5) + self.context.stroke() + + #coordinates --> which box + def get_point(self, event): + size = self.window.get_size() + x = int((event.x-size[0]*0.5/145.0)/(6*size[0]/145.0)) + y = int((event.y-size[1]*0.5/43.0)/(6*size[1]/43.0)) + + if x > 23: x = 23 + elif x < 0: x = 0 + if y > 6: y = 6 + elif y < 0: y = 0 + + return [x,y] + + #mouse down + def mouse_down(self, widget, event): + self.mouse_press = True + self.start_point = self.get_point(event) + + #if the same box -> change it + def mouse_up(self, widget, event): + self.mouse_press = False + end_point = self.get_point(event) + + #change color on mouseclick depending on the button + if end_point[0] is self.start_point[0] and end_point[1] is self.start_point[1]: + if event.button == 1: + self.button_state[end_point[0]][end_point[1]] += 1 + if self.button_state[end_point[0]][end_point[1]] > 2: + self.button_state[end_point[0]][end_point[1]] = 0 + elif event.button == 3: + self.button_state[end_point[0]][end_point[1]] -= 1 + if self.button_state[end_point[0]][end_point[1]] < 0: + self.button_state[end_point[0]][end_point[1]] = 2 + self.queue_draw() + + #if box changed and mouse is pressed draw all boxes from start point to end point + #set hover text etc.. + def mouse_hover(self, widget, event): + if self.get_point(event) != self.hover_point: + self.hover_point = self.get_point(event) + + self.hover_label.set_text(self.hover_days[self.hover_point[1]] + " " + str(self.hover_point[0]) + ":00 - " + str(self.hover_point[0]) + ":59") + + if self.mouse_press == True: + points = [[self.hover_point[0], self.start_point[0]], [self.hover_point[1], self.start_point[1]]] + + for x in xrange(min(points[0]), max(points[0])+1): + for y in xrange(min(points[1]), max(points[1])+1): + self.button_state[x][y] = self.button_state[self.start_point[0]][self.start_point[1]] + + self.queue_draw() + + #clear hover text on mouse leave + def mouse_leave(self, widget, event): + self.hover_label.set_text("") + self.hover_point = [-1,-1] + +class GtkUI(GtkPluginBase): + def enable(self): + self.create_prefs_page() + + 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.status_item = component.get("StatusBar").add_item( + image=get_resource("green.png"), + text="", + callback=self.on_status_item_clicked, + tooltip="Scheduler") + + def on_get_state(state): + self.status_item.set_image_from_file(get_resource(state.lower() + ".png")) + + self.state_deferred = client.scheduler.get_state().addCallback(on_get_state) + client.register_event_handler("SchedulerEvent", self.on_scheduler_event) + + def disable(self): + component.get("Preferences").remove_page("Scheduler") + component.get("PluginManager").deregister_hook("on_apply_prefs", self.on_apply_prefs) + component.get("PluginManager").deregister_hook("on_show_prefs", self.on_show_prefs) + + def on_apply_prefs(self): + log.debug("applying prefs for Scheduler") + config = {} + config["low_down"] = self.spin_download.get_value() + config["low_up"] = self.spin_upload.get_value() + config["low_active"] = self.spin_active.get_value_as_int() + config["button_state"] = self.scheduler_select.button_state + client.scheduler.set_config(config) + + def on_show_prefs(self): + def on_get_config(config): + log.debug("config: %s", config) + self.scheduler_select.set_button_state(config["button_state"]) + self.spin_download.set_value(config["low_down"]) + self.spin_upload.set_value(config["low_up"]) + self.spin_active.set_value(config["low_active"]) + + + client.scheduler.get_config().addCallback(on_get_config) + + def on_scheduler_event(self, state): + def on_state_deferred(s): + self.status_item.set_image_from_file(get_resource(state.lower() + ".png")) + + self.state_deferred.addCallback(on_state_deferred) + + def on_status_item_clicked(self, widget, event): + component.get("Preferences").show("Scheduler") + + #Configuration dialog + def create_prefs_page(self): + #Select Widget + hover = gtk.Label() + self.scheduler_select = SchedulerSelectWidget(hover) + + vbox = gtk.VBox(False, 5) + hbox = gtk.HBox(False, 5) + vbox_days = gtk.VBox() + for day in DAYS: + vbox_days.pack_start(gtk.Label(day)) + hbox.pack_start(vbox_days, False, False) + hbox.pack_start(self.scheduler_select, True, True) + frame = gtk.Frame() + label = gtk.Label() + label.set_markup("Schedule") + frame.set_label_widget(label) + frame.set_shadow_type(gtk.SHADOW_NONE) + frame.add(hbox) + + vbox.pack_start(frame, True, True) + vbox.pack_start(hover) + + table = gtk.Table(3, 2) + + label = gtk.Label(_("Download Limit:")) + label.set_alignment(0.0, 0.6) + table.attach(label, 0, 1, 0, 1, gtk.FILL) + self.spin_download = gtk.SpinButton() + self.spin_download.set_numeric(True) + self.spin_download.set_range(-1.0, 99999.0) + self.spin_download.set_increments(1, 10) + table.attach(self.spin_download, 1, 2, 0, 1, gtk.FILL) + + label = gtk.Label(_("Upload Limit:")) + label.set_alignment(0.0, 0.6) + table.attach(label, 0, 1, 1, 2, gtk.FILL) + self.spin_upload = gtk.SpinButton() + self.spin_upload.set_numeric(True) + self.spin_upload.set_range(-1.0, 99999.0) + self.spin_upload.set_increments(1, 10) + table.attach(self.spin_upload, 1, 2, 1, 2, gtk.FILL) + + label = gtk.Label(_("Active Torrents:")) + label.set_alignment(0.0, 0.6) + table.attach(label, 0, 1, 2, 3, gtk.FILL) + self.spin_active = gtk.SpinButton() + self.spin_active.set_numeric(True) + self.spin_active.set_range(-1, 9999) + self.spin_active.set_increments(1, 10) + table.attach(self.spin_active, 1, 2, 2, 3, gtk.FILL) + + eventbox = gtk.EventBox() + eventbox.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("#EDD400")) + eventbox.add(table) + frame = gtk.Frame() + label = gtk.Label() + label.set_markup(_("Slow Settings")) + frame.set_label_widget(label) + frame.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("#CDB400")) + frame.set_border_width(2) + frame.add(eventbox) + vbox.pack_start(frame, False, False) + + vbox.show_all() + component.get("Preferences").add_page("Scheduler", vbox) diff --git a/deluge/plugins/scheduler/scheduler/webui.py b/deluge/plugins/scheduler/scheduler/webui.py new file mode 100644 index 000000000..de1b5fdff --- /dev/null +++ b/deluge/plugins/scheduler/scheduler/webui.py @@ -0,0 +1,49 @@ +# +# webui.py +# +# Copyright (C) 2009 Andrew Resch +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken +# Copyright (C) 2007-2009 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 deluge.log import LOG as log +from deluge.ui.client import client +from deluge import component +from deluge.plugins.pluginbase import WebPluginBase + +class WebUI(WebPluginBase): + def enable(self): + pass + + def disable(self): + pass diff --git a/deluge/plugins/scheduler/setup.py b/deluge/plugins/scheduler/setup.py new file mode 100644 index 000000000..b5abee686 --- /dev/null +++ b/deluge/plugins/scheduler/setup.py @@ -0,0 +1,72 @@ +# +# setup.py +# +# Copyright (C) 2009 Andrew Resch +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken +# Copyright (C) 2007-2009 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 setuptools import setup + +__plugin_name__ = "Scheduler" +__author__ = "Andrew Resch" +__author_email__ = "andrewresch@gmail.com" +__version__ = "0.1" +__url__ = "http://deluge-torrent.org" +__license__ = "GPLv3" +__description__ = "Schedule limits on a per-hour per-day basis." +__long_description__ = """""" +__pkg_data__ = {__plugin_name__.lower(): ["template/*", "data/*"]} + +setup( + name=__plugin_name__, + version=__version__, + description=__description__, + author=__author__, + author_email=__author_email__, + url=__url__, + license=__license__, + long_description=__long_description__ if __long_description__ else __description__, + + packages=[__plugin_name__.lower()], + package_data = __pkg_data__, + + entry_points=""" + [deluge.plugin.core] + %s = %s:CorePlugin + [deluge.plugin.gtkui] + %s = %s:GtkUIPlugin + [deluge.plugin.webui] + %s = %s:WebUIPlugin + """ % ((__plugin_name__, __plugin_name__.lower())*3) +)