diff options
Diffstat (limited to 'deluge/plugins/Execute/deluge_execute')
-rw-r--r-- | deluge/plugins/Execute/deluge_execute/__init__.py | 36 | ||||
-rw-r--r-- | deluge/plugins/Execute/deluge_execute/common.py | 23 | ||||
-rw-r--r-- | deluge/plugins/Execute/deluge_execute/core.py | 185 | ||||
-rw-r--r-- | deluge/plugins/Execute/deluge_execute/data/execute.js | 312 | ||||
-rw-r--r-- | deluge/plugins/Execute/deluge_execute/data/execute_prefs.ui | 197 | ||||
-rw-r--r-- | deluge/plugins/Execute/deluge_execute/data/execute_prefs.ui~ | 190 | ||||
-rw-r--r-- | deluge/plugins/Execute/deluge_execute/gtkui.py | 165 | ||||
-rw-r--r-- | deluge/plugins/Execute/deluge_execute/webui.py | 24 |
8 files changed, 1132 insertions, 0 deletions
diff --git a/deluge/plugins/Execute/deluge_execute/__init__.py b/deluge/plugins/Execute/deluge_execute/__init__.py new file mode 100644 index 0000000..c6d55f4 --- /dev/null +++ b/deluge/plugins/Execute/deluge_execute/__init__.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com> +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +from __future__ import unicode_literals + +from deluge.plugins.init import PluginInitBase + + +class CorePlugin(PluginInitBase): + def __init__(self, plugin_name): + from .core import Core as _pluginCls + + self._plugin_cls = _pluginCls + super(CorePlugin, self).__init__(plugin_name) + + +class GtkUIPlugin(PluginInitBase): + def __init__(self, plugin_name): + from .gtkui import GtkUI as _pluginCls + + self._plugin_cls = _pluginCls + super(GtkUIPlugin, self).__init__(plugin_name) + + +class WebUIPlugin(PluginInitBase): + def __init__(self, plugin_name): + from .webui import WebUI as _pluginCls + + self._plugin_cls = _pluginCls + super(WebUIPlugin, self).__init__(plugin_name) diff --git a/deluge/plugins/Execute/deluge_execute/common.py b/deluge/plugins/Execute/deluge_execute/common.py new file mode 100644 index 0000000..4c9db09 --- /dev/null +++ b/deluge/plugins/Execute/deluge_execute/common.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> +# 2007-2009 Andrew Resch <andrewresch@gmail.com> +# 2009 Damien Churchill <damoxc@gmail.com> +# 2010 Pedro Algarvio <pedro@algarvio.me> +# 2017 Calum Lind <calumlind+deluge@gmail.com> +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +from __future__ import unicode_literals + +import os.path + +from pkg_resources import resource_filename + + +def get_resource(filename): + return resource_filename(__package__, os.path.join('data', filename)) diff --git a/deluge/plugins/Execute/deluge_execute/core.py b/deluge/plugins/Execute/deluge_execute/core.py new file mode 100644 index 0000000..9dcd97a --- /dev/null +++ b/deluge/plugins/Execute/deluge_execute/core.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +from __future__ import unicode_literals + +import hashlib +import logging +import os +import time + +from twisted.internet.utils import getProcessOutputAndValue + +import deluge.component as component +from deluge.common import windows_check +from deluge.configmanager import ConfigManager +from deluge.core.rpcserver import export +from deluge.event import DelugeEvent +from deluge.plugins.pluginbase import CorePluginBase + +log = logging.getLogger(__name__) + +DEFAULT_CONFIG = {'commands': []} + +EXECUTE_ID = 0 +EXECUTE_EVENT = 1 +EXECUTE_COMMAND = 2 + +EVENT_MAP = { + 'complete': 'TorrentFinishedEvent', + 'added': 'TorrentAddedEvent', + 'removed': 'TorrentRemovedEvent', +} + + +class ExecuteCommandAddedEvent(DelugeEvent): + """ + Emitted when a new command is added. + """ + + def __init__(self, command_id, event, command): + self._args = [command_id, event, command] + + +class ExecuteCommandRemovedEvent(DelugeEvent): + """ + Emitted when a command is removed. + """ + + def __init__(self, command_id): + self._args = [command_id] + + +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() diff --git a/deluge/plugins/Execute/deluge_execute/data/execute.js b/deluge/plugins/Execute/deluge_execute/data/execute.js new file mode 100644 index 0000000..bd6ac98 --- /dev/null +++ b/deluge/plugins/Execute/deluge_execute/data/execute.js @@ -0,0 +1,312 @@ +/** + * execute.js + * The client-side javascript code for the Execute plugin. + * + * Copyright (C) Damien Churchill 2010 <damoxc@gmail.com> + * + * This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with + * the additional special exception to link portions of this program with the OpenSSL library. + * See LICENSE for more details. + * + */ + +Ext.ns('Deluge.ux'); + +Deluge.ux.ExecuteWindowBase = Ext.extend(Ext.Window, { + layout: 'fit', + width: 400, + height: 130, + closeAction: 'hide', + + initComponent: function() { + Deluge.ux.ExecuteWindowBase.superclass.initComponent.call(this); + this.addButton(_('Cancel'), this.onCancelClick, this); + + this.form = this.add({ + xtype: 'form', + baseCls: 'x-plain', + bodyStyle: 'padding: 5px', + items: [ + { + xtype: 'combo', + width: 270, + fieldLabel: _('Event'), + store: new Ext.data.ArrayStore({ + fields: ['id', 'text'], + data: [ + ['complete', _('Torrent Complete')], + ['added', _('Torrent Added')], + ['removed', _('Torrent Removed')], + ], + }), + name: 'event', + mode: 'local', + editable: false, + triggerAction: 'all', + valueField: 'id', + displayField: 'text', + }, + { + xtype: 'textfield', + fieldLabel: _('Command'), + name: 'command', + width: 270, + }, + ], + }); + }, + + onCancelClick: function() { + this.hide(); + }, +}); + +Deluge.ux.EditExecuteCommandWindow = Ext.extend(Deluge.ux.ExecuteWindowBase, { + title: _('Edit Command'), + + initComponent: function() { + Deluge.ux.EditExecuteCommandWindow.superclass.initComponent.call(this); + this.addButton(_('Save'), this.onSaveClick, this); + this.addEvents({ + commandedit: true, + }); + }, + + show: function(command) { + Deluge.ux.EditExecuteCommandWindow.superclass.show.call(this); + this.command = command; + this.form.getForm().setValues({ + event: command.get('event'), + command: command.get('name'), + }); + }, + + onSaveClick: function() { + var values = this.form.getForm().getFieldValues(); + deluge.client.execute.save_command( + this.command.id, + values.event, + values.command, + { + success: function() { + this.fireEvent( + 'commandedit', + this, + values.event, + values.command + ); + }, + scope: this, + } + ); + this.hide(); + }, +}); + +Deluge.ux.AddExecuteCommandWindow = Ext.extend(Deluge.ux.ExecuteWindowBase, { + title: _('Add Command'), + + initComponent: function() { + Deluge.ux.AddExecuteCommandWindow.superclass.initComponent.call(this); + this.addButton(_('Add'), this.onAddClick, this); + this.addEvents({ + commandadd: true, + }); + }, + + onAddClick: function() { + var values = this.form.getForm().getFieldValues(); + deluge.client.execute.add_command(values.event, values.command, { + success: function() { + this.fireEvent( + 'commandadd', + this, + values.event, + values.command + ); + }, + scope: this, + }); + this.hide(); + }, +}); + +Ext.ns('Deluge.ux.preferences'); + +/** + * @class Deluge.ux.preferences.ExecutePage + * @extends Ext.Panel + */ +Deluge.ux.preferences.ExecutePage = Ext.extend(Ext.Panel, { + title: _('Execute'), + header: false, + layout: 'fit', + border: false, + + initComponent: function() { + Deluge.ux.preferences.ExecutePage.superclass.initComponent.call(this); + var event_map = (this.event_map = { + complete: _('Torrent Complete'), + added: _('Torrent Added'), + removed: _('Torrent Removed'), + }); + + this.list = new Ext.list.ListView({ + store: new Ext.data.SimpleStore({ + fields: [ + { name: 'event', mapping: 1 }, + { name: 'name', mapping: 2 }, + ], + id: 0, + }), + columns: [ + { + width: 0.3, + header: _('Event'), + sortable: true, + dataIndex: 'event', + tpl: new Ext.XTemplate('{[this.getEvent(values.event)]}', { + getEvent: function(e) { + return event_map[e] ? event_map[e] : e; + }, + }), + }, + { + id: 'name', + header: _('Command'), + sortable: true, + dataIndex: 'name', + }, + ], + singleSelect: true, + autoExpandColumn: 'name', + }); + this.list.on('selectionchange', this.onSelectionChange, this); + + this.panel = this.add({ + items: [this.list], + bbar: { + items: [ + { + text: _('Add'), + iconCls: 'icon-add', + handler: this.onAddClick, + scope: this, + }, + { + text: _('Edit'), + iconCls: 'icon-edit', + handler: this.onEditClick, + scope: this, + disabled: true, + }, + '->', + { + text: _('Remove'), + iconCls: 'icon-remove', + handler: this.onRemoveClick, + scope: this, + disabled: true, + }, + ], + }, + }); + + this.on('show', this.onPreferencesShow, this); + }, + + updateCommands: function() { + deluge.client.execute.get_commands({ + success: function(commands) { + this.list.getStore().loadData(commands); + }, + scope: this, + }); + }, + + onAddClick: function() { + if (!this.addWin) { + this.addWin = new Deluge.ux.AddExecuteCommandWindow(); + this.addWin.on( + 'commandadd', + function() { + this.updateCommands(); + }, + this + ); + } + this.addWin.show(); + }, + + onCommandAdded: function(win, evt, cmd) { + var record = new this.list.getStore().recordType({ + event: evt, + command: cmd, + }); + }, + + onEditClick: function() { + if (!this.editWin) { + this.editWin = new Deluge.ux.EditExecuteCommandWindow(); + this.editWin.on( + 'commandedit', + function() { + this.updateCommands(); + }, + this + ); + } + this.editWin.show(this.list.getSelectedRecords()[0]); + }, + + onPreferencesShow: function() { + this.updateCommands(); + }, + + onRemoveClick: function() { + var record = this.list.getSelectedRecords()[0]; + deluge.client.execute.remove_command(record.id, { + success: function() { + this.updateCommands(); + }, + scope: this, + }); + }, + + onSelectionChange: function(dv, selections) { + if (selections.length) { + this.panel + .getBottomToolbar() + .items.get(1) + .enable(); + this.panel + .getBottomToolbar() + .items.get(3) + .enable(); + } else { + this.panel + .getBottomToolbar() + .items.get(1) + .disable(); + this.panel + .getBottomToolbar() + .items.get(3) + .disable(); + } + }, +}); + +Deluge.plugins.ExecutePlugin = Ext.extend(Deluge.Plugin, { + name: 'Execute', + + onDisable: function() { + deluge.preferences.removePage(this.prefsPage); + }, + + onEnable: function() { + this.prefsPage = deluge.preferences.addPage( + new Deluge.ux.preferences.ExecutePage() + ); + }, +}); +Deluge.registerPlugin('Execute', Deluge.plugins.ExecutePlugin); diff --git a/deluge/plugins/Execute/deluge_execute/data/execute_prefs.ui b/deluge/plugins/Execute/deluge_execute/data/execute_prefs.ui new file mode 100644 index 0000000..e2a5cd5 --- /dev/null +++ b/deluge/plugins/Execute/deluge_execute/data/execute_prefs.ui @@ -0,0 +1,197 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkListStore" id="liststore1"> + <columns> + <!-- column-name item --> + <column type="gchararray"/> + </columns> + </object> + <object class="GtkWindow" id="execute_window"> + <property name="can_focus">False</property> + <child> + <placeholder/> + </child> + <child> + <object class="GtkBox" id="execute_box"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkFrame" id="add_frame"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="add_alignment"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="left_padding">12</property> + <property name="right_padding">10</property> + <child> + <object class="GtkTable" id="add_table"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">3</property> + <property name="n_columns">2</property> + <child> + <object class="GtkLabel" id="event_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Event</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + <property name="x_padding">5</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="command_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Command</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + <property name="x_padding">5</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="command_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkComboBox" id="event_combobox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="model">liststore1</property> + <child> + <object class="GtkCellRendererText" id="cellrenderertext1"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkHButtonBox" id="hbuttonbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">center</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button_add"> + <property name="label">gtk-add</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + <signal name="clicked" handler="on_add_button_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="right_attach">2</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="add_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes"><b>Add Command</b></property> + <property name="use_markup">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="commands_frame"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="commands_alignment"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="left_padding">12</property> + <child> + <object class="GtkBox" id="commands_vbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="halign">start</property> + <property name="orientation">vertical</property> + <child> + <placeholder/> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="commands_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes"><b>Commands</b></property> + <property name="use_markup">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> +</interface> diff --git a/deluge/plugins/Execute/deluge_execute/data/execute_prefs.ui~ b/deluge/plugins/Execute/deluge_execute/data/execute_prefs.ui~ new file mode 100644 index 0000000..cd9b4d4 --- /dev/null +++ b/deluge/plugins/Execute/deluge_execute/data/execute_prefs.ui~ @@ -0,0 +1,190 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.0"/> + <!-- interface-naming-policy project-wide --> + <object class="GtkListStore" id="liststore1"> + <columns> + <!-- column-name item text --> + <column type="gchararray"/> + </columns> + </object> + <object class="GtkWindow" id="execute_window"> + <property name="can_focus">False</property> + <child> + <object class="GtkVBox" id="execute_box"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkFrame" id="add_frame"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="add_alignment"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="left_padding">12</property> + <child> + <object class="GtkTable" id="add_table"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">3</property> + <property name="n_columns">2</property> + <child> + <object class="GtkLabel" id="event_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Event</property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + <property name="x_padding">5</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="command_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Command</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + <property name="x_padding">5</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="command_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkComboBox" id="event_combobox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="model">liststore1</property> + <child> + <object class="GtkCellRendererText" id="cellrenderertext1"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkHButtonBox" id="hbuttonbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button_add"> + <property name="label">gtk-add</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + <signal name="clicked" handler="on_add_button_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="right_attach">2</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="add_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes"><b>Add Command</b></property> + <property name="use_markup">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="commands_frame"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="commands_alignment"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="left_padding">12</property> + <child> + <object class="GtkVBox" id="commands_vbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <child> + <placeholder/> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="commands_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes"><b>Commands</b></property> + <property name="use_markup">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> +</interface> diff --git a/deluge/plugins/Execute/deluge_execute/gtkui.py b/deluge/plugins/Execute/deluge_execute/gtkui.py new file mode 100644 index 0000000..c0c7200 --- /dev/null +++ b/deluge/plugins/Execute/deluge_execute/gtkui.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com> +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +from __future__ import unicode_literals + +import logging + +import gi # isort:skip (Required before Gtk import). + +gi.require_version('Gtk', '3.0') # NOQA: E402 + +# isort:imports-thirdparty +from gi.repository import Gtk + +# isort:imports-firstparty +import deluge.component as component +from deluge.plugins.pluginbase import Gtk3PluginBase +from deluge.ui.client import client + +# isort:imports-localfolder +from . import common + +log = logging.getLogger(__name__) + +EXECUTE_ID = 0 +EXECUTE_EVENT = 1 +EXECUTE_COMMAND = 2 + +EVENT_MAP = { + 'complete': _('Torrent Complete'), + 'added': _('Torrent Added'), + 'removed': _('Torrent Removed'), +} + +EVENTS = ['complete', 'added', 'removed'] + + +class ExecutePreferences(object): + def __init__(self, plugin): + self.plugin = plugin + + def load(self): + log.debug('Adding Execute Preferences page') + self.builder = Gtk.Builder() + self.builder.add_from_file(common.get_resource('execute_prefs.ui')) + self.builder.connect_signals(self) + + events = self.builder.get_object('event_combobox') + + store = Gtk.ListStore(str, str) + for event in EVENTS: + event_label = EVENT_MAP[event] + store.append((event_label, event)) + events.set_model(store) + events.set_active(0) + + self.plugin.add_preferences_page( + _('Execute'), self.builder.get_object('execute_box') + ) + self.plugin.register_hook('on_show_prefs', self.load_commands) + self.plugin.register_hook('on_apply_prefs', self.on_apply_prefs) + + self.load_commands() + + client.register_event_handler( + 'ExecuteCommandAddedEvent', self.on_command_added_event + ) + client.register_event_handler( + 'ExecuteCommandRemovedEvent', self.on_command_removed_event + ) + + def unload(self): + self.plugin.remove_preferences_page(_('Execute')) + self.plugin.deregister_hook('on_apply_prefs', self.on_apply_prefs) + self.plugin.deregister_hook('on_show_prefs', self.load_commands) + + def add_command(self, command_id, event, command): + log.debug('Adding command `%s`', command_id) + vbox = self.builder.get_object('commands_vbox') + hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, spacing=5) + hbox.set_name(command_id + '_' + event) + label = Gtk.Label(EVENT_MAP[event]) + entry = Gtk.Entry() + entry.set_text(command) + button = Gtk.Button() + button.set_name('remove_%s' % command_id) + button.connect('clicked', self.on_remove_button_clicked) + + img = Gtk.Image() + img.set_from_stock(Gtk.STOCK_REMOVE, Gtk.IconSize.BUTTON) + button.set_image(img) + + hbox.pack_start(label, False, False, 0) + hbox.pack_start(entry, False, False, 0) + hbox.pack_start(button, True, True, 0) + hbox.show_all() + vbox.pack_start(hbox, True, True, 0) + + def remove_command(self, command_id): + vbox = self.builder.get_object('commands_vbox') + children = vbox.get_children() + for child in children: + if child.get_name().split('_')[0] == command_id: + vbox.remove(child) + break + + def clear_commands(self): + vbox = self.builder.get_object('commands_vbox') + children = vbox.get_children() + for child in children: + vbox.remove(child) + + def load_commands(self): + def on_get_commands(commands): + self.clear_commands() + log.debug('on_get_commands: %s', commands) + for command in commands: + command_id, event, command = command + self.add_command(command_id, event, command) + + client.execute.get_commands().addCallback(on_get_commands) + + def on_add_button_clicked(self, *args): + command = self.builder.get_object('command_entry').get_text() + events = self.builder.get_object('event_combobox') + event = events.get_model()[events.get_active()][1] + client.execute.add_command(event, command) + + def on_remove_button_clicked(self, widget, *args): + command_id = widget.get_name().replace('remove_', '') + client.execute.remove_command(command_id) + + def on_apply_prefs(self): + vbox = self.builder.get_object('commands_vbox') + children = vbox.get_children() + for child in children: + command_id, event = child.get_name().split('_') + for widget in child.get_children(): + if isinstance(widget, Gtk.Entry): + command = widget.get_text() + client.execute.save_command(command_id, event, command) + + def on_command_added_event(self, command_id, event, command): + log.debug('Adding command %s: %s', event, command) + self.add_command(command_id, event, command) + + def on_command_removed_event(self, command_id): + log.debug('Removing command %s', command_id) + self.remove_command(command_id) + + +class GtkUI(Gtk3PluginBase): + def enable(self): + self.plugin = component.get('PluginManager') + self.preferences = ExecutePreferences(self.plugin) + self.preferences.load() + + def disable(self): + self.preferences.unload() diff --git a/deluge/plugins/Execute/deluge_execute/webui.py b/deluge/plugins/Execute/deluge_execute/webui.py new file mode 100644 index 0000000..8327001 --- /dev/null +++ b/deluge/plugins/Execute/deluge_execute/webui.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com> +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +from __future__ import unicode_literals + +import logging + +from deluge.plugins.pluginbase import WebPluginBase + +from .common import get_resource + +log = logging.getLogger(__name__) + + +class WebUI(WebPluginBase): + + scripts = [get_resource('execute.js')] + debug_scripts = scripts |