summaryrefslogtreecommitdiffstats
path: root/deluge/plugins/Execute
diff options
context:
space:
mode:
Diffstat (limited to 'deluge/plugins/Execute')
-rw-r--r--deluge/plugins/Execute/deluge_execute/__init__.py33
-rw-r--r--deluge/plugins/Execute/deluge_execute/common.py20
-rw-r--r--deluge/plugins/Execute/deluge_execute/core.py182
-rw-r--r--deluge/plugins/Execute/deluge_execute/data/execute.js300
-rw-r--r--deluge/plugins/Execute/deluge_execute/data/execute_prefs.ui195
-rw-r--r--deluge/plugins/Execute/deluge_execute/gtkui.py162
-rw-r--r--deluge/plugins/Execute/deluge_execute/webui.py20
-rw-r--r--deluge/plugins/Execute/setup.py41
8 files changed, 953 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..3edfc4b
--- /dev/null
+++ b/deluge/plugins/Execute/deluge_execute/__init__.py
@@ -0,0 +1,33 @@
+#
+# 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 deluge.plugins.init import PluginInitBase
+
+
+class CorePlugin(PluginInitBase):
+ def __init__(self, plugin_name):
+ from .core import Core as _pluginCls
+
+ self._plugin_cls = _pluginCls
+ super().__init__(plugin_name)
+
+
+class GtkUIPlugin(PluginInitBase):
+ def __init__(self, plugin_name):
+ from .gtkui import GtkUI as _pluginCls
+
+ self._plugin_cls = _pluginCls
+ super().__init__(plugin_name)
+
+
+class WebUIPlugin(PluginInitBase):
+ def __init__(self, plugin_name):
+ from .webui import WebUI as _pluginCls
+
+ self._plugin_cls = _pluginCls
+ super().__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..eb47f13
--- /dev/null
+++ b/deluge/plugins/Execute/deluge_execute/common.py
@@ -0,0 +1,20 @@
+#
+# 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.
+#
+
+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..6d33e54
--- /dev/null
+++ b/deluge/plugins/Execute/deluge_execute/core.py
@@ -0,0 +1,182 @@
+#
+# 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.
+#
+
+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(b'&', b'^^^&') 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..dc0b111
--- /dev/null
+++ b/deluge/plugins/Execute/deluge_execute/data/execute.js
@@ -0,0 +1,300 @@
+/**
+ * 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..5d6354b
--- /dev/null
+++ b/deluge/plugins/Execute/deluge_execute/data/execute_prefs.ui
@@ -0,0 +1,195 @@
+<?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>
+ </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">&lt;b&gt;Add Command&lt;/b&gt;</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">&lt;b&gt;Commands&lt;/b&gt;</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..f56a6de
--- /dev/null
+++ b/deluge/plugins/Execute/deluge_execute/gtkui.py
@@ -0,0 +1,162 @@
+#
+# 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.
+#
+
+import logging
+
+import gi # isort:skip (Required before Gtk import).
+
+gi.require_version('Gtk', '3.0')
+
+# 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:
+ 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..3586371
--- /dev/null
+++ b/deluge/plugins/Execute/deluge_execute/webui.py
@@ -0,0 +1,20 @@
+#
+# 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.
+#
+
+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
diff --git a/deluge/plugins/Execute/setup.py b/deluge/plugins/Execute/setup.py
new file mode 100644
index 0000000..b65c1bd
--- /dev/null
+++ b/deluge/plugins/Execute/setup.py
@@ -0,0 +1,41 @@
+#
+# 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 setuptools import find_packages, setup
+
+__plugin_name__ = 'Execute'
+__author__ = 'Damien Churchill'
+__author_email__ = 'damoxc@gmail.com'
+__version__ = '1.3'
+__url__ = 'http://deluge-torrent.org'
+__license__ = 'GPLv3'
+__description__ = 'Plugin to execute a command upon an event'
+__long_description__ = __description__
+__pkg_data__ = {'deluge_' + __plugin_name__.lower(): ['data/*']}
+
+setup(
+ name=__plugin_name__,
+ version=__version__,
+ description=__description__,
+ author=__author__,
+ author_email=__author_email__,
+ url=__url__,
+ license=__license__,
+ long_description=__long_description__,
+ packages=find_packages(),
+ package_data=__pkg_data__,
+ entry_points="""
+ [deluge.plugin.core]
+ %s = deluge_%s:CorePlugin
+ [deluge.plugin.gtk3ui]
+ %s = deluge_%s:GtkUIPlugin
+ [deluge.plugin.web]
+ %s = deluge_%s:WebUIPlugin
+ """
+ % ((__plugin_name__, __plugin_name__.lower()) * 3),
+)