summaryrefslogtreecommitdiffstats
path: root/deluge/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'deluge/plugins')
-rw-r--r--deluge/plugins/AutoAdd/deluge_autoadd/__init__.py38
-rw-r--r--deluge/plugins/AutoAdd/deluge_autoadd/common.py21
-rw-r--r--deluge/plugins/AutoAdd/deluge_autoadd/core.py528
-rw-r--r--deluge/plugins/AutoAdd/deluge_autoadd/data/autoadd.js226
-rw-r--r--deluge/plugins/AutoAdd/deluge_autoadd/data/autoadd_options.js470
-rw-r--r--deluge/plugins/AutoAdd/deluge_autoadd/data/autoadd_options.ui1322
-rw-r--r--deluge/plugins/AutoAdd/deluge_autoadd/data/autoadd_options/main_tab.js304
-rw-r--r--deluge/plugins/AutoAdd/deluge_autoadd/data/autoadd_options/options_tab.js302
-rw-r--r--deluge/plugins/AutoAdd/deluge_autoadd/data/config.ui134
-rw-r--r--deluge/plugins/AutoAdd/deluge_autoadd/gtkui.py576
-rw-r--r--deluge/plugins/AutoAdd/deluge_autoadd/webui.py35
-rw-r--r--deluge/plugins/AutoAdd/setup.py47
-rw-r--r--deluge/plugins/Blocklist/deluge_blocklist/__init__.py33
-rw-r--r--deluge/plugins/Blocklist/deluge_blocklist/common.py172
-rw-r--r--deluge/plugins/Blocklist/deluge_blocklist/core.py549
-rw-r--r--deluge/plugins/Blocklist/deluge_blocklist/data/blocklist.js429
-rw-r--r--deluge/plugins/Blocklist/deluge_blocklist/data/blocklist16.pngbin0 -> 586 bytes
-rw-r--r--deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_download24.pngbin0 -> 764 bytes
-rw-r--r--deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_import24.pngbin0 -> 1091 bytes
-rw-r--r--deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_pref.ui603
-rw-r--r--deluge/plugins/Blocklist/deluge_blocklist/decompressers.py44
-rw-r--r--deluge/plugins/Blocklist/deluge_blocklist/detect.py48
-rw-r--r--deluge/plugins/Blocklist/deluge_blocklist/gtkui.py254
-rw-r--r--deluge/plugins/Blocklist/deluge_blocklist/peerguardian.py66
-rw-r--r--deluge/plugins/Blocklist/deluge_blocklist/readers.py99
-rw-r--r--deluge/plugins/Blocklist/deluge_blocklist/webui.py27
-rw-r--r--deluge/plugins/Blocklist/setup.py42
-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
-rw-r--r--deluge/plugins/Extractor/deluge_extractor/__init__.py37
-rw-r--r--deluge/plugins/Extractor/deluge_extractor/common.py20
-rw-r--r--deluge/plugins/Extractor/deluge_extractor/core.py186
-rw-r--r--deluge/plugins/Extractor/deluge_extractor/data/extractor.js100
-rw-r--r--deluge/plugins/Extractor/deluge_extractor/data/extractor_prefs.ui121
-rw-r--r--deluge/plugins/Extractor/deluge_extractor/gtkui.py93
-rw-r--r--deluge/plugins/Extractor/deluge_extractor/webui.py24
-rw-r--r--deluge/plugins/Extractor/setup.py54
-rw-r--r--deluge/plugins/Label/TODO11
-rw-r--r--deluge/plugins/Label/deluge_label/__init__.py37
-rw-r--r--deluge/plugins/Label/deluge_label/common.py20
-rw-r--r--deluge/plugins/Label/deluge_label/core.py348
-rw-r--r--deluge/plugins/Label/deluge_label/data/label.js635
-rw-r--r--deluge/plugins/Label/deluge_label/data/label_add.ui172
-rw-r--r--deluge/plugins/Label/deluge_label/data/label_options.ui723
-rw-r--r--deluge/plugins/Label/deluge_label/data/label_pref.ui56
-rw-r--r--deluge/plugins/Label/deluge_label/gtkui/__init__.py74
-rw-r--r--deluge/plugins/Label/deluge_label/gtkui/label_config.py58
-rw-r--r--deluge/plugins/Label/deluge_label/gtkui/sidebar_menu.py259
-rw-r--r--deluge/plugins/Label/deluge_label/gtkui/submenu.py62
-rw-r--r--deluge/plugins/Label/deluge_label/test.py47
-rw-r--r--deluge/plugins/Label/deluge_label/webui.py24
-rw-r--r--deluge/plugins/Label/setup.py45
-rwxr-xr-xdeluge/plugins/Notifications/create_dev_link.sh11
-rw-r--r--deluge/plugins/Notifications/deluge_notifications/__init__.py38
-rw-r--r--deluge/plugins/Notifications/deluge_notifications/common.py114
-rw-r--r--deluge/plugins/Notifications/deluge_notifications/core.py228
-rw-r--r--deluge/plugins/Notifications/deluge_notifications/data/config.ui641
-rw-r--r--deluge/plugins/Notifications/deluge_notifications/data/notifications.js522
-rw-r--r--deluge/plugins/Notifications/deluge_notifications/gtkui.py741
-rw-r--r--deluge/plugins/Notifications/deluge_notifications/test.py86
-rw-r--r--deluge/plugins/Notifications/deluge_notifications/webui.py31
-rwxr-xr-xdeluge/plugins/Notifications/setup.py53
-rw-r--r--deluge/plugins/Scheduler/deluge_scheduler/__init__.py37
-rw-r--r--deluge/plugins/Scheduler/deluge_scheduler/common.py20
-rw-r--r--deluge/plugins/Scheduler/deluge_scheduler/core.py167
-rw-r--r--deluge/plugins/Scheduler/deluge_scheduler/data/green.svg1
-rw-r--r--deluge/plugins/Scheduler/deluge_scheduler/data/red.svg1
-rw-r--r--deluge/plugins/Scheduler/deluge_scheduler/data/scheduler.js621
-rw-r--r--deluge/plugins/Scheduler/deluge_scheduler/data/yellow.svg1
-rw-r--r--deluge/plugins/Scheduler/deluge_scheduler/gtkui.py356
-rw-r--r--deluge/plugins/Scheduler/deluge_scheduler/webui.py23
-rw-r--r--deluge/plugins/Scheduler/setup.py45
-rwxr-xr-xdeluge/plugins/Stats/create_dev_link.sh11
-rw-r--r--deluge/plugins/Stats/deluge_stats/__init__.py37
-rw-r--r--deluge/plugins/Stats/deluge_stats/common.py20
-rw-r--r--deluge/plugins/Stats/deluge_stats/core.py218
-rw-r--r--deluge/plugins/Stats/deluge_stats/data/config.ui284
-rw-r--r--deluge/plugins/Stats/deluge_stats/data/stats.js27
-rw-r--r--deluge/plugins/Stats/deluge_stats/data/tabs.ui169
-rw-r--r--deluge/plugins/Stats/deluge_stats/graph.py343
-rw-r--r--deluge/plugins/Stats/deluge_stats/gtkui.py296
-rw-r--r--deluge/plugins/Stats/deluge_stats/template/graph.html12
-rw-r--r--deluge/plugins/Stats/deluge_stats/tests/__init__.py0
-rw-r--r--deluge/plugins/Stats/deluge_stats/tests/test.html9
-rw-r--r--deluge/plugins/Stats/deluge_stats/tests/test_stats.py106
-rw-r--r--deluge/plugins/Stats/deluge_stats/webui.py32
-rw-r--r--deluge/plugins/Stats/setup.py49
-rw-r--r--deluge/plugins/Toggle/deluge_toggle/__init__.py38
-rw-r--r--deluge/plugins/Toggle/deluge_toggle/common.py20
-rw-r--r--deluge/plugins/Toggle/deluge_toggle/core.py47
-rw-r--r--deluge/plugins/Toggle/deluge_toggle/data/toggle.js27
-rw-r--r--deluge/plugins/Toggle/deluge_toggle/gtkui.py53
-rw-r--r--deluge/plugins/Toggle/deluge_toggle/webui.py30
-rw-r--r--deluge/plugins/Toggle/setup.py46
-rwxr-xr-xdeluge/plugins/WebUi/create_dev_link.sh11
-rw-r--r--deluge/plugins/WebUi/deluge_webui/__init__.py37
-rw-r--r--deluge/plugins/WebUi/deluge_webui/common.py20
-rw-r--r--deluge/plugins/WebUi/deluge_webui/core.py117
-rw-r--r--deluge/plugins/WebUi/deluge_webui/data/config.ui127
-rw-r--r--deluge/plugins/WebUi/deluge_webui/gtkui.py97
-rw-r--r--deluge/plugins/WebUi/deluge_webui/tests/__init__.py0
-rw-r--r--deluge/plugins/WebUi/deluge_webui/tests/test_plugin_webui.py44
-rw-r--r--deluge/plugins/WebUi/setup.py43
-rw-r--r--deluge/plugins/__init__.py0
-rw-r--r--deluge/plugins/init.py27
-rw-r--r--deluge/plugins/pluginbase.py82
112 files changed, 16744 insertions, 0 deletions
diff --git a/deluge/plugins/AutoAdd/deluge_autoadd/__init__.py b/deluge/plugins/AutoAdd/deluge_autoadd/__init__.py
new file mode 100644
index 0000000..5f5e766
--- /dev/null
+++ b/deluge/plugins/AutoAdd/deluge_autoadd/__init__.py
@@ -0,0 +1,38 @@
+#
+# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
+# 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 Gtk3UIPlugin(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/AutoAdd/deluge_autoadd/common.py b/deluge/plugins/AutoAdd/deluge_autoadd/common.py
new file mode 100644
index 0000000..6a790cb
--- /dev/null
+++ b/deluge/plugins/AutoAdd/deluge_autoadd/common.py
@@ -0,0 +1,21 @@
+#
+# 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, subdir=False):
+ folder = os.path.join('data', 'autoadd_options') if subdir else 'data'
+ return resource_filename(__package__, os.path.join(folder, filename))
diff --git a/deluge/plugins/AutoAdd/deluge_autoadd/core.py b/deluge/plugins/AutoAdd/deluge_autoadd/core.py
new file mode 100644
index 0000000..271d5f0
--- /dev/null
+++ b/deluge/plugins/AutoAdd/deluge_autoadd/core.py
@@ -0,0 +1,528 @@
+#
+# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
+# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
+# 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 os
+import shutil
+from base64 import b64encode
+
+from twisted.internet import reactor
+from twisted.internet.defer import maybeDeferred
+from twisted.internet.task import LoopingCall, deferLater
+from twisted.python.failure import Failure
+
+import deluge.component as component
+import deluge.configmanager
+from deluge._libtorrent import lt
+from deluge.common import AUTH_LEVEL_ADMIN, is_magnet
+from deluge.core.rpcserver import export
+from deluge.error import AddTorrentError, InvalidTorrentError
+from deluge.event import DelugeEvent
+from deluge.plugins.pluginbase import CorePluginBase
+
+log = logging.getLogger(__name__)
+
+
+DEFAULT_PREFS = {'watchdirs': {}, 'next_id': 1}
+
+
+OPTIONS_AVAILABLE = { # option: builtin
+ 'enabled': False,
+ 'path': False,
+ 'append_extension': False,
+ 'copy_torrent': False,
+ 'delete_copy_torrent_toggle': False,
+ 'abspath': False,
+ 'download_location': True,
+ 'max_download_speed': True,
+ 'max_upload_speed': True,
+ 'max_connections': True,
+ 'max_upload_slots': True,
+ 'prioritize_first_last': True,
+ 'auto_managed': True,
+ 'stop_at_ratio': True,
+ 'stop_ratio': True,
+ 'remove_at_ratio': True,
+ 'move_completed': True,
+ 'move_completed_path': True,
+ 'label': False,
+ 'add_paused': True,
+ 'queue_to_top': False,
+ 'owner': True,
+ 'seed_mode': True,
+}
+
+MAX_NUM_ATTEMPTS = 10
+
+
+class AutoaddOptionsChangedEvent(DelugeEvent):
+ """Emitted when the options for the plugin are changed."""
+
+ def __init__(self):
+ pass
+
+
+def check_input(cond, message):
+ if not cond:
+ raise Exception(message)
+
+
+class Core(CorePluginBase):
+ def enable(self):
+ # reduce typing, assigning some values to self...
+ self.config = deluge.configmanager.ConfigManager('autoadd.conf', DEFAULT_PREFS)
+ self.config.run_converter((0, 1), 2, self.__migrate_config_1_to_2)
+ self.config.save()
+ self.watchdirs = self.config['watchdirs']
+
+ self.rpcserver = component.get('RPCServer')
+ component.get('EventManager').register_event_handler(
+ 'PreTorrentRemovedEvent', self.__on_pre_torrent_removed
+ )
+
+ # Dict of Filename:Attempts
+ self.invalid_torrents = {}
+ # Loopingcall timers for each enabled watchdir
+ self.update_timers = {}
+ deferLater(reactor, 5, self.enable_looping)
+
+ def enable_looping(self):
+ # Enable all looping calls for enabled watchdirs here
+ for watchdir_id, watchdir in self.watchdirs.items():
+ if watchdir['enabled']:
+ self.enable_watchdir(watchdir_id)
+
+ def disable(self):
+ # disable all running looping calls
+ component.get('EventManager').deregister_event_handler(
+ 'PreTorrentRemovedEvent', self.__on_pre_torrent_removed
+ )
+ for loopingcall in self.update_timers.values():
+ loopingcall.stop()
+ self.config.save()
+
+ def update(self):
+ pass
+
+ @export
+ def set_options(self, watchdir_id, options):
+ """Update the options for a watch folder."""
+ watchdir_id = str(watchdir_id)
+ options = self._make_unicode(options)
+ check_input(watchdir_id in self.watchdirs, _('Watch folder does not exist.'))
+ if 'path' in options:
+ options['abspath'] = os.path.abspath(options['path'])
+ check_input(os.path.isdir(options['abspath']), _('Path does not exist.'))
+ for w_id, w in self.watchdirs.items():
+ if options['abspath'] == w['abspath'] and watchdir_id != w_id:
+ raise Exception('Path is already being watched.')
+ for key in options:
+ if key not in OPTIONS_AVAILABLE:
+ if key not in [key2 + '_toggle' for key2 in OPTIONS_AVAILABLE]:
+ raise Exception('autoadd: Invalid options key:%s' % key)
+ # disable the watch loop if it was active
+ if watchdir_id in self.update_timers:
+ self.disable_watchdir(watchdir_id)
+
+ self.watchdirs[watchdir_id].update(options)
+ # re-enable watch loop if appropriate
+ if self.watchdirs[watchdir_id]['enabled']:
+ self.enable_watchdir(watchdir_id)
+ self.config.save()
+ component.get('EventManager').emit(AutoaddOptionsChangedEvent())
+
+ def load_torrent(self, filename, magnet):
+ log.debug('Attempting to open %s for add.', filename)
+ file_mode = 'r' if magnet else 'rb'
+ try:
+ with open(filename, file_mode) as _file:
+ filedump = _file.read()
+ except OSError as ex:
+ log.warning('Unable to open %s: %s', filename, ex)
+ raise ex
+
+ if not filedump:
+ raise EOFError('Torrent is 0 bytes!')
+
+ # Get the info to see if any exceptions are raised
+ if not magnet:
+ decoded_torrent = lt.bdecode(filedump)
+ if decoded_torrent is None:
+ raise InvalidTorrentError('Torrent file failed decoding.')
+ lt.torrent_info(decoded_torrent)
+
+ return filedump
+
+ def split_magnets(self, filename):
+ log.debug('Attempting to open %s for splitting magnets.', filename)
+ magnets = []
+ try:
+ with open(filename) as _file:
+ magnets = list(filter(len, _file.read().splitlines()))
+ except OSError as ex:
+ log.warning('Unable to open %s: %s', filename, ex)
+
+ if len(magnets) < 2:
+ return []
+
+ path = filename.rsplit(os.sep, 1)[0]
+ for magnet in magnets:
+ if not is_magnet(magnet):
+ log.warning('Found line which is not a magnet: %s', magnet)
+ continue
+
+ for part in magnet.split('&'):
+ if part.startswith('dn='):
+ name = part[3:].strip()
+ if name:
+ mname = os.sep.join([path, name + '.magnet'])
+ break
+ else:
+ short_hash = magnet.split('btih:')[1][:8]
+ mname = '.'.join([os.path.splitext(filename)[0], short_hash, 'magnet'])
+
+ try:
+ with open(mname, 'w') as _mfile:
+ _mfile.write(magnet)
+ except OSError as ex:
+ log.warning('Unable to open %s: %s', mname, ex)
+ return magnets
+
+ def update_watchdir(self, watchdir_id):
+ """Check the watch folder for new torrents to add."""
+ log.trace('Updating watchdir id: %s', watchdir_id)
+ watchdir_id = str(watchdir_id)
+ watchdir = self.watchdirs[watchdir_id]
+ if not watchdir['enabled']:
+ # We shouldn't be updating because this watchdir is not enabled
+ log.debug('Watchdir id %s is not enabled. Disabling it.', watchdir_id)
+ self.disable_watchdir(watchdir_id)
+ return
+
+ if not os.path.isdir(watchdir['abspath']):
+ log.warning('Invalid AutoAdd folder: %s', watchdir['abspath'])
+ self.disable_watchdir(watchdir_id)
+ return
+
+ # Generate options dict for watchdir
+ options = {}
+ if 'stop_at_ratio_toggle' in watchdir:
+ watchdir['stop_ratio_toggle'] = watchdir['stop_at_ratio_toggle']
+ # We default to True when reading _toggle values, so a config
+ # without them is valid, and applies all its settings.
+ for option, value in watchdir.items():
+ if OPTIONS_AVAILABLE.get(option):
+ if watchdir.get(option + '_toggle', True) or option in [
+ 'owner',
+ 'seed_mode',
+ ]:
+ options[option] = value
+
+ # Check for .magnet files containing multiple magnet links and
+ # create a new .magnet file for each of them.
+ for filename in os.listdir(watchdir['abspath']):
+ try:
+ filepath = os.path.join(watchdir['abspath'], filename)
+ except UnicodeDecodeError as ex:
+ log.error(
+ 'Unable to auto add torrent due to improper filename encoding: %s',
+ ex,
+ )
+ continue
+ if os.path.isdir(filepath):
+ # Skip directories
+ continue
+ elif os.path.splitext(filename)[1] == '.magnet' and self.split_magnets(
+ filepath
+ ):
+ os.remove(filepath)
+
+ for filename in os.listdir(watchdir['abspath']):
+ try:
+ filepath = os.path.join(watchdir['abspath'], filename)
+ except UnicodeDecodeError as ex:
+ log.error(
+ 'Unable to auto add torrent due to improper filename encoding: %s',
+ ex,
+ )
+ continue
+
+ if os.path.isdir(filepath):
+ # Skip directories
+ continue
+
+ ext = os.path.splitext(filename)[1].lower()
+ magnet = ext == '.magnet'
+ if not magnet and not ext == '.torrent':
+ log.debug('File checked for auto-loading is invalid: %s', filename)
+ continue
+
+ try:
+ filedump = self.load_torrent(filepath, magnet)
+ except (OSError, EOFError, RuntimeError, InvalidTorrentError) as ex:
+ # If torrent is invalid, keep track of it so can try again on the next pass.
+ # This catches torrent files that may not be fully saved to disk at load time.
+ log.debug('Torrent is invalid: %s', ex)
+ if filename in self.invalid_torrents:
+ self.invalid_torrents[filename] += 1
+ if self.invalid_torrents[filename] >= MAX_NUM_ATTEMPTS:
+ log.warning(
+ 'Maximum attempts reached while trying to add the '
+ 'torrent file with the path %s',
+ filepath,
+ )
+ os.rename(filepath, filepath + '.invalid')
+ del self.invalid_torrents[filename]
+ else:
+ self.invalid_torrents[filename] = 1
+ continue
+
+ def on_torrent_added(torrent_id, filename, filepath):
+ if 'Label' in component.get('CorePluginManager').get_enabled_plugins():
+ if watchdir.get('label_toggle', True) and watchdir.get('label'):
+ label = component.get('CorePlugin.Label')
+ if not watchdir['label'] in label.get_labels():
+ label.add(watchdir['label'])
+ try:
+ label.set_torrent(torrent_id, watchdir['label'])
+ except Exception as ex:
+ log.error('Unable to set label: %s', ex)
+
+ if (
+ watchdir.get('queue_to_top_toggle', True)
+ and 'queue_to_top' in watchdir
+ ):
+ if watchdir['queue_to_top']:
+ component.get('TorrentManager').queue_top(torrent_id)
+ else:
+ component.get('TorrentManager').queue_bottom(torrent_id)
+
+ # Rename, copy or delete the torrent once added to deluge.
+ if watchdir.get('append_extension_toggle'):
+ if not watchdir.get('append_extension'):
+ watchdir['append_extension'] = '.added'
+ os.rename(filepath, filepath + watchdir['append_extension'])
+ elif watchdir.get('copy_torrent_toggle'):
+ copy_torrent_path = watchdir['copy_torrent']
+ copy_torrent_file = os.path.join(copy_torrent_path, filename)
+ log.debug(
+ 'Moving added torrent file "%s" to "%s"',
+ os.path.basename(filepath),
+ copy_torrent_path,
+ )
+ shutil.move(filepath, copy_torrent_file)
+ else:
+ os.remove(filepath)
+
+ def fail_torrent_add(err_msg, filepath, magnet):
+ if isinstance(err_msg, Failure):
+ err_msg = err_msg.getErrorMessage()
+
+ # torrent handle is invalid and so is the magnet link
+ log.error(
+ 'Cannot Autoadd %s: %s: %s',
+ 'magnet' if magnet else 'torrent file',
+ filepath,
+ err_msg,
+ )
+ os.rename(filepath, filepath + '.invalid')
+
+ try:
+ # The torrent looks good, so lets add it to the session.
+ if magnet:
+ d = maybeDeferred(
+ component.get('Core').add_torrent_magnet,
+ filedump.strip(),
+ options,
+ )
+ else:
+ d = component.get('Core').add_torrent_file_async(
+ filename, b64encode(filedump), options
+ )
+ d.addCallback(on_torrent_added, filename, filepath)
+ d.addErrback(fail_torrent_add, filepath, magnet)
+ except AddTorrentError as ex:
+ fail_torrent_add(str(ex), filepath, magnet)
+
+ def on_update_watchdir_error(self, failure, watchdir_id):
+ """Disables any watch folders with un-handled exceptions."""
+ self.disable_watchdir(watchdir_id)
+ log.error(
+ 'Disabling "%s", error during update: %s',
+ self.watchdirs[watchdir_id]['path'],
+ failure,
+ )
+
+ @export
+ def enable_watchdir(self, watchdir_id):
+ w_id = str(watchdir_id)
+ # Enable the looping call
+ if w_id not in self.update_timers or not self.update_timers[w_id].running:
+ self.update_timers[w_id] = LoopingCall(self.update_watchdir, w_id)
+ self.update_timers[w_id].start(5).addErrback(
+ self.on_update_watchdir_error, w_id
+ )
+ # Update the config
+ if not self.watchdirs[w_id]['enabled']:
+ self.watchdirs[w_id]['enabled'] = True
+ self.config.save()
+ component.get('EventManager').emit(AutoaddOptionsChangedEvent())
+
+ @export
+ def disable_watchdir(self, watchdir_id):
+ w_id = str(watchdir_id)
+ # Disable the looping call
+ if w_id in self.update_timers:
+ if self.update_timers[w_id].running:
+ self.update_timers[w_id].stop()
+ del self.update_timers[w_id]
+ # Update the config
+ if self.watchdirs[w_id]['enabled']:
+ self.watchdirs[w_id]['enabled'] = False
+ self.config.save()
+ component.get('EventManager').emit(AutoaddOptionsChangedEvent())
+
+ @export
+ def set_config(self, config):
+ """Sets the config dictionary."""
+ config = self._make_unicode(config)
+ for key in config:
+ self.config[key] = config[key]
+ self.config.save()
+ component.get('EventManager').emit(AutoaddOptionsChangedEvent())
+
+ @export
+ def get_config(self):
+ """Returns the config dictionary."""
+ return self.config.config
+
+ @export
+ def get_watchdirs(self):
+ session_user = self.rpcserver.get_session_user()
+ session_auth_level = self.rpcserver.get_session_auth_level()
+ if session_auth_level == AUTH_LEVEL_ADMIN:
+ log.debug(
+ 'Current logged in user %s is an ADMIN, send all ' 'watchdirs',
+ session_user,
+ )
+ return self.watchdirs
+
+ watchdirs = {}
+ for watchdir_id, watchdir in self.watchdirs.items():
+ if watchdir.get('owner', 'localclient') == session_user:
+ watchdirs[watchdir_id] = watchdir
+
+ log.debug(
+ 'Current logged in user %s is not an ADMIN, send only '
+ 'their watchdirs: %s',
+ session_user,
+ list(watchdirs),
+ )
+ return watchdirs
+
+ def _make_unicode(self, options):
+ opts = {}
+ for key in options:
+ if isinstance(options[key], bytes):
+ options[key] = options[key].decode('utf8')
+ opts[key] = options[key]
+ return opts
+
+ @export
+ def add(self, options=None):
+ """Add a watch folder."""
+ if options is None:
+ options = {}
+ options = self._make_unicode(options)
+ abswatchdir = os.path.abspath(options['path'])
+ check_input(os.path.isdir(abswatchdir), _('Path does not exist.'))
+ check_input(
+ os.access(abswatchdir, os.R_OK | os.W_OK),
+ 'You must have read and write access to watch folder.',
+ )
+ if abswatchdir in [wd['abspath'] for wd in self.watchdirs.values()]:
+ raise Exception('Path is already being watched.')
+ options.setdefault('enabled', False)
+ options['abspath'] = abswatchdir
+ watchdir_id = self.config['next_id']
+ self.watchdirs[str(watchdir_id)] = options
+ if options.get('enabled'):
+ self.enable_watchdir(watchdir_id)
+ self.config['next_id'] = watchdir_id + 1
+ self.config.save()
+ component.get('EventManager').emit(AutoaddOptionsChangedEvent())
+ return watchdir_id
+
+ @export
+ def remove(self, watchdir_id):
+ """Remove a watch folder."""
+ watchdir_id = str(watchdir_id)
+ check_input(
+ watchdir_id in self.watchdirs, 'Unknown Watchdir: %s' % self.watchdirs
+ )
+ if self.watchdirs[watchdir_id]['enabled']:
+ self.disable_watchdir(watchdir_id)
+ del self.watchdirs[watchdir_id]
+ self.config.save()
+ component.get('EventManager').emit(AutoaddOptionsChangedEvent())
+
+ def __migrate_config_1_to_2(self, config):
+ for watchdir_id in config['watchdirs']:
+ config['watchdirs'][watchdir_id]['owner'] = 'localclient'
+ return config
+
+ def __on_pre_torrent_removed(self, torrent_id):
+ try:
+ torrent = component.get('TorrentManager')[torrent_id]
+ except KeyError:
+ log.warning(
+ 'Unable to remove torrent file for torrent id %s. It'
+ 'was already deleted from the TorrentManager',
+ torrent_id,
+ )
+ return
+ torrent_fname = torrent.filename
+ for watchdir in self.watchdirs.values():
+ if not watchdir.get('copy_torrent_toggle', False):
+ # This watchlist does copy torrents
+ continue
+ elif not watchdir.get('delete_copy_torrent_toggle', False):
+ # This watchlist is not set to delete finished torrents
+ continue
+ copy_torrent_path = watchdir['copy_torrent']
+ torrent_fname_path = os.path.join(copy_torrent_path, torrent_fname)
+ if os.path.isfile(torrent_fname_path):
+ try:
+ os.remove(torrent_fname_path)
+ log.info(
+ 'Removed torrent file "%s" from "%s"',
+ torrent_fname,
+ copy_torrent_path,
+ )
+ break
+ except OSError as ex:
+ log.info(
+ 'Failed to removed torrent file "%s" from "%s": %s',
+ torrent_fname,
+ copy_torrent_path,
+ ex,
+ )
+
+ @export
+ def is_admin_level(self):
+ return self.rpcserver.get_session_auth_level() == deluge.common.AUTH_LEVEL_ADMIN
+
+ @export
+ def get_auth_user(self):
+ return self.rpcserver.get_session_user()
diff --git a/deluge/plugins/AutoAdd/deluge_autoadd/data/autoadd.js b/deluge/plugins/AutoAdd/deluge_autoadd/data/autoadd.js
new file mode 100644
index 0000000..e68fce3
--- /dev/null
+++ b/deluge/plugins/AutoAdd/deluge_autoadd/data/autoadd.js
@@ -0,0 +1,226 @@
+/**
+ * Script: autoadd.js
+ * The client-side javascript code for the AutoAdd plugin.
+ *
+ * Copyright (C) 2009 GazpachoKing <chase.sterling@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.AutoAdd');
+Deluge.ux.AutoAdd.onClickFunctions = {};
+
+Ext.ns('Deluge.ux.preferences');
+
+/**
+ * @class Deluge.ux.preferences.AutoAddPage
+ * @extends Ext.Panel
+ */
+Deluge.ux.preferences.AutoAddPage = Ext.extend(Ext.Panel, {
+ title: _('AutoAdd'),
+ header: false,
+ layout: 'fit',
+ border: false,
+ watchdirs: {},
+
+ initComponent: function () {
+ Deluge.ux.preferences.AutoAddPage.superclass.initComponent.call(this);
+
+ var autoAdd = this;
+
+ this.list = new Ext.list.ListView({
+ store: new Ext.data.JsonStore({
+ fields: ['id', 'enabled', 'owner', 'path'],
+ }),
+ columns: [
+ {
+ id: 'enabled',
+ header: _('Active'),
+ sortable: true,
+ dataIndex: 'enabled',
+ tpl: new Ext.XTemplate('{enabled:this.getCheckbox}', {
+ getCheckbox: function (checked, selected) {
+ Deluge.ux.AutoAdd.onClickFunctions[selected.id] =
+ function () {
+ if (selected.enabled) {
+ deluge.client.autoadd.disable_watchdir(
+ selected.id
+ );
+ checked = false;
+ } else {
+ deluge.client.autoadd.enable_watchdir(
+ selected.id
+ );
+ checked = true;
+ }
+ autoAdd.updateWatchDirs();
+ };
+ return (
+ '<input id="enabled-' +
+ selected.id +
+ '" type="checkbox"' +
+ (checked ? ' checked' : '') +
+ ' onclick="Deluge.ux.AutoAdd.onClickFunctions[' +
+ selected.id +
+ ']()" />'
+ );
+ },
+ }),
+ width: 0.15,
+ },
+ {
+ id: 'owner',
+ header: _('Owner'),
+ sortable: true,
+ dataIndex: 'owner',
+ width: 0.2,
+ },
+ {
+ id: 'path',
+ header: _('Path'),
+ sortable: true,
+ dataIndex: 'path',
+ },
+ ],
+ singleSelect: true,
+ autoExpandColumn: 'path',
+ });
+ 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);
+ },
+
+ updateWatchDirs: function () {
+ deluge.client.autoadd.get_watchdirs({
+ success: function (watchdirs) {
+ this.watchdirs = watchdirs;
+ var watchdirsArray = [];
+ for (var id in watchdirs) {
+ if (watchdirs.hasOwnProperty(id)) {
+ var watchdir = {};
+ watchdir['id'] = id;
+ watchdir['enabled'] = watchdirs[id].enabled;
+ watchdir['owner'] =
+ watchdirs[id].owner || 'localclient';
+ watchdir['path'] = watchdirs[id].path;
+
+ watchdirsArray.push(watchdir);
+ }
+ }
+ this.list.getStore().loadData(watchdirsArray);
+ },
+ scope: this,
+ });
+ },
+
+ onAddClick: function () {
+ if (!this.addWin) {
+ this.addWin = new Deluge.ux.AutoAdd.AddAutoAddCommandWindow();
+ this.addWin.on(
+ 'watchdiradd',
+ function () {
+ this.updateWatchDirs();
+ },
+ this
+ );
+ }
+ this.addWin.show();
+ },
+
+ onEditClick: function () {
+ if (!this.editWin) {
+ this.editWin = new Deluge.ux.AutoAdd.EditAutoAddCommandWindow();
+ this.editWin.on(
+ 'watchdiredit',
+ function () {
+ this.updateWatchDirs();
+ },
+ this
+ );
+ }
+ var id = this.list.getSelectedRecords()[0].id;
+ this.editWin.show(id, this.watchdirs[id]);
+ },
+
+ onPreferencesShow: function () {
+ this.updateWatchDirs();
+ },
+
+ onRemoveClick: function () {
+ var record = this.list.getSelectedRecords()[0];
+ deluge.client.autoadd.remove(record.id, {
+ success: function () {
+ this.updateWatchDirs();
+ },
+ 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.AutoAddPlugin = Ext.extend(Deluge.Plugin, {
+ name: 'AutoAdd',
+
+ static: {
+ prefsPage: null,
+ },
+
+ onDisable: function () {
+ deluge.preferences.removePage(Deluge.plugins.AutoAddPlugin.prefsPage);
+ Deluge.plugins.AutoAddPlugin.prefsPage = null;
+ },
+
+ onEnable: function () {
+ /*
+ * Called for each of the JavaScript files.
+ * This will prevent adding unnecessary tabs to the preferences window.
+ */
+ if (!Deluge.plugins.AutoAddPlugin.prefsPage) {
+ Deluge.plugins.AutoAddPlugin.prefsPage = deluge.preferences.addPage(
+ new Deluge.ux.preferences.AutoAddPage()
+ );
+ }
+ },
+});
+
+Deluge.registerPlugin('AutoAdd', Deluge.plugins.AutoAddPlugin);
diff --git a/deluge/plugins/AutoAdd/deluge_autoadd/data/autoadd_options.js b/deluge/plugins/AutoAdd/deluge_autoadd/data/autoadd_options.js
new file mode 100644
index 0000000..7ec4448
--- /dev/null
+++ b/deluge/plugins/AutoAdd/deluge_autoadd/data/autoadd_options.js
@@ -0,0 +1,470 @@
+/**
+ * Script: autoadd.js
+ * The client-side javascript code for the AutoAdd plugin.
+ *
+ * Copyright (C) 2009 GazpachoKing <chase.sterling@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.AutoAdd');
+
+/**
+ * @class Deluge.ux.AutoAdd.AutoAddWindowBase
+ * @extends Ext.Window
+ */
+Deluge.ux.AutoAdd.AutoAddWindowBase = Ext.extend(Ext.Window, {
+ width: 350,
+ autoHeight: true,
+ closeAction: 'hide',
+
+ spin_ids: ['max_download_speed', 'max_upload_speed', 'stop_ratio'],
+ spin_int_ids: ['max_upload_slots', 'max_connections'],
+ chk_ids: [
+ 'stop_at_ratio',
+ 'remove_at_ratio',
+ 'move_completed',
+ 'add_paused',
+ 'auto_managed',
+ 'queue_to_top',
+ ],
+ toggle_ids: [
+ 'append_extension_toggle',
+ 'download_location_toggle',
+ 'label_toggle',
+ 'copy_torrent_toggle',
+ 'delete_copy_torrent_toggle',
+ 'seed_mode',
+ ],
+
+ accounts: new Ext.data.ArrayStore({
+ storeId: 'accountStore',
+ id: 0,
+ fields: [
+ {
+ name: 'displayText',
+ type: 'string',
+ },
+ ],
+ }),
+ labels: new Ext.data.ArrayStore({
+ storeId: 'labelStore',
+ id: 0,
+ fields: [
+ {
+ name: 'displayText',
+ type: 'string',
+ },
+ ],
+ }),
+
+ initComponent: function () {
+ Deluge.ux.AutoAdd.AutoAddWindowBase.superclass.initComponent.call(this);
+ this.addButton(_('Cancel'), this.onCancelClick, this);
+
+ this.MainTab = new Deluge.ux.AutoAdd.AutoAddMainPanel();
+ this.OptionsTab = new Deluge.ux.AutoAdd.AutoAddOptionsPanel();
+
+ this.form = this.add({
+ xtype: 'form',
+ baseCls: 'x-plain',
+ bodyStyle: 'padding: 5px',
+ items: [
+ {
+ xtype: 'tabpanel',
+ activeTab: 0,
+ items: [this.MainTab, this.OptionsTab],
+ },
+ ],
+ });
+ },
+
+ onCancelClick: function () {
+ this.hide();
+ },
+
+ getOptions: function () {
+ var options = {};
+
+ options['enabled'] = Ext.getCmp('enabled').getValue();
+ options['path'] = Ext.getCmp('path').getValue();
+ options['download_location'] =
+ Ext.getCmp('download_location').getValue();
+ options['move_completed_path'] = Ext.getCmp(
+ 'move_completed_path'
+ ).getValue();
+ options['copy_torrent'] = Ext.getCmp('copy_torrent').getValue();
+
+ options['label'] = Ext.getCmp('label').getValue();
+ options['append_extension'] = Ext.getCmp('append_extension').getValue();
+ options['owner'] = Ext.getCmp('owner').getValue();
+
+ this.toggle_ids.forEach(function (toggle_id) {
+ options[toggle_id] = Ext.getCmp(toggle_id).getValue();
+ });
+ this.spin_ids.forEach(function (spin_id) {
+ options[spin_id] = Ext.getCmp(spin_id).getValue();
+ options[spin_id + '_toggle'] = Ext.getCmp(
+ spin_id + '_toggle'
+ ).getValue();
+ });
+ this.spin_int_ids.forEach(function (spin_int_id) {
+ options[spin_int_id] = Ext.getCmp(spin_int_id).getValue();
+ options[spin_int_id + '_toggle'] = Ext.getCmp(
+ spin_int_id + '_toggle'
+ ).getValue();
+ });
+ this.chk_ids.forEach(function (chk_id) {
+ options[chk_id] = Ext.getCmp(chk_id).getValue();
+ options[chk_id + '_toggle'] = Ext.getCmp(
+ chk_id + '_toggle'
+ ).getValue();
+ });
+
+ if (
+ options['copy_torrent_toggle'] &&
+ options['path'] === options['copy_torrent']
+ ) {
+ throw _(
+ '"Watch Folder" directory and "Copy of .torrent' +
+ ' files to" directory cannot be the same!'
+ );
+ }
+
+ return options;
+ },
+
+ loadOptions: function (options) {
+ /*
+ * Populate all available options data to the UI
+ */
+ var value;
+
+ if (options === undefined) {
+ options = {};
+ }
+ Ext.getCmp('enabled').setValue(
+ options['enabled'] !== undefined ? options['enabled'] : true
+ );
+ Ext.getCmp('isnt_append_extension').setValue(true);
+ Ext.getCmp('append_extension_toggle').setValue(
+ options['append_extension_toggle'] !== undefined
+ ? options['append_extension_toggle']
+ : false
+ );
+ Ext.getCmp('append_extension').setValue(
+ options['append_extension'] !== undefined
+ ? options['append_extension']
+ : '.added'
+ );
+ Ext.getCmp('download_location_toggle').setValue(
+ options['download_location_toggle'] !== undefined
+ ? options['download_location_toggle']
+ : false
+ );
+ Ext.getCmp('copy_torrent_toggle').setValue(
+ options['copy_torrent_toggle'] !== undefined
+ ? options['copy_torrent_toggle']
+ : false
+ );
+ Ext.getCmp('delete_copy_torrent_toggle').setValue(
+ options['delete_copy_torrent_toggle'] !== undefined
+ ? options['delete_copy_torrent_toggle']
+ : false
+ );
+
+ value =
+ options['seed_mode'] !== undefined ? options['seed_mode'] : false;
+ Ext.getCmp('seed_mode').setValue(value);
+
+ this.accounts.removeAll(true);
+ this.labels.removeAll(true);
+ Ext.getCmp('owner').store = this.accounts;
+ Ext.getCmp('label').store = this.labels;
+ Ext.getCmp('label').setValue(
+ options['label'] !== undefined ? options['label'] : ''
+ );
+ Ext.getCmp('label_toggle').setValue(
+ options['label_toggle'] !== undefined
+ ? options['label_toggle']
+ : false
+ );
+
+ this.spin_ids.forEach(function (spin_id) {
+ Ext.getCmp(spin_id).setValue(
+ options[spin_id] !== undefined ? options[spin_id] : 0
+ );
+ Ext.getCmp(spin_id + '_toggle').setValue(
+ options[spin_id + '_toggle'] !== undefined
+ ? options[spin_id + '_toggle']
+ : false
+ );
+ });
+ this.chk_ids.forEach(function (chk_id) {
+ Ext.getCmp(chk_id).setValue(
+ options[chk_id] !== undefined ? options[chk_id] : true
+ );
+ Ext.getCmp(chk_id + '_toggle').setValue(
+ options[chk_id + '_toggle'] !== undefined
+ ? options[chk_id + '_toggle']
+ : false
+ );
+ });
+ value =
+ options['add_paused'] !== undefined ? options['add_paused'] : true;
+ if (!value) {
+ Ext.getCmp('not_add_paused').setValue(true);
+ }
+ value =
+ options['queue_to_top'] !== undefined
+ ? options['queue_to_top']
+ : true;
+ if (!value) {
+ Ext.getCmp('not_queue_to_top').setValue(true);
+ }
+ value =
+ options['auto_managed'] !== undefined
+ ? options['auto_managed']
+ : true;
+ if (!value) {
+ Ext.getCmp('not_auto_managed').setValue(true);
+ }
+ [
+ 'move_completed_path',
+ 'path',
+ 'download_location',
+ 'copy_torrent',
+ ].forEach(function (field) {
+ value = options[field] !== undefined ? options[field] : '';
+ Ext.getCmp(field).setValue(value);
+ });
+
+ if (Object.keys(options).length === 0) {
+ deluge.client.core.get_config({
+ success: function (config) {
+ var value;
+ Ext.getCmp('download_location').setValue(
+ options['download_location'] !== undefined
+ ? options['download_location']
+ : config['download_location']
+ );
+ value =
+ options['move_completed_toggle'] !== undefined
+ ? options['move_completed_toggle']
+ : config['move_completed'];
+ if (value) {
+ Ext.getCmp('move_completed_toggle').setValue(
+ options['move_completed_toggle'] !== undefined
+ ? options['move_completed_toggle']
+ : false
+ );
+ Ext.getCmp('move_completed_path').setValue(
+ options['move_completed_path'] !== undefined
+ ? options['move_completed_path']
+ : config['move_completed_path']
+ );
+ }
+ value =
+ options['copy_torrent_toggle'] !== undefined
+ ? options['copy_torrent_toggle']
+ : config['copy_torrent_file'];
+ if (value) {
+ Ext.getCmp('copy_torrent_toggle').setValue(true);
+ Ext.getCmp('copy_torrent').setValue(
+ options['copy_torrent'] !== undefined
+ ? options['copy_torrent']
+ : config['torrentfiles_location']
+ );
+ }
+ value =
+ options['delete_copy_torrent_toggle'] !== undefined
+ ? options['copy_torrent_toggle']
+ : config['del_copy_torrent_file'];
+ if (value) {
+ Ext.getCmp('delete_copy_torrent_toggle').setValue(true);
+ }
+ },
+ });
+ }
+
+ deluge.client.core.get_enabled_plugins({
+ success: function (plugins) {
+ if (plugins !== undefined && plugins.indexOf('Label') > -1) {
+ this.MainTab.LabelFset.setVisible(true);
+ deluge.client.label.get_labels({
+ success: function (labels) {
+ for (
+ var index = 0;
+ index < labels.length;
+ index++
+ ) {
+ labels[index] = [labels[index]];
+ }
+ this.labels.loadData(labels, false);
+ },
+ failure: function (failure) {
+ console.error(failure);
+ },
+ scope: this,
+ });
+ } else {
+ this.MainTab.LabelFset.setVisible(false);
+ }
+ },
+ scope: this,
+ });
+
+ var me = this;
+
+ function on_accounts(accounts, owner) {
+ for (var index = 0; index < accounts.length; index++) {
+ accounts[index] = [accounts[index]['username']];
+ }
+ me.accounts.loadData(accounts, false);
+ Ext.getCmp('owner').setValue(owner).enable();
+ }
+
+ function on_accounts_failure(failure) {
+ deluge.client.autoadd.get_auth_user({
+ success: function (user) {
+ me.accounts.loadData([[user]], false);
+ Ext.getCmp('owner').setValue(user).disable(true);
+ },
+ scope: this,
+ });
+ }
+
+ deluge.client.autoadd.is_admin_level({
+ success: function (is_admin) {
+ if (is_admin) {
+ deluge.client.core.get_known_accounts({
+ success: function (accounts) {
+ deluge.client.autoadd.get_auth_user({
+ success: function (user) {
+ on_accounts(
+ accounts,
+ options['owner'] !== undefined
+ ? options['owner']
+ : user
+ );
+ },
+ scope: this,
+ });
+ },
+ failure: on_accounts_failure,
+ scope: this,
+ });
+ } else {
+ on_accounts_failure(null);
+ }
+ },
+ scope: this,
+ });
+ },
+});
+
+/**
+ * @class Deluge.ux.AutoAdd.EditAutoAddCommandWindow
+ * @extends Deluge.ux.AutoAdd.AutoAddWindowBase
+ */
+Deluge.ux.AutoAdd.EditAutoAddCommandWindow = Ext.extend(
+ Deluge.ux.AutoAdd.AutoAddWindowBase,
+ {
+ title: _('Edit Watch Folder'),
+
+ initComponent: function () {
+ Deluge.ux.AutoAdd.EditAutoAddCommandWindow.superclass.initComponent.call(
+ this
+ );
+ this.addButton(_('Save'), this.onSaveClick, this);
+ this.addEvents({
+ watchdiredit: true,
+ });
+ },
+
+ show: function (watchdir_id, options) {
+ Deluge.ux.AutoAdd.EditAutoAddCommandWindow.superclass.show.call(
+ this
+ );
+ this.watchdir_id = watchdir_id;
+ this.loadOptions(options);
+ },
+
+ onSaveClick: function () {
+ try {
+ var options = this.getOptions();
+ deluge.client.autoadd.set_options(this.watchdir_id, options, {
+ success: function () {
+ this.fireEvent('watchdiredit', this, options);
+ },
+ scope: this,
+ });
+ } catch (err) {
+ Ext.Msg.show({
+ title: _('Incompatible Option'),
+ msg: err,
+ buttons: Ext.Msg.OK,
+ scope: this,
+ });
+ }
+
+ this.hide();
+ },
+ }
+);
+
+/**
+ * @class Deluge.ux.AutoAdd.AddAutoAddCommandWindow
+ * @extends Deluge.ux.AutoAdd.AutoAddWindowBase
+ */
+Deluge.ux.AutoAdd.AddAutoAddCommandWindow = Ext.extend(
+ Deluge.ux.AutoAdd.AutoAddWindowBase,
+ {
+ title: _('Add Watch Folder'),
+
+ initComponent: function () {
+ Deluge.ux.AutoAdd.AddAutoAddCommandWindow.superclass.initComponent.call(
+ this
+ );
+ this.addButton(_('Add'), this.onAddClick, this);
+ this.addEvents({
+ watchdiradd: true,
+ });
+ },
+
+ show: function () {
+ Deluge.ux.AutoAdd.AddAutoAddCommandWindow.superclass.show.call(
+ this
+ );
+ this.loadOptions();
+ },
+
+ onAddClick: function () {
+ var options = this.getOptions();
+ deluge.client.autoadd.add(options, {
+ success: function () {
+ this.fireEvent('watchdiradd', this, options);
+ this.hide();
+ },
+ failure: function (err) {
+ const regex = /: (.*\n)\n?\]/m;
+ var error;
+ if ((error = regex.exec(err.error.message)) !== null) {
+ error = error[1];
+ } else {
+ error = err.error.message;
+ }
+ Ext.Msg.show({
+ title: _('Incompatible Option'),
+ msg: error,
+ buttons: Ext.Msg.OK,
+ scope: this,
+ });
+ },
+ scope: this,
+ });
+ },
+ }
+);
diff --git a/deluge/plugins/AutoAdd/deluge_autoadd/data/autoadd_options.ui b/deluge/plugins/AutoAdd/deluge_autoadd/data/autoadd_options.ui
new file mode 100644
index 0000000..f1870f1
--- /dev/null
+++ b/deluge/plugins/AutoAdd/deluge_autoadd/data/autoadd_options.ui
@@ -0,0 +1,1322 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <object class="GtkAdjustment" id="adjustment1">
+ <property name="lower">-1</property>
+ <property name="upper">10000</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">10</property>
+ </object>
+ <object class="GtkAdjustment" id="adjustment2">
+ <property name="lower">-1</property>
+ <property name="upper">10000</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">10</property>
+ </object>
+ <object class="GtkAdjustment" id="adjustment3">
+ <property name="lower">-1</property>
+ <property name="upper">10000</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">10</property>
+ </object>
+ <object class="GtkAdjustment" id="adjustment4">
+ <property name="lower">-1</property>
+ <property name="upper">10000</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">10</property>
+ </object>
+ <object class="GtkAdjustment" id="adjustment5">
+ <property name="upper">100</property>
+ <property name="value">2</property>
+ <property name="step_increment">0.10000000149</property>
+ <property name="page_increment">10</property>
+ </object>
+ <object class="GtkDialog" id="options_dialog">
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Watch Folder Properties</property>
+ <property name="resizable">False</property>
+ <property name="modal">True</property>
+ <property name="type_hint">dialog</property>
+ <signal name="close" handler="on_options_dialog_close" swapped="no"/>
+ <child>
+ <placeholder/>
+ </child>
+ <child internal-child="vbox">
+ <object class="GtkBox" id="dialog-vbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child internal-child="action_area">
+ <object class="GtkButtonBox" id="dialog-action_area1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="opts_cancel_button">
+ <property name="label">gtk-cancel</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="on_opts_cancel" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="opts_add_button">
+ <property name="label">gtk-add</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="on_opts_add" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="opts_apply_button">
+ <property name="label">gtk-apply</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="on_opts_apply" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="vbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkNotebook" id="notebook1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <child>
+ <object class="GtkBox" id="vbox2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="border_width">6</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkFrame" id="frame2">
+ <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="alignment3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="left_padding">12</property>
+ <child>
+ <object class="GtkBox" id="vbox6">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="hbox3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkEntry" id="path_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="tooltip_text" translatable="yes">If a .torrent file is added to this directory,
+it will be added to the session.</property>
+ <property name="invisible_char">●</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFileChooserButton" id="path_chooser">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="tooltip_text" translatable="yes">If a .torrent file is added to this directory,
+it will be added to the session.</property>
+ <property name="action">select-folder</property>
+ <property name="title" translatable="yes">Select A Folder</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="enabled">
+ <property name="label" translatable="yes">Enable this watch folder</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label6">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;b&gt;Watch Folder&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkFrame" id="frame1">
+ <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="alignment1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="left_padding">12</property>
+ <child>
+ <object class="GtkAlignment" id="alignment2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkBox" id="vbox7">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkRadioButton" id="isnt_append_extension">
+ <property name="label" translatable="yes">Delete .torrent after adding</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="tooltip_text" translatable="yes">Once the torrent is added to the session,
+the .torrent will be deleted.</property>
+ <property name="draw_indicator">True</property>
+ <signal name="toggled" handler="on_toggle_toggled" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="hbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkRadioButton" id="append_extension_toggle">
+ <property name="label" translatable="yes">Append extension after adding:</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="tooltip_text" translatable="yes">Once the torrent is added to the session,
+an extension will be appended to the .torrent
+and it will remain in the same directory.</property>
+ <property name="draw_indicator">True</property>
+ <property name="group">isnt_append_extension</property>
+ <signal name="toggled" handler="on_toggle_toggled" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="append_extension">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">•</property>
+ <property name="text" translatable="yes">.added</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkTable" id="table4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="n_rows">2</property>
+ <property name="n_columns">2</property>
+ <child>
+ <object class="GtkRadioButton" id="copy_torrent_toggle">
+ <property name="label" translatable="yes">Copy of .torrent files to:</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="tooltip_text" translatable="yes">Once the torrent is added to the session,
+the .torrent will copied to the chosen directory
+and deleted from the watch folder.</property>
+ <property name="draw_indicator">True</property>
+ <property name="group">isnt_append_extension</property>
+ <signal name="toggled" handler="on_toggle_toggled" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="hbox7">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkEntry" id="copy_torrent_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">•</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFileChooserButton" id="copy_torrent_chooser">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="action">select-folder</property>
+ <property name="title" translatable="yes">Select A Folder</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="delete_copy_torrent_toggle">
+ <property name="label" translatable="yes">Delete copy of torrent file on remove</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="has_tooltip">True</property>
+ <property name="tooltip_text" translatable="yes">Once the torrent is deleted from the session,
+also delete the .torrent file used to add it.</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="right_attach">2</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_padding">15</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;b&gt;Torrent File Action&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFrame" id="frame3">
+ <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="alignment4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="left_padding">12</property>
+ <child>
+ <object class="GtkBox" id="vbox3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkCheckButton" id="download_location_toggle">
+ <property name="label" translatable="yes">Set download folder</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="tooltip_text" translatable="yes">This folder will be where the torrent data is downloaded to.</property>
+ <property name="use_underline">True</property>
+ <property name="draw_indicator">True</property>
+ <signal name="toggled" handler="on_toggle_toggled" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="hbox4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkEntry" id="download_location_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">●</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFileChooserButton" id="download_location_chooser">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="action">select-folder</property>
+ <property name="title" translatable="yes">Select A Folder</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label7">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;b&gt;Download Folder&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">False</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFrame" id="frame4">
+ <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="alignment6">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="left_padding">12</property>
+ <child>
+ <object class="GtkBox" id="vbox4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkCheckButton" id="move_completed_toggle">
+ <property name="label" translatable="yes">Set move completed folder</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="draw_indicator">True</property>
+ <signal name="toggled" handler="on_toggle_toggled" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="hbox5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkEntry" id="move_completed_path_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">●</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFileChooserButton" id="move_completed_path_chooser">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="action">select-folder</property>
+ <property name="title" translatable="yes">Select A Folder</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="move_completed">
+ <property name="sensitive">False</property>
+ <property name="can_focus">False</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label8">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;b&gt;Move Completed&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">False</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFrame" id="label_frame">
+ <property name="can_focus">False</property>
+ <property name="label_xalign">0</property>
+ <property name="shadow_type">none</property>
+ <child>
+ <object class="GtkAlignment" id="alignment15">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="left_padding">12</property>
+ <child>
+ <object class="GtkBox" id="hbox11">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkCheckButton" id="label_toggle">
+ <property name="label" translatable="yes">Label: </property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="draw_indicator">True</property>
+ <signal name="toggled" handler="on_toggle_toggled" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBox" id="label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="has_entry">True</property>
+ <child internal-child="entry">
+ <object class="GtkEntry" id="combobox-entry1">
+ <property name="can_focus">False</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label17">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;b&gt;Label&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">5</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="label4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Main</property>
+ </object>
+ <packing>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="vbox5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="border_width">6</property>
+ <property name="spacing">5</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkFrame" id="OwnerFrame">
+ <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="alignment5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="left_padding">12</property>
+ <child>
+ <object class="GtkComboBox" id="OwnerCombobox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="has_tooltip">True</property>
+ <property name="tooltip_text" translatable="yes">The user selected here will be the owner of the torrent.</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;b&gt;Owner&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFrame" id="frame5">
+ <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="alignment11">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="left_padding">12</property>
+ <child>
+ <object class="GtkTable" id="table1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="border_width">3</property>
+ <property name="n_rows">4</property>
+ <property name="n_columns">3</property>
+ <property name="column_spacing">2</property>
+ <property name="row_spacing">4</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="max_upload_speed_toggle">
+ <property name="label" translatable="yes">Max Upload Speed:</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="draw_indicator">True</property>
+ <signal name="toggled" handler="on_toggle_toggled" swapped="no"/>
+ </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"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="max_connections_toggle">
+ <property name="label" translatable="yes">Max Connections:</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="draw_indicator">True</property>
+ <signal name="toggled" handler="on_toggle_toggled" swapped="no"/>
+ </object>
+ <packing>
+ <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>
+ <child>
+ <object class="GtkCheckButton" id="max_upload_slots_toggle">
+ <property name="label" translatable="yes">Max Upload Slots:</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="draw_indicator">True</property>
+ <signal name="toggled" handler="on_toggle_toggled" swapped="no"/>
+ </object>
+ <packing>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="max_download_speed">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="adjustment">adjustment1</property>
+ <property name="climb_rate">1</property>
+ <property name="digits">1</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="max_upload_speed">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="adjustment">adjustment2</property>
+ <property name="climb_rate">1</property>
+ <property name="digits">1</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="GtkSpinButton" id="max_connections">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="adjustment">adjustment3</property>
+ <property name="climb_rate">1</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="max_upload_slots">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="adjustment">adjustment4</property>
+ <property name="climb_rate">1</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label14">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xpad">5</property>
+ <property name="label" translatable="yes">KiB/s</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label15">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xpad">5</property>
+ <property name="label" translatable="yes">KiB/s</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="max_download_speed_toggle">
+ <property name="label" translatable="yes">Max Download Speed:</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="draw_indicator">True</property>
+ <signal name="toggled" handler="on_toggle_toggled" swapped="no"/>
+ </object>
+ <packing>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;b&gt;Bandwidth&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFrame" id="frame6">
+ <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="alignment12">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="left_padding">12</property>
+ <child>
+ <object class="GtkTable" id="table2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="n_rows">6</property>
+ <property name="n_columns">3</property>
+ <property name="column_spacing">2</property>
+ <property name="row_spacing">4</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkAlignment" id="alignment13">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkCheckButton" id="stop_at_ratio_toggle">
+ <property name="label" translatable="yes">Stop seed at ratio:</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="draw_indicator">True</property>
+ <signal name="toggled" handler="on_toggle_toggled" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ <property name="x_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkAlignment" id="alignment14">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="yalign">0</property>
+ <property name="left_padding">12</property>
+ <child>
+ <object class="GtkCheckButton" id="remove_at_ratio">
+ <property name="label" translatable="yes">Remove at ratio</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="top_attach">4</property>
+ <property name="bottom_attach">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="auto_managed_toggle">
+ <property name="label" translatable="yes">Auto Managed:</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="draw_indicator">True</property>
+ <signal name="toggled" handler="on_toggle_toggled" swapped="no"/>
+ </object>
+ <packing>
+ <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>
+ <child>
+ <object class="GtkCheckButton" id="remove_at_ratio_toggle">
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">4</property>
+ <property name="bottom_attach">5</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="stop_ratio_toggle">
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">4</property>
+ <property name="bottom_attach">5</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="stop_ratio">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">●</property>
+ <property name="adjustment">adjustment5</property>
+ <property name="climb_rate">1</property>
+ <property name="digits">1</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="auto_managed_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <object class="GtkRadioButton" id="auto_managed">
+ <property name="label">gtk-yes</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_stock">True</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRadioButton" id="isnt_auto_managed">
+ <property name="label">gtk-no</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_stock">True</property>
+ <property name="draw_indicator">True</property>
+ <property name="group">auto_managed</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <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">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="stop_at_ratio">
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="add_paused_toggle">
+ <property name="label" translatable="yes">Add Paused:</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ <signal name="toggled" handler="on_toggle_toggled" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="add_paused_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <object class="GtkRadioButton" id="add_paused">
+ <property name="label">gtk-yes</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_stock">True</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRadioButton" id="isnt_add_paused">
+ <property name="label">gtk-no</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_stock">True</property>
+ <property name="draw_indicator">True</property>
+ <property name="group">add_paused</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="queue_to_top_toggle">
+ <property name="label" translatable="yes">Queue to:</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ <signal name="toggled" handler="on_toggle_toggled" swapped="no"/>
+ </object>
+ <packing>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="hbox2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <object class="GtkRadioButton" id="queue_to_top">
+ <property name="label" translatable="yes">Top</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRadioButton" id="isnt_queue_to_top">
+ <property name="label" translatable="yes">Bottom</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ <property name="group">queue_to_top</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </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>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="seed_mode">
+ <property name="label" translatable="yes">Skip File Hash Check</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="top_attach">5</property>
+ <property name="bottom_attach">6</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="label16">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;b&gt;Queue&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="label5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Options</property>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHButtonBox" id="hbuttonbox2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="0">opts_cancel_button</action-widget>
+ <action-widget response="0">opts_add_button</action-widget>
+ <action-widget response="0">opts_apply_button</action-widget>
+ </action-widgets>
+ </object>
+</interface>
diff --git a/deluge/plugins/AutoAdd/deluge_autoadd/data/autoadd_options/main_tab.js b/deluge/plugins/AutoAdd/deluge_autoadd/data/autoadd_options/main_tab.js
new file mode 100644
index 0000000..f685ff2
--- /dev/null
+++ b/deluge/plugins/AutoAdd/deluge_autoadd/data/autoadd_options/main_tab.js
@@ -0,0 +1,304 @@
+/**
+ * Script: main_tab.js
+ * The client-side javascript code for the AutoAdd plugin.
+ *
+ * Copyright (C) 2009 GazpachoKing <chase.sterling@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.AutoAdd');
+
+/**
+ * @class Deluge.ux.AutoAdd.AutoAddMainPanel
+ * @extends Ext.Panel
+ */
+Deluge.ux.AutoAdd.AutoAddMainPanel = Ext.extend(Ext.Panel, {
+ id: 'main_tab_panel',
+ title: _('Main'),
+
+ initComponent: function () {
+ Deluge.ux.AutoAdd.AutoAddMainPanel.superclass.initComponent.call(this);
+ this.watchFolderFset = new Ext.form.FieldSet({
+ xtype: 'fieldset',
+ border: false,
+ title: _('Watch Folder'),
+ defaultType: 'textfield',
+ style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
+ width: '85%',
+ labelWidth: 1,
+ items: [
+ {
+ xtype: 'textfield',
+ id: 'path',
+ hideLabel: true,
+ width: 304,
+ },
+ {
+ hideLabel: true,
+ id: 'enabled',
+ xtype: 'checkbox',
+ boxLabel: _('Enable this watch folder'),
+ checked: true,
+ },
+ ],
+ });
+
+ this.torrentActionFset = new Ext.form.FieldSet({
+ xtype: 'fieldset',
+ border: false,
+ title: _('Torrent File Action'),
+ style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
+ width: '85%',
+ labelWidth: 1,
+ defaults: {
+ style: 'margin-bottom: 2px',
+ },
+ items: [
+ {
+ xtype: 'radiogroup',
+ columns: 1,
+ items: [
+ {
+ xtype: 'radio',
+ name: 'torrent_action',
+ id: 'isnt_append_extension',
+ boxLabel: _('Delete .torrent after adding'),
+ checked: true,
+ hideLabel: true,
+ listeners: {
+ check: function (cb, newValue) {
+ if (newValue) {
+ Ext.getCmp(
+ 'append_extension'
+ ).setDisabled(newValue);
+ Ext.getCmp('copy_torrent').setDisabled(
+ newValue
+ );
+ Ext.getCmp(
+ 'delete_copy_torrent_toggle'
+ ).setDisabled(newValue);
+ }
+ },
+ },
+ },
+ {
+ xtype: 'container',
+ layout: 'hbox',
+ hideLabel: true,
+ items: [
+ {
+ xtype: 'radio',
+ name: 'torrent_action',
+ id: 'append_extension_toggle',
+ boxLabel: _(
+ 'Append extension after adding:'
+ ),
+ hideLabel: true,
+ listeners: {
+ check: function (cb, newValue) {
+ if (newValue) {
+ Ext.getCmp(
+ 'append_extension'
+ ).setDisabled(!newValue);
+ Ext.getCmp(
+ 'copy_torrent'
+ ).setDisabled(newValue);
+ Ext.getCmp(
+ 'delete_copy_torrent_toggle'
+ ).setDisabled(newValue);
+ }
+ },
+ },
+ },
+ {
+ xtype: 'textfield',
+ id: 'append_extension',
+ hideLabel: true,
+ disabled: true,
+ style: 'margin-left: 2px',
+ width: 112,
+ },
+ ],
+ },
+ {
+ xtype: 'container',
+ hideLabel: true,
+ items: [
+ {
+ xtype: 'container',
+ layout: 'hbox',
+ hideLabel: true,
+ items: [
+ {
+ xtype: 'radio',
+ name: 'torrent_action',
+ id: 'copy_torrent_toggle',
+ boxLabel: _(
+ 'Copy of .torrent files to:'
+ ),
+ hideLabel: true,
+ listeners: {
+ check: function (cb, newValue) {
+ if (newValue) {
+ Ext.getCmp(
+ 'append_extension'
+ ).setDisabled(newValue);
+ Ext.getCmp(
+ 'copy_torrent'
+ ).setDisabled(
+ !newValue
+ );
+ Ext.getCmp(
+ 'delete_copy_torrent_toggle'
+ ).setDisabled(
+ !newValue
+ );
+ }
+ },
+ },
+ },
+ {
+ xtype: 'textfield',
+ id: 'copy_torrent',
+ hideLabel: true,
+ disabled: true,
+ style: 'margin-left: 2px',
+ width: 152,
+ },
+ ],
+ },
+ {
+ xtype: 'checkbox',
+ id: 'delete_copy_torrent_toggle',
+ boxLabel: _(
+ 'Delete copy of torrent file on remove'
+ ),
+ style: 'margin-left: 10px',
+ disabled: true,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ });
+
+ this.downloadFolderFset = new Ext.form.FieldSet({
+ xtype: 'fieldset',
+ border: false,
+ title: _('Download Folder'),
+ defaultType: 'textfield',
+ style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
+ width: '85%',
+ labelWidth: 1,
+ items: [
+ {
+ hideLabel: true,
+ id: 'download_location_toggle',
+ xtype: 'checkbox',
+ boxLabel: _('Set download folder'),
+ listeners: {
+ check: function (cb, checked) {
+ Ext.getCmp('download_location').setDisabled(
+ !checked
+ );
+ },
+ },
+ },
+ {
+ xtype: 'textfield',
+ id: 'download_location',
+ hideLabel: true,
+ width: 304,
+ disabled: true,
+ },
+ ],
+ });
+
+ this.moveCompletedFset = new Ext.form.FieldSet({
+ xtype: 'fieldset',
+ border: false,
+ title: _('Move Completed'),
+ defaultType: 'textfield',
+ style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
+ width: '85%',
+ labelWidth: 1,
+ items: [
+ {
+ hideLabel: true,
+ id: 'move_completed_toggle',
+ xtype: 'checkbox',
+ boxLabel: _('Set move completed folder'),
+ listeners: {
+ check: function (cb, checked) {
+ Ext.getCmp('move_completed_path').setDisabled(
+ !checked
+ );
+ },
+ },
+ },
+ {
+ xtype: 'textfield',
+ id: 'move_completed_path',
+ hideLabel: true,
+ width: 304,
+ disabled: true,
+ },
+ ],
+ });
+
+ this.LabelFset = new Ext.form.FieldSet({
+ xtype: 'fieldset',
+ border: false,
+ title: _('Label'),
+ defaultType: 'textfield',
+ style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 3px;',
+ //width: '85%',
+ labelWidth: 1,
+ //hidden: true,
+ items: [
+ {
+ xtype: 'container',
+ layout: 'hbox',
+ hideLabel: true,
+ items: [
+ {
+ hashLabel: false,
+ id: 'label_toggle',
+ xtype: 'checkbox',
+ boxLabel: _('Label:'),
+ listeners: {
+ check: function (cb, checked) {
+ Ext.getCmp('label').setDisabled(!checked);
+ },
+ },
+ },
+ {
+ xtype: 'combo',
+ id: 'label',
+ hideLabel: true,
+ //width: 220,
+ width: 254,
+ disabled: true,
+ style: 'margin-left: 2px',
+ mode: 'local',
+ valueField: 'displayText',
+ displayField: 'displayText',
+ },
+ ],
+ },
+ ],
+ });
+
+ this.add([
+ this.watchFolderFset,
+ this.torrentActionFset,
+ this.downloadFolderFset,
+ this.moveCompletedFset,
+ this.LabelFset,
+ ]);
+ },
+});
diff --git a/deluge/plugins/AutoAdd/deluge_autoadd/data/autoadd_options/options_tab.js b/deluge/plugins/AutoAdd/deluge_autoadd/data/autoadd_options/options_tab.js
new file mode 100644
index 0000000..4ce030e
--- /dev/null
+++ b/deluge/plugins/AutoAdd/deluge_autoadd/data/autoadd_options/options_tab.js
@@ -0,0 +1,302 @@
+/**
+ * Script: options_tab.js
+ * The client-side javascript code for the AutoAdd plugin.
+ *
+ * Copyright (C) 2009 GazpachoKing <chase.sterling@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.AutoAdd');
+
+/**
+ * @class Deluge.ux.AutoAdd.AutoAddOptionsPanel
+ * @extends Ext.Panel
+ */
+Deluge.ux.AutoAdd.AutoAddOptionsPanel = Ext.extend(Ext.Panel, {
+ id: 'options_tab_panel',
+ title: _('Options'),
+
+ initComponent: function () {
+ Deluge.ux.AutoAdd.AutoAddOptionsPanel.superclass.initComponent.call(
+ this
+ );
+ var maxDownload = {
+ idCheckbox: 'max_download_speed_toggle',
+ labelCheckbox: 'Max Download Speed (KiB/s):',
+ idSpinner: 'max_download_speed',
+ decimalPrecision: 1,
+ };
+ var maxUploadSpeed = {
+ idCheckbox: 'max_upload_speed_toggle',
+ labelCheckbox: 'Max upload Speed (KiB/s):',
+ idSpinner: 'max_upload_speed',
+ decimalPrecision: 1,
+ };
+ var maxConnections = {
+ idCheckbox: 'max_connections_toggle',
+ labelCheckbox: 'Max Connections::',
+ idSpinner: 'max_connections',
+ decimalPrecision: 0,
+ };
+ var maxUploadSlots = {
+ idCheckbox: 'max_upload_slots_toggle',
+ labelCheckbox: 'Max Upload Slots:',
+ idSpinner: 'max_upload_slots',
+ decimalPrecision: 0,
+ };
+ // queue data
+ var addPause = {
+ idCheckbox: 'add_paused_toggle',
+ labelCheckbox: 'Add Pause:',
+ nameRadio: 'add_paused',
+ labelRadio: {
+ yes: 'Yes',
+ no: 'No',
+ },
+ };
+ var queueTo = {
+ idCheckbox: 'queue_to_top_toggle',
+ labelCheckbox: 'Queue To:',
+ nameRadio: 'queue_to_top',
+ labelRadio: {
+ yes: 'Top',
+ no: 'Bottom',
+ },
+ };
+ var autoManaged = {
+ idCheckbox: 'auto_managed_toggle',
+ labelCheckbox: 'Auto Managed:',
+ nameRadio: 'auto_managed',
+ labelRadio: {
+ yes: 'Yes',
+ no: 'No',
+ },
+ };
+
+ this.ownerFset = new Ext.form.FieldSet({
+ xtype: 'fieldset',
+ border: false,
+ title: _('Owner'),
+ style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
+ //width: '85%',
+ labelWidth: 1,
+ items: [
+ {
+ xtype: 'combo',
+ id: 'owner',
+ hideLabel: true,
+ width: 312,
+ mode: 'local',
+ valueField: 'displayText',
+ displayField: 'displayText',
+ },
+ ],
+ });
+
+ this.bandwidthFset = new Ext.form.FieldSet({
+ xtype: 'fieldset',
+ border: false,
+ title: _('Bandwidth'),
+ style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
+ //width: '85%',
+ labelWidth: 1,
+ defaults: {
+ style: 'margin-bottom: 5px',
+ },
+ });
+ this.bandwidthFset.add(this._getBandwidthContainer(maxDownload));
+ this.bandwidthFset.add(this._getBandwidthContainer(maxUploadSpeed));
+ this.bandwidthFset.add(this._getBandwidthContainer(maxConnections));
+ this.bandwidthFset.add(this._getBandwidthContainer(maxUploadSlots));
+
+ this.queueFset = new Ext.form.FieldSet({
+ xtype: 'fieldset',
+ border: false,
+ title: _('Queue'),
+ style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
+ //width: '85%',
+ labelWidth: 1,
+ defaults: {
+ style: 'margin-bottom: 5px',
+ },
+ items: [
+ {
+ xtype: 'container',
+ layout: 'hbox',
+ hideLabel: true,
+ },
+ ],
+ });
+ this.queueFset.add(this._getQueueContainer(addPause));
+ this.queueFset.add(this._getQueueContainer(queueTo));
+ this.queueFset.add(this._getQueueContainer(autoManaged));
+ this.queueFset.add({
+ xtype: 'container',
+ hideLabel: true,
+ items: [
+ {
+ xtype: 'container',
+ layout: 'hbox',
+ hideLabel: true,
+ items: [
+ {
+ xtype: 'checkbox',
+ id: 'stop_at_ratio_toggle',
+ boxLabel: _('Stop seed at ratio:'),
+ hideLabel: true,
+ width: 175,
+ listeners: {
+ check: function (cb, checked) {
+ Ext.getCmp('stop_ratio').setDisabled(
+ !checked
+ );
+ Ext.getCmp('remove_at_ratio').setDisabled(
+ !checked
+ );
+ },
+ },
+ },
+ {
+ xtype: 'spinnerfield',
+ id: 'stop_ratio',
+ hideLabel: true,
+ disabled: true,
+ value: 0.0,
+ minValue: 0.0,
+ maxValue: 100.0,
+ decimalPrecision: 1,
+ incrementValue: 0.1,
+ style: 'margin-left: 2px',
+ width: 100,
+ },
+ ],
+ },
+ {
+ xtype: 'container',
+ layout: 'hbox',
+ hideLabel: true,
+ style: 'margin-left: 10px',
+ items: [
+ {
+ xtype: 'checkbox',
+ id: 'remove_at_ratio',
+ boxLabel: _('Remove at ratio'),
+ disabled: true,
+ checked: true,
+ },
+ {
+ xtype: 'checkbox',
+ id: 'remove_at_ratio_toggle',
+ disabled: true,
+ checked: true,
+ hidden: true,
+ },
+ {
+ xtype: 'checkbox',
+ id: 'stop_ratio_toggle',
+ disabled: true,
+ checked: true,
+ hidden: true,
+ },
+ {
+ xtype: 'checkbox',
+ id: 'stop_ratio_toggle',
+ disabled: true,
+ checked: true,
+ hidden: true,
+ },
+ ],
+ },
+ ],
+ });
+ this.queueFset.add({
+ xtype: 'checkbox',
+ id: 'seed_mode',
+ boxLabel: _('Skip File Hash Check'),
+ hideLabel: true,
+ width: 175,
+ });
+
+ this.add([this.ownerFset, this.bandwidthFset, this.queueFset]);
+ },
+
+ _getBandwidthContainer: function (values) {
+ return new Ext.Container({
+ xtype: 'container',
+ layout: 'hbox',
+ hideLabel: true,
+ items: [
+ {
+ xtype: 'checkbox',
+ hideLabel: true,
+ id: values.idCheckbox,
+ boxLabel: _(values.labelCheckbox),
+ width: 175,
+ listeners: {
+ check: function (cb, checked) {
+ Ext.getCmp(values.idSpinner).setDisabled(!checked);
+ },
+ },
+ },
+ {
+ xtype: 'spinnerfield',
+ id: values.idSpinner,
+ hideLabel: true,
+ disabled: true,
+ minValue: -1,
+ maxValue: 10000,
+ value: 0.0,
+ decimalPrecision: values.decimalPrecision,
+ style: 'margin-left: 2px',
+ width: 100,
+ },
+ ],
+ });
+ },
+
+ _getQueueContainer: function (values) {
+ return new Ext.Container({
+ xtype: 'container',
+ layout: 'hbox',
+ hideLabel: true,
+ items: [
+ {
+ xtype: 'checkbox',
+ hideLabel: true,
+ id: values.idCheckbox,
+ boxLabel: _(values.labelCheckbox),
+ width: 175,
+ listeners: {
+ check: function (cb, checked) {
+ Ext.getCmp(values.nameRadio).setDisabled(!checked);
+ Ext.getCmp('not_' + values.nameRadio).setDisabled(
+ !checked
+ );
+ },
+ },
+ },
+ {
+ xtype: 'radio',
+ name: values.nameRadio,
+ id: values.nameRadio,
+ boxLabel: _(values.labelRadio.yes),
+ hideLabel: true,
+ checked: true,
+ disabled: true,
+ width: 50,
+ },
+ {
+ xtype: 'radio',
+ name: values.nameRadio,
+ id: 'not_' + values.nameRadio,
+ boxLabel: _(values.labelRadio.no),
+ hideLabel: true,
+ disabled: true,
+ },
+ ],
+ });
+ },
+});
diff --git a/deluge/plugins/AutoAdd/deluge_autoadd/data/config.ui b/deluge/plugins/AutoAdd/deluge_autoadd/data/config.ui
new file mode 100644
index 0000000..0e645d3
--- /dev/null
+++ b/deluge/plugins/AutoAdd/deluge_autoadd/data/config.ui
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <object class="GtkWindow" id="prefs_window">
+ <property name="can_focus">False</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkBox" id="hbox9">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkAlignment" id="prefs_box_1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkBox" id="prefs_box">
+ <property name="width_request">340</property>
+ <property name="height_request">390</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="border_width">3</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkFrame" id="frame1">
+ <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="GtkBox" id="watchdirs_vbox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="homogeneous">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;b&gt;Watch Folders:&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHButtonBox" id="hbuttonbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkButton" id="add_button">
+ <property name="label">gtk-add</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">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>
+ <child>
+ <object class="GtkButton" id="remove_button">
+ <property name="label">gtk-remove</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="on_remove_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="edit_button">
+ <property name="label">gtk-edit</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="on_edit_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/deluge/plugins/AutoAdd/deluge_autoadd/gtkui.py b/deluge/plugins/AutoAdd/deluge_autoadd/gtkui.py
new file mode 100644
index 0000000..80fb9fc
--- /dev/null
+++ b/deluge/plugins/AutoAdd/deluge_autoadd/gtkui.py
@@ -0,0 +1,576 @@
+#
+# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
+# 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 os
+
+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.common
+import deluge.component as component
+from deluge.plugins.pluginbase import Gtk3PluginBase
+from deluge.ui.client import client
+from deluge.ui.gtk3 import dialogs
+
+# isort:imports-localfolder
+from .common import get_resource
+
+log = logging.getLogger(__name__)
+
+
+class IncompatibleOption(Exception):
+ pass
+
+
+class OptionsDialog:
+ spin_ids = ['max_download_speed', 'max_upload_speed', 'stop_ratio']
+ spin_int_ids = ['max_upload_slots', 'max_connections']
+ chk_ids = [
+ 'stop_at_ratio',
+ 'remove_at_ratio',
+ 'move_completed',
+ 'add_paused',
+ 'auto_managed',
+ 'queue_to_top',
+ ]
+
+ def __init__(self):
+ self.accounts = Gtk.ListStore(str)
+ self.labels = Gtk.ListStore(str)
+ self.core_config = {}
+
+ def show(self, options=None, watchdir_id=None):
+ if options is None:
+ options = {}
+ self.builder = Gtk.Builder()
+ self.builder.add_from_file(get_resource('autoadd_options.ui'))
+ self.builder.connect_signals(
+ {
+ 'on_opts_add': self.on_add,
+ 'on_opts_apply': self.on_apply,
+ 'on_opts_cancel': self.on_cancel,
+ 'on_options_dialog_close': self.on_cancel,
+ 'on_toggle_toggled': self.on_toggle_toggled,
+ }
+ )
+ self.dialog = self.builder.get_object('options_dialog')
+ self.dialog.set_transient_for(component.get('Preferences').pref_dialog)
+
+ if watchdir_id:
+ # We have an existing watchdir_id, we are editing
+ self.builder.get_object('opts_add_button').hide()
+ self.builder.get_object('opts_apply_button').show()
+ self.watchdir_id = watchdir_id
+ else:
+ # We don't have an id, adding
+ self.builder.get_object('opts_add_button').show()
+ self.builder.get_object('opts_apply_button').hide()
+ self.watchdir_id = None
+
+ self.load_options(options)
+ self.dialog.run()
+
+ def load_options(self, options):
+ self.builder.get_object('enabled').set_active(options.get('enabled', True))
+ self.builder.get_object('append_extension_toggle').set_active(
+ options.get('append_extension_toggle', False)
+ )
+ self.builder.get_object('append_extension').set_text(
+ options.get('append_extension', '.added')
+ )
+ self.builder.get_object('download_location_toggle').set_active(
+ options.get('download_location_toggle', False)
+ )
+ self.builder.get_object('copy_torrent_toggle').set_active(
+ options.get('copy_torrent_toggle', False)
+ )
+ self.builder.get_object('delete_copy_torrent_toggle').set_active(
+ options.get('delete_copy_torrent_toggle', False)
+ )
+ self.builder.get_object('seed_mode').set_active(options.get('seed_mode', False))
+ self.accounts.clear()
+ self.labels.clear()
+ combobox = self.builder.get_object('OwnerCombobox')
+ combobox_render = Gtk.CellRendererText()
+ combobox.pack_start(combobox_render, True)
+ combobox.add_attribute(combobox_render, 'text', 0)
+ combobox.set_model(self.accounts)
+
+ label_widget = self.builder.get_object('label')
+ label_widget.get_child().set_text(options.get('label', ''))
+ label_widget.set_model(self.labels)
+ label_widget.set_entry_text_column(0)
+ self.builder.get_object('label_toggle').set_active(
+ options.get('label_toggle', False)
+ )
+
+ for spin_id in self.spin_ids + self.spin_int_ids:
+ self.builder.get_object(spin_id).set_value(options.get(spin_id, 0))
+ self.builder.get_object(spin_id + '_toggle').set_active(
+ options.get(spin_id + '_toggle', False)
+ )
+ for chk_id in self.chk_ids:
+ self.builder.get_object(chk_id).set_active(bool(options.get(chk_id, True)))
+ self.builder.get_object(chk_id + '_toggle').set_active(
+ options.get(chk_id + '_toggle', False)
+ )
+ if not options.get('add_paused', True):
+ self.builder.get_object('isnt_add_paused').set_active(True)
+ if not options.get('queue_to_top', True):
+ self.builder.get_object('isnt_queue_to_top').set_active(True)
+ if not options.get('auto_managed', True):
+ self.builder.get_object('isnt_auto_managed').set_active(True)
+ for field in [
+ 'move_completed_path',
+ 'path',
+ 'download_location',
+ 'copy_torrent',
+ ]:
+ if client.is_localhost():
+ self.builder.get_object(field + '_chooser').set_current_folder(
+ options.get(field, os.path.expanduser('~'))
+ )
+ self.builder.get_object(field + '_chooser').show()
+ self.builder.get_object(field + '_entry').hide()
+ else:
+ self.builder.get_object(field + '_entry').set_text(
+ options.get(field, '')
+ )
+ self.builder.get_object(field + '_entry').show()
+ self.builder.get_object(field + '_chooser').hide()
+ self.set_sensitive()
+
+ def on_core_config(config):
+ if client.is_localhost():
+ self.builder.get_object('download_location_chooser').set_current_folder(
+ options.get('download_location', config['download_location'])
+ )
+ if options.get('move_completed_toggle', config['move_completed']):
+ self.builder.get_object('move_completed_toggle').set_active(True)
+ self.builder.get_object(
+ 'move_completed_path_chooser'
+ ).set_current_folder(
+ options.get(
+ 'move_completed_path', config['move_completed_path']
+ )
+ )
+ if options.get('copy_torrent_toggle', config['copy_torrent_file']):
+ self.builder.get_object('copy_torrent_toggle').set_active(True)
+ self.builder.get_object('copy_torrent_chooser').set_current_folder(
+ options.get('copy_torrent', config['torrentfiles_location'])
+ )
+ else:
+ self.builder.get_object('download_location_entry').set_text(
+ options.get('download_location', config['download_location'])
+ )
+ if options.get('move_completed_toggle', config['move_completed']):
+ self.builder.get_object('move_completed_toggle').set_active(
+ options.get('move_completed_toggle', False)
+ )
+ self.builder.get_object('move_completed_path_entry').set_text(
+ options.get(
+ 'move_completed_path', config['move_completed_path']
+ )
+ )
+ if options.get('copy_torrent_toggle', config['copy_torrent_file']):
+ self.builder.get_object('copy_torrent_toggle').set_active(True)
+ self.builder.get_object('copy_torrent_entry').set_text(
+ options.get('copy_torrent', config['torrentfiles_location'])
+ )
+
+ if options.get(
+ 'delete_copy_torrent_toggle', config['del_copy_torrent_file']
+ ):
+ self.builder.get_object('delete_copy_torrent_toggle').set_active(True)
+
+ if not options:
+ client.core.get_config().addCallback(on_core_config)
+
+ def on_accounts(accounts, owner):
+ log.debug('Got Accounts')
+ selected_iter = None
+ for account in accounts:
+ acc_iter = self.accounts.append()
+ self.accounts.set_value(acc_iter, 0, account['username'])
+ if account['username'] == owner:
+ selected_iter = acc_iter
+ self.builder.get_object('OwnerCombobox').set_active_iter(selected_iter)
+
+ def on_accounts_failure(failure):
+ log.debug('Failed to get accounts!!! %s', failure)
+ acc_iter = self.accounts.append()
+ self.accounts.set_value(acc_iter, 0, client.get_auth_user())
+ self.builder.get_object('OwnerCombobox').set_active(0)
+ self.builder.get_object('OwnerCombobox').set_sensitive(False)
+
+ def on_labels(labels):
+ log.debug('Got Labels: %s', labels)
+ for label in labels:
+ self.labels.set_value(self.labels.append(), 0, label)
+ label_widget = self.builder.get_object('label')
+ label_widget.set_model(self.labels)
+ label_widget.set_entry_text_column(0)
+
+ def on_failure(failure):
+ log.exception(failure)
+
+ def on_get_enabled_plugins(result):
+ if 'Label' in result:
+ self.builder.get_object('label_frame').show()
+ client.label.get_labels().addCallback(on_labels).addErrback(on_failure)
+ else:
+ self.builder.get_object('label_frame').hide()
+ self.builder.get_object('label_toggle').set_active(False)
+
+ client.core.get_enabled_plugins().addCallback(on_get_enabled_plugins)
+ if client.get_auth_level() == deluge.common.AUTH_LEVEL_ADMIN:
+ client.core.get_known_accounts().addCallback(
+ on_accounts, options.get('owner', client.get_auth_user())
+ ).addErrback(on_accounts_failure)
+ else:
+ acc_iter = self.accounts.append()
+ self.accounts.set_value(acc_iter, 0, client.get_auth_user())
+ self.builder.get_object('OwnerCombobox').set_active(0)
+ self.builder.get_object('OwnerCombobox').set_sensitive(False)
+
+ def set_sensitive(self):
+ maintoggles = [
+ 'download_location',
+ 'append_extension',
+ 'move_completed',
+ 'label',
+ 'max_download_speed',
+ 'max_upload_speed',
+ 'max_connections',
+ 'max_upload_slots',
+ 'add_paused',
+ 'auto_managed',
+ 'stop_at_ratio',
+ 'queue_to_top',
+ 'copy_torrent',
+ ]
+ for maintoggle in maintoggles:
+ self.on_toggle_toggled(self.builder.get_object(maintoggle + '_toggle'))
+
+ def on_toggle_toggled(self, tb):
+ toggle = tb.get_name().replace('_toggle', '')
+ isactive = tb.get_active()
+ if toggle == 'download_location':
+ self.builder.get_object('download_location_chooser').set_sensitive(isactive)
+ self.builder.get_object('download_location_entry').set_sensitive(isactive)
+ elif toggle == 'append_extension':
+ self.builder.get_object('append_extension').set_sensitive(isactive)
+ elif toggle == 'copy_torrent':
+ self.builder.get_object('copy_torrent_entry').set_sensitive(isactive)
+ self.builder.get_object('copy_torrent_chooser').set_sensitive(isactive)
+ self.builder.get_object('delete_copy_torrent_toggle').set_sensitive(
+ isactive
+ )
+ elif toggle == 'move_completed':
+ self.builder.get_object('move_completed_path_chooser').set_sensitive(
+ isactive
+ )
+ self.builder.get_object('move_completed_path_entry').set_sensitive(isactive)
+ self.builder.get_object('move_completed').set_active(isactive)
+ elif toggle == 'label':
+ self.builder.get_object('label').set_sensitive(isactive)
+ elif toggle == 'max_download_speed':
+ self.builder.get_object('max_download_speed').set_sensitive(isactive)
+ elif toggle == 'max_upload_speed':
+ self.builder.get_object('max_upload_speed').set_sensitive(isactive)
+ elif toggle == 'max_connections':
+ self.builder.get_object('max_connections').set_sensitive(isactive)
+ elif toggle == 'max_upload_slots':
+ self.builder.get_object('max_upload_slots').set_sensitive(isactive)
+ elif toggle == 'add_paused':
+ self.builder.get_object('add_paused').set_sensitive(isactive)
+ self.builder.get_object('isnt_add_paused').set_sensitive(isactive)
+ elif toggle == 'queue_to_top':
+ self.builder.get_object('queue_to_top').set_sensitive(isactive)
+ self.builder.get_object('isnt_queue_to_top').set_sensitive(isactive)
+ elif toggle == 'auto_managed':
+ self.builder.get_object('auto_managed').set_sensitive(isactive)
+ self.builder.get_object('isnt_auto_managed').set_sensitive(isactive)
+ elif toggle == 'stop_at_ratio':
+ self.builder.get_object('remove_at_ratio_toggle').set_active(isactive)
+ self.builder.get_object('stop_ratio_toggle').set_active(isactive)
+ self.builder.get_object('stop_at_ratio').set_active(isactive)
+ self.builder.get_object('stop_ratio').set_sensitive(isactive)
+ self.builder.get_object('remove_at_ratio').set_sensitive(isactive)
+
+ def on_apply(self, event=None):
+ try:
+ options = self.generate_opts()
+ client.autoadd.set_options(str(self.watchdir_id), options).addCallbacks(
+ self.on_added, self.on_error_show
+ )
+ except IncompatibleOption as ex:
+ dialogs.ErrorDialog(_('Incompatible Option'), str(ex), self.dialog).run()
+
+ def on_error_show(self, result):
+ d = dialogs.ErrorDialog(_('Error'), result.value.message, self.dialog)
+ result.cleanFailure()
+ d.run()
+
+ def on_added(self, result):
+ self.dialog.destroy()
+
+ def on_add(self, event=None):
+ try:
+ options = self.generate_opts()
+ client.autoadd.add(options).addCallbacks(self.on_added, self.on_error_show)
+ except IncompatibleOption as ex:
+ dialogs.ErrorDialog(_('Incompatible Option'), str(ex), self.dialog).run()
+
+ def on_cancel(self, event=None):
+ self.dialog.destroy()
+
+ def generate_opts(self):
+ # generate options dict based on gtk objects
+ options = {}
+ options['enabled'] = self.builder.get_object('enabled').get_active()
+ if client.is_localhost():
+ options['path'] = self.builder.get_object('path_chooser').get_filename()
+ options['download_location'] = self.builder.get_object(
+ 'download_location_chooser'
+ ).get_filename()
+ options['move_completed_path'] = self.builder.get_object(
+ 'move_completed_path_chooser'
+ ).get_filename()
+ options['copy_torrent'] = self.builder.get_object(
+ 'copy_torrent_chooser'
+ ).get_filename()
+ else:
+ options['path'] = self.builder.get_object('path_entry').get_text()
+ options['download_location'] = self.builder.get_object(
+ 'download_location_entry'
+ ).get_text()
+ options['move_completed_path'] = self.builder.get_object(
+ 'move_completed_path_entry'
+ ).get_text()
+ options['copy_torrent'] = self.builder.get_object(
+ 'copy_torrent_entry'
+ ).get_text()
+
+ options['label'] = (
+ self.builder.get_object('label').get_child().get_text().lower()
+ )
+ options['append_extension'] = self.builder.get_object(
+ 'append_extension'
+ ).get_text()
+ options['owner'] = self.accounts[
+ self.builder.get_object('OwnerCombobox').get_active()
+ ][0]
+
+ for key in [
+ 'append_extension_toggle',
+ 'download_location_toggle',
+ 'label_toggle',
+ 'copy_torrent_toggle',
+ 'delete_copy_torrent_toggle',
+ 'seed_mode',
+ ]:
+ options[key] = self.builder.get_object(key).get_active()
+
+ for spin_id in self.spin_ids:
+ options[spin_id] = self.builder.get_object(spin_id).get_value()
+ options[spin_id + '_toggle'] = self.builder.get_object(
+ spin_id + '_toggle'
+ ).get_active()
+ for spin_int_id in self.spin_int_ids:
+ options[spin_int_id] = self.builder.get_object(
+ spin_int_id
+ ).get_value_as_int()
+ options[spin_int_id + '_toggle'] = self.builder.get_object(
+ spin_int_id + '_toggle'
+ ).get_active()
+ for chk_id in self.chk_ids:
+ options[chk_id] = self.builder.get_object(chk_id).get_active()
+ options[chk_id + '_toggle'] = self.builder.get_object(
+ chk_id + '_toggle'
+ ).get_active()
+
+ if (
+ options['copy_torrent_toggle']
+ and options['path'] == options['copy_torrent']
+ ):
+ raise IncompatibleOption(
+ _(
+ '"Watch Folder" directory and "Copy of .torrent'
+ ' files to" directory cannot be the same!'
+ )
+ )
+ return options
+
+
+class GtkUI(Gtk3PluginBase):
+ def enable(self):
+ self.builder = Gtk.Builder()
+ self.builder.add_from_file(get_resource('config.ui'))
+ self.builder.connect_signals(self)
+ self.opts_dialog = OptionsDialog()
+
+ component.get('PluginManager').register_hook(
+ 'on_apply_prefs', self.on_apply_prefs
+ )
+ component.get('PluginManager').register_hook(
+ 'on_show_prefs', self.on_show_prefs
+ )
+ client.register_event_handler(
+ 'AutoaddOptionsChangedEvent', self.on_options_changed_event
+ )
+
+ self.watchdirs = {}
+
+ vbox = self.builder.get_object('watchdirs_vbox')
+ sw = Gtk.ScrolledWindow()
+ sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
+ sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
+
+ vbox.pack_start(sw, True, True, 0)
+
+ self.store = self.create_model()
+
+ self.treeView = Gtk.TreeView(self.store)
+ self.treeView.connect('cursor-changed', self.on_listitem_activated)
+ self.treeView.connect('row-activated', self.on_edit_button_clicked)
+
+ self.create_columns(self.treeView)
+ sw.add(self.treeView)
+ sw.show_all()
+ component.get('Preferences').add_page(
+ _('AutoAdd'), self.builder.get_object('prefs_box')
+ )
+
+ def disable(self):
+ component.get('Preferences').remove_page(_('AutoAdd'))
+ 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 create_model(self):
+ store = Gtk.ListStore(str, bool, str, str)
+ for watchdir_id, watchdir in self.watchdirs.items():
+ store.append(
+ [
+ watchdir_id,
+ watchdir['enabled'],
+ watchdir.get('owner', 'localclient'),
+ watchdir['path'],
+ ]
+ )
+ return store
+
+ def create_columns(self, treeview):
+ renderer_toggle = Gtk.CellRendererToggle()
+ column = Gtk.TreeViewColumn(
+ _('Active'), renderer_toggle, activatable=1, active=1
+ )
+ column.set_sort_column_id(1)
+ treeview.append_column(column)
+ tt = Gtk.Tooltip()
+ tt.set_text(_('Double-click to toggle'))
+ treeview.set_tooltip_cell(tt, None, None, renderer_toggle)
+
+ renderertext = Gtk.CellRendererText()
+ column = Gtk.TreeViewColumn(_('Owner'), renderertext, text=2)
+ column.set_sort_column_id(2)
+ treeview.append_column(column)
+ tt2 = Gtk.Tooltip()
+ tt2.set_text(_('Double-click to edit'))
+ treeview.set_has_tooltip(True)
+
+ renderertext = Gtk.CellRendererText()
+ column = Gtk.TreeViewColumn(_('Path'), renderertext, text=3)
+ column.set_sort_column_id(3)
+ treeview.append_column(column)
+ tt2 = Gtk.Tooltip()
+ tt2.set_text(_('Double-click to edit'))
+ treeview.set_has_tooltip(True)
+
+ def load_watchdir_list(self):
+ pass
+
+ def add_watchdir_entry(self):
+ pass
+
+ def on_add_button_clicked(self, event=None):
+ # display options_window
+ self.opts_dialog.show()
+
+ def on_remove_button_clicked(self, event=None):
+ tree, tree_id = self.treeView.get_selection().get_selected()
+ watchdir_id = str(self.store.get_value(tree_id, 0))
+ if watchdir_id:
+ client.autoadd.remove(watchdir_id)
+
+ def on_edit_button_clicked(self, event=None, a=None, col=None):
+ tree, tree_id = self.treeView.get_selection().get_selected()
+ watchdir_id = str(self.store.get_value(tree_id, 0))
+ if watchdir_id:
+ if col and col.get_title() == _('Active'):
+ if self.watchdirs[watchdir_id]['enabled']:
+ client.autoadd.disable_watchdir(watchdir_id)
+ else:
+ client.autoadd.enable_watchdir(watchdir_id)
+ else:
+ self.opts_dialog.show(self.watchdirs[watchdir_id], watchdir_id)
+
+ def on_listitem_activated(self, treeview):
+ tree, tree_id = self.treeView.get_selection().get_selected()
+ if tree_id:
+ self.builder.get_object('edit_button').set_sensitive(True)
+ self.builder.get_object('remove_button').set_sensitive(True)
+ else:
+ self.builder.get_object('edit_button').set_sensitive(False)
+ self.builder.get_object('remove_button').set_sensitive(False)
+
+ def on_apply_prefs(self):
+ log.debug('applying prefs for AutoAdd')
+ for watchdir_id, watchdir in self.watchdirs.items():
+ client.autoadd.set_options(watchdir_id, watchdir)
+
+ def on_show_prefs(self):
+ client.autoadd.get_watchdirs().addCallback(self.cb_get_config)
+
+ def on_options_changed_event(self):
+ client.autoadd.get_watchdirs().addCallback(self.cb_get_config)
+
+ def cb_get_config(self, watchdirs):
+ """callback for on show_prefs"""
+ log.trace('Got whatchdirs from core: %s', watchdirs)
+ self.watchdirs = watchdirs or {}
+ self.store.clear()
+ for watchdir_id, watchdir in self.watchdirs.items():
+ self.store.append(
+ [
+ watchdir_id,
+ watchdir['enabled'],
+ watchdir.get('owner', 'localclient'),
+ watchdir['path'],
+ ]
+ )
+ # Workaround for cached glade signal appearing when re-enabling plugin in same session
+ if self.builder.get_object('edit_button'):
+ # Disable the remove and edit buttons, because nothing in the store is selected
+ self.builder.get_object('remove_button').set_sensitive(False)
+ self.builder.get_object('edit_button').set_sensitive(False)
diff --git a/deluge/plugins/AutoAdd/deluge_autoadd/webui.py b/deluge/plugins/AutoAdd/deluge_autoadd/webui.py
new file mode 100644
index 0000000..d328432
--- /dev/null
+++ b/deluge/plugins/AutoAdd/deluge_autoadd/webui.py
@@ -0,0 +1,35 @@
+#
+# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
+# 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('autoadd.js'),
+ get_resource('autoadd_options.js'),
+ get_resource('main_tab.js', True),
+ get_resource('options_tab.js', True),
+ ]
+
+ def enable(self):
+ pass
+
+ def disable(self):
+ pass
diff --git a/deluge/plugins/AutoAdd/setup.py b/deluge/plugins/AutoAdd/setup.py
new file mode 100644
index 0000000..5a01ee9
--- /dev/null
+++ b/deluge/plugins/AutoAdd/setup.py
@@ -0,0 +1,47 @@
+#
+# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
+# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
+# 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__ = 'AutoAdd'
+__author__ = 'Chase Sterling, Pedro Algarvio'
+__author_email__ = 'chase.sterling@gmail.com, pedro@algarvio.me'
+__version__ = '1.8'
+__url__ = 'http://dev.deluge-torrent.org/wiki/Plugins/AutoAdd'
+__license__ = 'GPLv3'
+__description__ = 'Monitors folders for .torrent files.'
+__long_description__ = """"""
+__pkg_data__ = {'deluge_' + __plugin_name__.lower(): ['data/*', '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=find_packages(),
+ package_data=__pkg_data__,
+ entry_points="""
+ [deluge.plugin.core]
+ %s = deluge_%s:CorePlugin
+ [deluge.plugin.gtk3ui]
+ %s = deluge_%s:Gtk3UIPlugin
+ [deluge.plugin.web]
+ %s = deluge_%s:WebUIPlugin
+ """
+ % ((__plugin_name__, __plugin_name__.lower()) * 3),
+)
diff --git a/deluge/plugins/Blocklist/deluge_blocklist/__init__.py b/deluge/plugins/Blocklist/deluge_blocklist/__init__.py
new file mode 100644
index 0000000..40ce1d1
--- /dev/null
+++ b/deluge/plugins/Blocklist/deluge_blocklist/__init__.py
@@ -0,0 +1,33 @@
+#
+# Copyright (C) 2007-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 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/Blocklist/deluge_blocklist/common.py b/deluge/plugins/Blocklist/deluge_blocklist/common.py
new file mode 100644
index 0000000..35b2f87
--- /dev/null
+++ b/deluge/plugins/Blocklist/deluge_blocklist/common.py
@@ -0,0 +1,172 @@
+#
+# 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 functools import wraps
+from sys import exc_info
+
+from pkg_resources import resource_filename
+
+
+def get_resource(filename):
+ return resource_filename(__package__, os.path.join('data', filename))
+
+
+def raises_errors_as(error):
+ """Factory class that returns a decorator which wraps the decorated
+ function to raise all exceptions as the specified error type.
+
+ """
+
+ def decorator(func):
+ """Returns a function which wraps the given func to raise all exceptions as error."""
+
+ @wraps(func)
+ def wrapper(self, *args, **kwargs):
+ """Wraps the function in a try..except block and calls it with the specified args.
+
+ Raises:
+ Any exceptions as error preserving the message and traceback.
+
+ """
+ try:
+ return func(self, *args, **kwargs)
+ except Exception:
+ (value, tb) = exc_info()[1:]
+ raise error(value).with_traceback(tb) from None
+
+ return wrapper
+
+ return decorator
+
+
+def remove_zeros(ip):
+ """Removes unneeded zeros from ip addresses.
+
+ Args:
+ ip (str): The ip address.
+
+ Returns:
+ str: The ip address without the unneeded zeros.
+
+ Example:
+ 000.000.000.003 -> 0.0.0.3
+
+ """
+ return '.'.join([part.lstrip('0').zfill(1) for part in ip.split('.')])
+
+
+class BadIP(Exception):
+ _message = None
+
+ def __init__(self, message):
+ super().__init__(message)
+
+ def __set_message(self, message):
+ self._message = message
+
+ def __get_message(self):
+ return self._message
+
+ message = property(__get_message, __set_message)
+ del __get_message, __set_message
+
+
+class IP:
+ __slots__ = ('q1', 'q2', 'q3', 'q4', '_long')
+
+ def __init__(self, q1, q2, q3, q4):
+ self.q1 = q1
+ self.q2 = q2
+ self.q3 = q3
+ self.q4 = q4
+ self._long = 0
+ for q in self.quadrants():
+ self._long = (self._long << 8) | int(q)
+
+ @property
+ def address(self):
+ return '.'.join([str(q) for q in [self.q1, self.q2, self.q3, self.q4]])
+
+ @property
+ def long(self):
+ return self._long
+
+ @classmethod
+ def parse(cls, ip):
+ try:
+ q1, q2, q3, q4 = (int(q) for q in ip.split('.'))
+ except ValueError:
+ raise BadIP(_('The IP address "%s" is badly formed' % ip))
+ if q1 < 0 or q2 < 0 or q3 < 0 or q4 < 0:
+ raise BadIP(_('The IP address "%s" is badly formed' % ip))
+ elif q1 > 255 or q2 > 255 or q3 > 255 or q4 > 255:
+ raise BadIP(_('The IP address "%s" is badly formed' % ip))
+ return cls(q1, q2, q3, q4)
+
+ def quadrants(self):
+ return (self.q1, self.q2, self.q3, self.q4)
+
+ # def next_ip(self):
+ # (q1, q2, q3, q4) = self.quadrants()
+ # if q4 >= 255:
+ # if q3 >= 255:
+ # if q2 >= 255:
+ # if q1 >= 255:
+ # raise BadIP(_('There is not a next IP address'))
+ # q1 += 1
+ # else:
+ # q2 += 1
+ # else:
+ # q3 += 1
+ # else:
+ # q4 += 1
+ # return IP(q1, q2, q3, q4)
+ #
+ # def previous_ip(self):
+ # (q1, q2, q3, q4) = self.quadrants()
+ # if q4 <= 1:
+ # if q3 <= 1:
+ # if q2 <= 1:
+ # if q1 <= 1:
+ # raise BadIP(_('There is not a previous IP address'))
+ # q1 -= 1
+ # else:
+ # q2 -= 1
+ # else:
+ # q3 -= 1
+ # else:
+ # q4 -= 1
+ # return IP(q1, q2, q3, q4)
+
+ def __lt__(self, other):
+ if isinstance(other, ''.__class__):
+ other = IP.parse(other)
+ return self.long < other.long
+
+ def __gt__(self, other):
+ if isinstance(other, ''.__class__):
+ other = IP.parse(other)
+ return self.long > other.long
+
+ def __eq__(self, other):
+ if isinstance(other, ''.__class__):
+ other = IP.parse(other)
+ return self.long == other.long
+
+ def __repr__(self):
+ return '<{} long={} address="{}">'.format(
+ self.__class__.__name__,
+ self.long,
+ self.address,
+ )
diff --git a/deluge/plugins/Blocklist/deluge_blocklist/core.py b/deluge/plugins/Blocklist/deluge_blocklist/core.py
new file mode 100644
index 0000000..1765767
--- /dev/null
+++ b/deluge/plugins/Blocklist/deluge_blocklist/core.py
@@ -0,0 +1,549 @@
+#
+# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
+# Copyright (C) 2009-2010 John Garland <johnnybg+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 logging
+import os
+import shutil
+import time
+from datetime import datetime, timedelta
+from email.utils import formatdate
+from urllib.parse import urljoin
+
+from twisted.internet import defer, threads
+from twisted.internet.task import LoopingCall
+from twisted.web import error
+
+import deluge.component as component
+import deluge.configmanager
+from deluge.common import is_url
+from deluge.core.rpcserver import export
+from deluge.httpdownloader import download_file
+from deluge.plugins.pluginbase import CorePluginBase
+
+from .common import IP, BadIP
+from .detect import UnknownFormatError, create_reader, detect_compression, detect_format
+from .readers import ReaderParseError
+
+# TODO: check return values for deferred callbacks
+# TODO: review class attributes for redundancy
+
+log = logging.getLogger(__name__)
+
+DEFAULT_PREFS = {
+ 'url': '',
+ 'load_on_start': False,
+ 'check_after_days': 4,
+ 'list_compression': '',
+ 'list_type': '',
+ 'last_update': 0.0,
+ 'list_size': 0,
+ 'timeout': 180,
+ 'try_times': 3,
+ 'whitelisted': [],
+}
+
+# Constants
+ALLOW_RANGE = 0
+BLOCK_RANGE = 1
+
+
+class Core(CorePluginBase):
+ def enable(self):
+ log.debug('Blocklist: Plugin enabled...')
+
+ self.is_url = True
+ self.is_downloading = False
+ self.is_importing = False
+ self.has_imported = False
+ self.up_to_date = False
+ self.need_to_resume_session = False
+ self.num_whited = 0
+ self.num_blocked = 0
+ self.file_progress = 0.0
+
+ self.core = component.get('Core')
+ self.config = deluge.configmanager.ConfigManager(
+ 'blocklist.conf', DEFAULT_PREFS
+ )
+ if 'whitelisted' not in self.config:
+ self.config['whitelisted'] = []
+
+ self.reader = create_reader(
+ self.config['list_type'], self.config['list_compression']
+ )
+
+ if not isinstance(self.config['last_update'], float):
+ self.config.config['last_update'] = 0.0
+
+ update_now = False
+ if self.config['load_on_start']:
+ self.pause_session()
+ if self.config['last_update']:
+ last_update = datetime.fromtimestamp(self.config['last_update'])
+ check_period = timedelta(days=self.config['check_after_days'])
+ if (
+ not self.config['last_update']
+ or last_update + check_period < datetime.now()
+ ):
+ update_now = True
+ else:
+ d = self.import_list(
+ deluge.configmanager.get_config_dir('blocklist.cache')
+ )
+ d.addCallbacks(self.on_import_complete, self.on_import_error)
+ if self.need_to_resume_session:
+ d.addBoth(self.resume_session)
+
+ # This function is called every 'check_after_days' days, to download
+ # and import a new list if needed.
+ self.update_timer = LoopingCall(self.check_import)
+ if self.config['check_after_days'] > 0:
+ self.update_timer.start(
+ self.config['check_after_days'] * 24 * 60 * 60, update_now
+ )
+
+ def disable(self):
+ self.config.save()
+ log.debug('Reset IP filter')
+ self.core.session.get_ip_filter().add_rule(
+ '0.0.0.0', '255.255.255.255', ALLOW_RANGE
+ )
+ log.debug('Blocklist: Plugin disabled')
+
+ def update(self):
+ pass
+
+ # Exported RPC methods #
+ @export
+ def check_import(self, force=False):
+ """Imports latest blocklist specified by blocklist url.
+
+ Args:
+ force (bool, optional): Force the download/import, default is False.
+
+ Returns:
+ Deferred: A Deferred which fires when the blocklist has been imported.
+
+ """
+ if not self.config['url']:
+ return
+
+ # Reset variables
+ self.filename = None
+ self.force_download = force
+ self.failed_attempts = 0
+ self.auto_detected = False
+ self.up_to_date = False
+ if force:
+ self.reader = None
+ self.is_url = is_url(self.config['url'])
+
+ # Start callback chain
+ if self.is_url:
+ d = self.download_list()
+ d.addCallbacks(self.on_download_complete, self.on_download_error)
+ d.addCallback(self.import_list)
+ else:
+ d = self.import_list(self.config['url'])
+ d.addCallbacks(self.on_import_complete, self.on_import_error)
+ if self.need_to_resume_session:
+ d.addBoth(self.resume_session)
+
+ return d
+
+ @export
+ def get_config(self):
+ """Gets the blocklist config dictionary.
+
+ Returns:
+ dict: The config dictionary.
+
+ """
+ return self.config.config
+
+ @export
+ def set_config(self, config):
+ """Sets the blocklist config.
+
+ Args:
+ config (dict): config to set.
+
+ """
+ needs_blocklist_import = False
+ for key in config:
+ if key == 'whitelisted':
+ saved = set(self.config[key])
+ update = set(config[key])
+ diff = saved.symmetric_difference(update)
+ if diff:
+ log.debug('Whitelist changed. Updating...')
+ added = update.intersection(diff)
+ removed = saved.intersection(diff)
+ if added:
+ for ip in added:
+ try:
+ ip = IP.parse(ip)
+ self.blocklist.add_rule(
+ ip.address, ip.address, ALLOW_RANGE
+ )
+ saved.add(ip.address)
+ log.debug('Added %s to whitelisted', ip)
+ self.num_whited += 1
+ except BadIP as ex:
+ log.error('Bad IP: %s', ex)
+ continue
+ if removed:
+ needs_blocklist_import = True
+ for ip in removed:
+ try:
+ ip = IP.parse(ip)
+ saved.remove(ip.address)
+ log.debug('Removed %s from whitelisted', ip)
+ except BadIP as ex:
+ log.error('Bad IP: %s', ex)
+ continue
+
+ self.config[key] = list(saved)
+ continue
+ elif key == 'check_after_days':
+ if self.config[key] != config[key]:
+ self.config[key] = config[key]
+ update_now = False
+ if self.config['last_update']:
+ last_update = datetime.fromtimestamp(self.config['last_update'])
+ check_period = timedelta(days=self.config['check_after_days'])
+ if (
+ not self.config['last_update']
+ or last_update + check_period < datetime.now()
+ ):
+ update_now = True
+ if self.update_timer.running:
+ self.update_timer.stop()
+ if self.config['check_after_days'] > 0:
+ self.update_timer.start(
+ self.config['check_after_days'] * 24 * 60 * 60, update_now
+ )
+ continue
+ self.config[key] = config[key]
+
+ if needs_blocklist_import:
+ log.debug(
+ 'IP addresses were removed from the whitelist. Since we '
+ 'do not know if they were blocked before. Re-import '
+ 'current blocklist and re-add whitelisted.'
+ )
+ self.has_imported = False
+ d = self.import_list(deluge.configmanager.get_config_dir('blocklist.cache'))
+ d.addCallbacks(self.on_import_complete, self.on_import_error)
+
+ @export
+ def get_status(self):
+ """Get the status of the plugin.
+
+ Returns:
+ dict: The status dict of the plugin.
+
+ """
+ status = {}
+ if self.is_downloading:
+ status['state'] = 'Downloading'
+ elif self.is_importing:
+ status['state'] = 'Importing'
+ else:
+ status['state'] = 'Idle'
+
+ status['up_to_date'] = self.up_to_date
+ status['num_whited'] = self.num_whited
+ status['num_blocked'] = self.num_blocked
+ status['file_progress'] = self.file_progress
+ status['file_url'] = self.config['url']
+ status['file_size'] = self.config['list_size']
+ status['file_date'] = self.config['last_update']
+ status['file_type'] = self.config['list_type']
+ status['whitelisted'] = self.config['whitelisted']
+ if self.config['list_compression']:
+ status['file_type'] += ' (%s)' % self.config['list_compression']
+ return status
+
+ ####
+
+ def update_info(self, blocklist):
+ """Updates blocklist info.
+
+ Args:
+ blocklist (str): Path of blocklist.
+
+ Returns:
+ str: Path of blocklist.
+
+ """
+ log.debug('Updating blocklist info: %s', blocklist)
+ self.config['last_update'] = time.time()
+ self.config['list_size'] = os.path.getsize(blocklist)
+ self.filename = blocklist
+ return blocklist
+
+ def download_list(self, url=None):
+ """Downloads the blocklist specified by 'url' in the config.
+
+ Args:
+ url (str, optional): url to download from, defaults to config value.
+
+ Returns:
+ Deferred: a Deferred which fires once the blocklist has been downloaded.
+
+ """
+
+ def on_retrieve_data(data, current_length, total_length):
+ if total_length:
+ fp = current_length / total_length
+ if fp > 1.0:
+ fp = 1.0
+ else:
+ fp = 0.0
+
+ self.file_progress = fp
+
+ import socket
+
+ socket.setdefaulttimeout(self.config['timeout'])
+
+ if not url:
+ url = self.config['url']
+
+ headers = {}
+ if self.config['last_update'] and not self.force_download:
+ headers['If-Modified-Since'] = formatdate(
+ self.config['last_update'], usegmt=True
+ )
+
+ log.debug('Attempting to download blocklist %s', url)
+ log.debug('Sending headers: %s', headers)
+ self.is_downloading = True
+ return download_file(
+ url,
+ deluge.configmanager.get_config_dir('blocklist.download'),
+ on_retrieve_data,
+ headers,
+ )
+
+ def on_download_complete(self, blocklist):
+ """Runs any download clean up functions.
+
+ Args:
+ blocklist (str): Path of blocklist.
+
+ Returns:
+ Deferred: a Deferred which fires when clean up is done.
+
+ """
+ log.debug('Blocklist download complete: %s', blocklist)
+ self.is_downloading = False
+ return threads.deferToThread(self.update_info, blocklist)
+
+ def on_download_error(self, f):
+ """Recovers from download error.
+
+ Args:
+ f (Failure): Failure that occurred.
+
+ Returns:
+ Deferred or Failure: A Deferred if recovery was possible else original Failure.
+
+ """
+ self.is_downloading = False
+ error_msg = f.getErrorMessage()
+ d = f
+ if f.check(error.PageRedirect):
+ # Handle redirect errors
+ location = urljoin(self.config['url'], error_msg.split(' to ')[1])
+ if 'Moved Permanently' in error_msg:
+ log.debug('Setting blocklist url to %s', location)
+ self.config['url'] = location
+ d = self.download_list(location)
+ d.addCallbacks(self.on_download_complete, self.on_download_error)
+ else:
+ if 'Not Modified' in error_msg:
+ log.debug('Blocklist is up-to-date!')
+ self.up_to_date = True
+ blocklist = deluge.configmanager.get_config_dir('blocklist.cache')
+ d = threads.deferToThread(self.update_info, blocklist)
+ else:
+ log.warning('Blocklist download failed: %s', error_msg)
+ if self.failed_attempts < self.config['try_times']:
+ log.debug(
+ 'Try downloading blocklist again... (%s/%s)',
+ self.failed_attempts,
+ self.config['try_times'],
+ )
+ self.failed_attempts += 1
+ d = self.download_list()
+ d.addCallbacks(self.on_download_complete, self.on_download_error)
+ return d
+
+ def import_list(self, blocklist):
+ """Imports the downloaded blocklist into the session.
+
+ Args:
+ blocklist (str): path of blocklist.
+
+ Returns:
+ Deferred: A Deferred that fires when the blocklist has been imported.
+
+ """
+ log.trace('on import_list')
+
+ def on_read_ip_range(start, end):
+ """Add ip range to blocklist"""
+ # log.trace('Adding ip range %s - %s to ipfilter as blocked', start, end)
+ self.blocklist.add_rule(start.address, end.address, BLOCK_RANGE)
+ self.num_blocked += 1
+
+ def on_finish_read(result):
+ """Add any whitelisted IP's and add the blocklist to session"""
+ # White listing happens last because the last rules added have
+ # priority
+ log.info('Added %d ranges to ipfilter as blocked', self.num_blocked)
+ for ip in self.config['whitelisted']:
+ ip = IP.parse(ip)
+ self.blocklist.add_rule(ip.address, ip.address, ALLOW_RANGE)
+ self.num_whited += 1
+ log.trace('Added %s to the ipfiler as white-listed', ip.address)
+ log.info('Added %d ranges to ipfilter as white-listed', self.num_whited)
+ self.core.session.set_ip_filter(self.blocklist)
+ return result
+
+ # TODO: double check logic
+ if self.up_to_date and self.has_imported:
+ log.debug('Latest blocklist is already imported')
+ return defer.succeed(blocklist)
+
+ self.is_importing = True
+ self.num_blocked = 0
+ self.num_whited = 0
+ self.blocklist = self.core.session.get_ip_filter()
+
+ if not blocklist:
+ blocklist = self.filename
+
+ if not self.reader:
+ self.auto_detect(blocklist)
+ self.auto_detected = True
+
+ def on_reader_failure(failure):
+ log.error('Failed to read!!!!!!')
+ log.exception(failure)
+
+ log.debug('Importing using reader: %s', self.reader)
+ log.debug(
+ 'Reader type: %s compression: %s',
+ self.config['list_type'],
+ self.config['list_compression'],
+ )
+ log.debug('Clearing current ip filtering')
+ # self.blocklist.add_rule('0.0.0.0', '255.255.255.255', ALLOW_RANGE)
+ d = threads.deferToThread(self.reader(blocklist).read, on_read_ip_range)
+ d.addCallback(on_finish_read).addErrback(on_reader_failure)
+
+ return d
+
+ def on_import_complete(self, blocklist):
+ """Runs any import clean up functions.
+
+ Args:
+ blocklist (str): Path of blocklist.
+
+ Returns:
+ Deferred: A Deferred that fires when clean up is done.
+
+ """
+ log.trace('on_import_list_complete')
+ d = blocklist
+ self.is_importing = False
+ self.has_imported = True
+ log.debug('Blocklist import complete!')
+ cache = deluge.configmanager.get_config_dir('blocklist.cache')
+ if blocklist != cache:
+ if self.is_url:
+ log.debug('Moving %s to %s', blocklist, cache)
+ d = threads.deferToThread(shutil.move, blocklist, cache)
+ else:
+ log.debug('Copying %s to %s', blocklist, cache)
+ d = threads.deferToThread(shutil.copy, blocklist, cache)
+ return d
+
+ def on_import_error(self, f):
+ """Recovers from import error.
+
+ Args:
+ f (Failure): Failure that occurred.
+
+ Returns:
+ Deferred or Failure: A Deferred if recovery was possible else original Failure.
+
+ """
+ log.trace('on_import_error: %s', f)
+ d = f
+ self.is_importing = False
+ try_again = False
+ cache = deluge.configmanager.get_config_dir('blocklist.cache')
+
+ if f.check(ReaderParseError) and not self.auto_detected:
+ # Invalid / corrupt list, let's detect it
+ log.warning('Invalid / corrupt blocklist')
+ self.reader = None
+ blocklist = None
+ try_again = True
+ elif self.filename != cache and os.path.exists(cache):
+ # If we have a backup and we haven't already used it
+ log.warning('Error reading blocklist: %s', f.getErrorMessage())
+ blocklist = cache
+ try_again = True
+
+ if try_again:
+ d = self.import_list(blocklist)
+ d.addCallbacks(self.on_import_complete, self.on_import_error)
+
+ return d
+
+ def auto_detect(self, blocklist):
+ """Attempts to auto-detect the blocklist type.
+
+ Args:
+ blocklist (str): Path of blocklist.
+
+ Raises:
+ UnknownFormatError: If the format cannot be detected.
+
+ """
+ self.config['list_compression'] = detect_compression(blocklist)
+ self.config['list_type'] = detect_format(
+ blocklist, self.config['list_compression']
+ )
+ log.debug(
+ 'Auto-detected type: %s compression: %s',
+ self.config['list_type'],
+ self.config['list_compression'],
+ )
+ if not self.config['list_type']:
+ self.config['list_compression'] = ''
+ raise UnknownFormatError
+ else:
+ self.reader = create_reader(
+ self.config['list_type'], self.config['list_compression']
+ )
+
+ def pause_session(self):
+ self.need_to_resume_session = not self.core.session.is_paused()
+ self.core.pause_session()
+
+ def resume_session(self, result):
+ self.core.resume_session()
+ self.need_to_resume_session = False
+ return result
diff --git a/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist.js b/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist.js
new file mode 100644
index 0000000..3c10b81
--- /dev/null
+++ b/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist.js
@@ -0,0 +1,429 @@
+/**
+ * blocklist.js
+ *
+ * Copyright (C) Omar Alvarez 2014 <omar.alvarez@udc.es>
+ *
+ * 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.preferences');
+
+/**
+ * @class Deluge.ux.preferences.BlocklistPage
+ * @extends Ext.Panel
+ */
+Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
+ title: _('Blocklist'),
+ header: false,
+ layout: 'fit',
+ border: false,
+ autoScroll: true,
+
+ initComponent: function () {
+ Deluge.ux.preferences.BlocklistPage.superclass.initComponent.call(this);
+
+ this.URLFset = this.add({
+ xtype: 'fieldset',
+ border: false,
+ title: _('General'),
+ autoHeight: true,
+ defaultType: 'textfield',
+ style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
+ autoWidth: true,
+ labelWidth: 40,
+ });
+
+ this.URL = this.URLFset.add({
+ fieldLabel: _('URL:'),
+ labelSeparator: '',
+ name: 'url',
+ width: '80%',
+ });
+
+ this.SettingsFset = this.add({
+ xtype: 'fieldset',
+ border: false,
+ title: _('Settings'),
+ autoHeight: true,
+ defaultType: 'spinnerfield',
+ style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
+ autoWidth: true,
+ labelWidth: 160,
+ });
+
+ this.checkListDays = this.SettingsFset.add({
+ fieldLabel: _('Check for new list every (days):'),
+ labelSeparator: '',
+ name: 'check_list_days',
+ value: 4,
+ decimalPrecision: 0,
+ width: 80,
+ });
+
+ this.chkImportOnStart = this.SettingsFset.add({
+ xtype: 'checkbox',
+ fieldLabel: _('Import blocklist on startup'),
+ name: 'check_import_startup',
+ });
+
+ this.OptionsFset = this.add({
+ xtype: 'fieldset',
+ border: false,
+ title: _('Options'),
+ autoHeight: true,
+ defaultType: 'button',
+ style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
+ autoWidth: false,
+ width: '80%',
+ labelWidth: 0,
+ });
+
+ this.checkDownload = this.OptionsFset.add({
+ fieldLabel: _(''),
+ name: 'check_download',
+ xtype: 'container',
+ layout: 'hbox',
+ margins: '4 0 0 5',
+ items: [
+ {
+ xtype: 'button',
+ text: ' Check Download and Import ',
+ scale: 'medium',
+ },
+ {
+ xtype: 'box',
+ autoEl: {
+ tag: 'img',
+ src: '../icons/ok.png',
+ },
+ margins: '4 0 0 3',
+ },
+ ],
+ });
+
+ this.forceDownload = this.OptionsFset.add({
+ fieldLabel: _(''),
+ name: 'force_download',
+ text: ' Force Download and Import ',
+ margins: '2 0 0 0',
+ //icon: '../icons/blocklist_import24.png',
+ scale: 'medium',
+ });
+
+ this.ProgressFset = this.add({
+ xtype: 'fieldset',
+ border: false,
+ title: _('Info'),
+ autoHeight: true,
+ defaultType: 'progress',
+ style: 'margin-top: 1px; margin-bottom: 0px; padding-bottom: 0px;',
+ autoWidth: true,
+ labelWidth: 0,
+ hidden: true,
+ });
+
+ this.downProgBar = this.ProgressFset.add({
+ fieldLabel: _(''),
+ name: 'progress_bar',
+ width: '90%',
+ });
+
+ this.InfoFset = this.add({
+ xtype: 'fieldset',
+ border: false,
+ title: _('Info'),
+ autoHeight: true,
+ defaultType: 'label',
+ style: 'margin-top: 0px; margin-bottom: 0px; padding-bottom: 0px;',
+ labelWidth: 60,
+ });
+
+ this.lblFileSize = this.InfoFset.add({
+ fieldLabel: _('File Size:'),
+ labelSeparator: '',
+ name: 'file_size',
+ });
+
+ this.lblDate = this.InfoFset.add({
+ fieldLabel: _('Date:'),
+ labelSeparator: '',
+ name: 'date',
+ });
+
+ this.lblType = this.InfoFset.add({
+ fieldLabel: _('Type:'),
+ labelSeparator: '',
+ name: 'type',
+ });
+
+ this.lblURL = this.InfoFset.add({
+ fieldLabel: _('URL:'),
+ labelSeparator: '',
+ name: 'lbl_URL',
+ });
+
+ this.WhitelistFset = this.add({
+ xtype: 'fieldset',
+ border: false,
+ title: _('Whitelist'),
+ autoHeight: true,
+ defaultType: 'editorgrid',
+ style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
+ autoWidth: true,
+ labelWidth: 0,
+ items: [
+ {
+ fieldLabel: _(''),
+ name: 'whitelist',
+ margins: '2 0 5 5',
+ height: 100,
+ width: 260,
+ autoExpandColumn: 'ip',
+ viewConfig: {
+ emptyText: _('Add an IP...'),
+ deferEmptyText: false,
+ },
+ colModel: new Ext.grid.ColumnModel({
+ columns: [
+ {
+ id: 'ip',
+ header: _('IP'),
+ dataIndex: 'ip',
+ sortable: true,
+ hideable: false,
+ editable: true,
+ editor: {
+ xtype: 'textfield',
+ },
+ },
+ ],
+ }),
+ selModel: new Ext.grid.RowSelectionModel({
+ singleSelect: false,
+ moveEditorOnEnter: false,
+ }),
+ store: new Ext.data.ArrayStore({
+ autoDestroy: true,
+ fields: [{ name: 'ip' }],
+ }),
+ listeners: {
+ afteredit: function (e) {
+ e.record.commit();
+ },
+ },
+ setEmptyText: function (text) {
+ if (this.viewReady) {
+ this.getView().emptyText = text;
+ this.getView().refresh();
+ } else {
+ Ext.apply(this.viewConfig, { emptyText: text });
+ }
+ },
+ loadData: function (data) {
+ this.getStore().loadData(data);
+ if (this.viewReady) {
+ this.getView().updateHeaders();
+ }
+ },
+ },
+ ],
+ });
+
+ this.ipButtonsContainer = this.WhitelistFset.add({
+ xtype: 'container',
+ layout: 'hbox',
+ margins: '4 0 0 5',
+ items: [
+ {
+ xtype: 'button',
+ text: ' Add IP ',
+ margins: '0 5 0 0',
+ },
+ {
+ xtype: 'button',
+ text: ' Delete IP ',
+ },
+ ],
+ });
+
+ this.updateTask = Ext.TaskMgr.start({
+ interval: 2000,
+ run: this.onUpdate,
+ scope: this,
+ });
+
+ this.on('show', this.updateConfig, this);
+
+ this.ipButtonsContainer.getComponent(0).setHandler(this.addIP, this);
+ this.ipButtonsContainer.getComponent(1).setHandler(this.deleteIP, this);
+
+ this.checkDownload.getComponent(0).setHandler(this.checkDown, this);
+ this.forceDownload.setHandler(this.forceDown, this);
+ },
+
+ onApply: function () {
+ var config = {};
+
+ config['url'] = this.URL.getValue();
+ config['check_after_days'] = this.checkListDays.getValue();
+ config['load_on_start'] = this.chkImportOnStart.getValue();
+
+ var ipList = [];
+ var store = this.WhitelistFset.getComponent(0).getStore();
+
+ for (var i = 0; i < store.getCount(); i++) {
+ var record = store.getAt(i);
+ var ip = record.get('ip');
+ ipList.push(ip);
+ }
+
+ config['whitelisted'] = ipList;
+
+ deluge.client.blocklist.set_config(config);
+ },
+
+ onOk: function () {
+ this.onApply();
+ },
+
+ onUpdate: function () {
+ deluge.client.blocklist.get_status({
+ success: function (status) {
+ if (status['state'] == 'Downloading') {
+ this.InfoFset.hide();
+ this.checkDownload.getComponent(0).setDisabled(true);
+ this.checkDownload.getComponent(1).hide();
+ this.forceDownload.setDisabled(true);
+
+ this.ProgressFset.show();
+ this.downProgBar.updateProgress(
+ status['file_progress'],
+ 'Downloading '
+ .concat((status['file_progress'] * 100).toFixed(2))
+ .concat('%'),
+ true
+ );
+ } else if (status['state'] == 'Importing') {
+ this.InfoFset.hide();
+ this.checkDownload.getComponent(0).setDisabled(true);
+ this.checkDownload.getComponent(1).hide();
+ this.forceDownload.setDisabled(true);
+
+ this.ProgressFset.show();
+ this.downProgBar.updateText(
+ 'Importing '.concat(status['num_blocked'])
+ );
+ } else if (status['state'] == 'Idle') {
+ this.ProgressFset.hide();
+ this.checkDownload.getComponent(0).setDisabled(false);
+ this.forceDownload.setDisabled(false);
+ if (status['up_to_date']) {
+ this.checkDownload.getComponent(1).show();
+ this.checkDownload.doLayout();
+ } else {
+ this.checkDownload.getComponent(1).hide();
+ }
+ this.InfoFset.show();
+ this.lblFileSize.setText(fsize(status['file_size']));
+ this.lblDate.setText(fdate(status['file_date']));
+ this.lblType.setText(status['file_type']);
+ this.lblURL.setText(
+ status['file_url'].substr(0, 40).concat('...')
+ );
+ }
+ },
+ scope: this,
+ });
+ },
+
+ checkDown: function () {
+ this.onApply();
+ deluge.client.blocklist.check_import();
+ },
+
+ forceDown: function () {
+ this.onApply();
+ deluge.client.blocklist.check_import((force = true));
+ },
+
+ updateConfig: function () {
+ deluge.client.blocklist.get_config({
+ success: function (config) {
+ this.URL.setValue(config['url']);
+ this.checkListDays.setValue(config['check_after_days']);
+ this.chkImportOnStart.setValue(config['load_on_start']);
+
+ var data = [];
+ var keys = Ext.keys(config['whitelisted']);
+ for (var i = 0; i < keys.length; i++) {
+ var key = keys[i];
+ data.push([config['whitelisted'][key]]);
+ }
+
+ this.WhitelistFset.getComponent(0).loadData(data);
+ },
+ scope: this,
+ });
+
+ deluge.client.blocklist.get_status({
+ success: function (status) {
+ this.lblFileSize.setText(fsize(status['file_size']));
+ this.lblDate.setText(fdate(status['file_date']));
+ this.lblType.setText(status['file_type']);
+ this.lblURL.setText(
+ status['file_url'].substr(0, 40).concat('...')
+ );
+ },
+ scope: this,
+ });
+ },
+
+ addIP: function () {
+ var store = this.WhitelistFset.getComponent(0).getStore();
+ var IP = store.recordType;
+ var i = new IP({
+ ip: '',
+ });
+ this.WhitelistFset.getComponent(0).stopEditing();
+ store.insert(0, i);
+ this.WhitelistFset.getComponent(0).startEditing(0, 0);
+ },
+
+ deleteIP: function () {
+ var selections = this.WhitelistFset.getComponent(0)
+ .getSelectionModel()
+ .getSelections();
+ var store = this.WhitelistFset.getComponent(0).getStore();
+
+ this.WhitelistFset.getComponent(0).stopEditing();
+ for (var i = 0; i < selections.length; i++) store.remove(selections[i]);
+ store.commitChanges();
+ },
+
+ onDestroy: function () {
+ Ext.TaskMgr.stop(this.updateTask);
+
+ deluge.preferences.un('show', this.updateConfig, this);
+
+ Deluge.ux.preferences.BlocklistPage.superclass.onDestroy.call(this);
+ },
+});
+
+Deluge.plugins.BlocklistPlugin = Ext.extend(Deluge.Plugin, {
+ name: 'Blocklist',
+
+ onDisable: function () {
+ deluge.preferences.removePage(this.prefsPage);
+ },
+
+ onEnable: function () {
+ this.prefsPage = deluge.preferences.addPage(
+ new Deluge.ux.preferences.BlocklistPage()
+ );
+ },
+});
+
+Deluge.registerPlugin('Blocklist', Deluge.plugins.BlocklistPlugin);
diff --git a/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist16.png b/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist16.png
new file mode 100644
index 0000000..15b4299
--- /dev/null
+++ b/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist16.png
Binary files differ
diff --git a/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_download24.png b/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_download24.png
new file mode 100644
index 0000000..6de3a0d
--- /dev/null
+++ b/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_download24.png
Binary files differ
diff --git a/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_import24.png b/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_import24.png
new file mode 100644
index 0000000..f1a02e7
--- /dev/null
+++ b/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_import24.png
Binary files differ
diff --git a/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_pref.ui b/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_pref.ui
new file mode 100644
index 0000000..8c1f7a7
--- /dev/null
+++ b/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_pref.ui
@@ -0,0 +1,603 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <object class="GtkAdjustment" id="adjustment1">
+ <property name="lower">1</property>
+ <property name="upper">100</property>
+ <property name="value">1</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">10</property>
+ </object>
+ <object class="GtkWindow" id="window1">
+ <property name="can_focus">False</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkBox" id="blocklist_prefs_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">5</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkFrame" id="frame1">
+ <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="alignment1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="left_padding">12</property>
+ <child>
+ <object class="GtkBox" id="hbox2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">5</property>
+ <child>
+ <object class="GtkLabel" id="label3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">URL:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="entry_url">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">●</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="ypad">5</property>
+ <property name="label" translatable="yes">&lt;b&gt;General&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="frame2">
+ <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="alignment2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="left_padding">12</property>
+ <child>
+ <object class="GtkBox" id="vbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">5</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkTable" id="table1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="n_columns">3</property>
+ <property name="column_spacing">5</property>
+ <property name="row_spacing">5</property>
+ <child>
+ <object class="GtkLabel" id="label8">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Days</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="spin_check_days">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="adjustment">adjustment1</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Check for new list every (days):</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="chk_import_on_start">
+ <property name="label" translatable="yes">Import blocklist on startup</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label10">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="ypad">5</property>
+ <property name="label" translatable="yes">&lt;b&gt;Settings&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>
+ <child>
+ <object class="GtkFrame" id="frame3">
+ <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="alignment3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="xscale">0</property>
+ <property name="left_padding">12</property>
+ <child>
+ <object class="GtkBox" id="hbox3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkBox" id="vbox3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkButton" id="button_check_download">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="tooltip_text" translatable="yes">Download the blocklist file if necessary and import the file.</property>
+ <signal name="clicked" handler="on_button_check_download_clicked" swapped="no"/>
+ <child>
+ <object class="GtkBox" id="hbox4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">5</property>
+ <child>
+ <object class="GtkImage" id="image_download">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-missing-image</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label12">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Check Download and Import</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="button_force_download">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="tooltip_text" translatable="yes">Download a new blocklist file and import it.</property>
+ <signal name="clicked" handler="on_button_force_download_clicked" swapped="no"/>
+ <child>
+ <object class="GtkBox" id="hbox5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">5</property>
+ <child>
+ <object class="GtkImage" id="image_import">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-missing-image</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label7">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Force Download and Import</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkImage" id="image_up_to_date">
+ <property name="can_focus">False</property>
+ <property name="tooltip_text" translatable="yes">Blocklist is up to date</property>
+ <property name="yalign">0.15000000596046448</property>
+ <property name="xpad">2</property>
+ <property name="stock">gtk-yes</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label11">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="ypad">5</property>
+ <property name="label" translatable="yes">&lt;b&gt;Options&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">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFrame" id="frame4">
+ <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="alignment4">
+ <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="vbox4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkProgressBar" id="progressbar">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkTable" id="table_info">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="n_rows">4</property>
+ <property name="n_columns">2</property>
+ <property name="column_spacing">5</property>
+ <child>
+ <object class="GtkLabel" id="label_url">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label_type">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label_modified">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</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>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label_filesize">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label17">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">URL:</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ <property name="x_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label16">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Type:</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="x_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label15">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Date:</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>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label14">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">File Size:</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="x_options">GTK_FILL</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label13">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;b&gt;Info&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">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFrame" id="whitelist_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="alignment5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="left_padding">12</property>
+ <child>
+ <object class="GtkBox" id="hbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <child>
+ <object class="GtkTreeView" id="whitelist_treeview">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="headers_visible">False</property>
+ <property name="headers_clickable">False</property>
+ <child internal-child="selection">
+ <object class="GtkTreeSelection"/>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkVButtonBox" id="vbuttonbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="homogeneous">True</property>
+ <property name="layout_style">start</property>
+ <child>
+ <object class="GtkButton" id="whitelist_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_whitelist_add_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="whitelist_delete">
+ <property name="label">gtk-delete</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_whitelist_remove_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;b&gt;Whitelist&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/deluge/plugins/Blocklist/deluge_blocklist/decompressers.py b/deluge/plugins/Blocklist/deluge_blocklist/decompressers.py
new file mode 100644
index 0000000..cd2ee8c
--- /dev/null
+++ b/deluge/plugins/Blocklist/deluge_blocklist/decompressers.py
@@ -0,0 +1,44 @@
+#
+# Copyright (C) 2009-2010 John Garland <johnnybg+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.
+#
+# pylint: disable=redefined-builtin
+
+import bz2
+import gzip
+import zipfile
+
+
+def Zipped(reader): # NOQA: N802
+ """Blocklist reader for zipped blocklists"""
+
+ def _open(self):
+ z = zipfile.ZipFile(self.file)
+ f = z.open(z.namelist()[0])
+ return f
+
+ reader.open = _open
+ return reader
+
+
+def GZipped(reader): # NOQA: N802
+ """Blocklist reader for gzipped blocklists"""
+
+ def _open(self):
+ return gzip.open(self.file)
+
+ reader.open = _open
+ return reader
+
+
+def BZipped2(reader): # NOQA: N802
+ """Blocklist reader for bzipped2 blocklists"""
+
+ def _open(self):
+ return bz2.BZ2File(self.file)
+
+ reader.open = _open
+ return reader
diff --git a/deluge/plugins/Blocklist/deluge_blocklist/detect.py b/deluge/plugins/Blocklist/deluge_blocklist/detect.py
new file mode 100644
index 0000000..43ad305
--- /dev/null
+++ b/deluge/plugins/Blocklist/deluge_blocklist/detect.py
@@ -0,0 +1,48 @@
+#
+# Copyright (C) 2009-2010 John Garland <johnnybg+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 .decompressers import BZipped2, GZipped, Zipped
+from .readers import EmuleReader, PeerGuardianReader, SafePeerReader
+
+COMPRESSION_TYPES = {b'PK': 'Zip', b'\x1f\x8b': 'GZip', b'BZ': 'BZip2'}
+
+DECOMPRESSERS = {'Zip': Zipped, 'GZip': GZipped, 'BZip2': BZipped2}
+
+READERS = {
+ 'Emule': EmuleReader,
+ 'SafePeer': SafePeerReader,
+ 'PeerGuardian': PeerGuardianReader,
+}
+
+
+class UnknownFormatError(Exception):
+ pass
+
+
+def detect_compression(filename):
+ with open(filename, 'rb') as _file:
+ magic_number = _file.read(2)
+ return COMPRESSION_TYPES.get(magic_number, '')
+
+
+def detect_format(filename, compression=''):
+ file_format = ''
+ for reader in READERS:
+ if create_reader(reader, compression)(filename).is_valid():
+ file_format = reader
+ break
+ return file_format
+
+
+def create_reader(file_format, compression=''):
+ reader = READERS.get(file_format)
+ if reader and compression:
+ decompressor = DECOMPRESSERS.get(compression)
+ if decompressor:
+ reader = decompressor(reader)
+ return reader
diff --git a/deluge/plugins/Blocklist/deluge_blocklist/gtkui.py b/deluge/plugins/Blocklist/deluge_blocklist/gtkui.py
new file mode 100644
index 0000000..e6105cd
--- /dev/null
+++ b/deluge/plugins/Blocklist/deluge_blocklist/gtkui.py
@@ -0,0 +1,254 @@
+#
+# Copyright (C) 2008 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 logging
+from datetime import datetime
+
+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.common
+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__)
+
+
+class GtkUI(Gtk3PluginBase):
+ def enable(self):
+ log.debug('Blocklist GtkUI enable..')
+ self.plugin = component.get('PluginManager')
+
+ self.load_preferences_page()
+
+ self.status_item = component.get('StatusBar').add_item(
+ image=common.get_resource('blocklist16.png'),
+ text='',
+ callback=self._on_status_item_clicked,
+ tooltip=_('Blocked IP Ranges /Whitelisted IP Ranges'),
+ )
+
+ # Register some hooks
+ self.plugin.register_hook('on_apply_prefs', self._on_apply_prefs)
+ self.plugin.register_hook('on_show_prefs', self._on_show_prefs)
+
+ def disable(self):
+ log.debug('Blocklist GtkUI disable..')
+
+ # Remove the preferences page
+ self.plugin.remove_preferences_page(_('Blocklist'))
+
+ # Remove status item
+ component.get('StatusBar').remove_item(self.status_item)
+ del self.status_item
+
+ # Deregister the hooks
+ self.plugin.deregister_hook('on_apply_prefs', self._on_apply_prefs)
+ self.plugin.deregister_hook('on_show_prefs', self._on_show_prefs)
+
+ del self.glade
+
+ def update(self):
+ def _on_get_status(status):
+ if status['state'] == 'Downloading':
+ self.table_info.hide()
+ self.builder.get_object('button_check_download').set_sensitive(False)
+ self.builder.get_object('button_force_download').set_sensitive(False)
+ self.builder.get_object('image_up_to_date').hide()
+
+ self.status_item.set_text(
+ 'Downloading %.2f%%' % (status['file_progress'] * 100)
+ )
+ self.progress_bar.set_text(
+ 'Downloading %.2f%%' % (status['file_progress'] * 100)
+ )
+ self.progress_bar.set_fraction(status['file_progress'])
+ self.progress_bar.show()
+
+ elif status['state'] == 'Importing':
+ self.table_info.hide()
+ self.builder.get_object('button_check_download').set_sensitive(False)
+ self.builder.get_object('button_force_download').set_sensitive(False)
+ self.builder.get_object('image_up_to_date').hide()
+
+ self.status_item.set_text('Importing ' + str(status['num_blocked']))
+ self.progress_bar.set_text('Importing %s' % (status['num_blocked']))
+ self.progress_bar.pulse()
+ self.progress_bar.show()
+
+ elif status['state'] == 'Idle':
+ self.progress_bar.hide()
+ self.builder.get_object('button_check_download').set_sensitive(True)
+ self.builder.get_object('button_force_download').set_sensitive(True)
+ if status['up_to_date']:
+ self.builder.get_object('image_up_to_date').show()
+ else:
+ self.builder.get_object('image_up_to_date').hide()
+
+ self.table_info.show()
+ self.status_item.set_text('%(num_blocked)s/%(num_whited)s' % status)
+
+ self.builder.get_object('label_filesize').set_text(
+ deluge.common.fsize(status['file_size'])
+ )
+ self.builder.get_object('label_modified').set_text(
+ datetime.fromtimestamp(status['file_date']).strftime('%c')
+ )
+ self.builder.get_object('label_type').set_text(status['file_type'])
+ self.builder.get_object('label_url').set_text(status['file_url'])
+
+ client.blocklist.get_status().addCallback(_on_get_status)
+
+ def _on_show_prefs(self):
+ def _on_get_config(config):
+ log.trace('Loaded config: %s', config)
+ self.builder.get_object('entry_url').set_text(config['url'])
+ self.builder.get_object('spin_check_days').set_value(
+ config['check_after_days']
+ )
+ self.builder.get_object('chk_import_on_start').set_active(
+ config['load_on_start']
+ )
+ self.populate_whitelist(config['whitelisted'])
+
+ client.blocklist.get_config().addCallback(_on_get_config)
+
+ def _on_apply_prefs(self):
+ config = {}
+ config['url'] = self.builder.get_object('entry_url').get_text().strip()
+ config['check_after_days'] = self.builder.get_object(
+ 'spin_check_days'
+ ).get_value_as_int()
+ config['load_on_start'] = self.builder.get_object(
+ 'chk_import_on_start'
+ ).get_active()
+ config['whitelisted'] = [
+ ip[0] for ip in self.whitelist_model if ip[0] != 'IP HERE'
+ ]
+ client.blocklist.set_config(config)
+
+ def _on_button_check_download_clicked(self, widget):
+ self._on_apply_prefs()
+ client.blocklist.check_import()
+
+ def _on_button_force_download_clicked(self, widget):
+ self._on_apply_prefs()
+ client.blocklist.check_import(force=True)
+
+ def _on_status_item_clicked(self, widget, event):
+ component.get('Preferences').show(_('Blocklist'))
+
+ def load_preferences_page(self):
+ """Initializes the preferences page and adds it to the preferences dialog"""
+ # Load the preferences page
+ self.builder = Gtk.Builder()
+ self.builder.add_from_file(common.get_resource('blocklist_pref.ui'))
+
+ self.whitelist_frame = self.builder.get_object('whitelist_frame')
+ self.progress_bar = self.builder.get_object('progressbar')
+ self.table_info = self.builder.get_object('table_info')
+
+ # Hide the progress bar initially
+ self.progress_bar.hide()
+ self.table_info.show()
+
+ # Create the whitelisted model
+ self.build_whitelist_model_treeview()
+
+ self.builder.connect_signals(
+ {
+ 'on_button_check_download_clicked': self._on_button_check_download_clicked,
+ 'on_button_force_download_clicked': self._on_button_force_download_clicked,
+ 'on_whitelist_add_clicked': (
+ self.on_add_button_clicked,
+ self.whitelist_treeview,
+ ),
+ 'on_whitelist_remove_clicked': (
+ self.on_delete_button_clicked,
+ self.whitelist_treeview,
+ ),
+ }
+ )
+
+ # Set button icons
+ self.builder.get_object('image_download').set_from_file(
+ common.get_resource('blocklist_download24.png')
+ )
+
+ self.builder.get_object('image_import').set_from_file(
+ common.get_resource('blocklist_import24.png')
+ )
+
+ # Update the preferences page with config values from the core
+ self._on_show_prefs()
+
+ # Add the page to the preferences dialog
+ self.plugin.add_preferences_page(
+ _('Blocklist'), self.builder.get_object('blocklist_prefs_box')
+ )
+
+ def build_whitelist_model_treeview(self):
+ self.whitelist_treeview = self.builder.get_object('whitelist_treeview')
+ treeview_selection = self.whitelist_treeview.get_selection()
+ treeview_selection.connect(
+ 'changed', self.on_whitelist_treeview_selection_changed
+ )
+ self.whitelist_model = Gtk.ListStore(str, bool)
+ renderer = Gtk.CellRendererText()
+ renderer.connect('edited', self.on_cell_edited, self.whitelist_model)
+ renderer.ip = 0
+
+ column = Gtk.TreeViewColumn('IPs', renderer, text=0, editable=1)
+ column.set_expand(True)
+ self.whitelist_treeview.append_column(column)
+ self.whitelist_treeview.set_model(self.whitelist_model)
+
+ def on_cell_edited(self, cell, path_string, new_text, model):
+ # iter = model.get_iter_from_string(path_string)
+ # path = model.get_path(iter)[0]
+ try:
+ ip = common.IP.parse(new_text)
+ model.set(model.get_iter_from_string(path_string), 0, ip.address)
+ except common.BadIP as ex:
+ model.remove(model.get_iter_from_string(path_string))
+ from deluge.ui.gtkui import dialogs
+
+ d = dialogs.ErrorDialog(_('Bad IP address'), ex.message)
+ d.run()
+
+ def on_whitelist_treeview_selection_changed(self, selection):
+ model, selected_connection_iter = selection.get_selected()
+ if selected_connection_iter:
+ self.builder.get_object('whitelist_delete').set_property('sensitive', True)
+ else:
+ self.builder.get_object('whitelist_delete').set_property('sensitive', False)
+
+ def on_add_button_clicked(self, widget, treeview):
+ model = treeview.get_model()
+ model.set(model.append(), 0, 'IP HERE', 1, True)
+
+ def on_delete_button_clicked(self, widget, treeview):
+ selection = treeview.get_selection()
+ model, selected_iter = selection.get_selected()
+ if selected_iter:
+ # path = model.get_path(iter)[0]
+ model.remove(selected_iter)
+
+ def populate_whitelist(self, whitelist):
+ self.whitelist_model.clear()
+ for ip in whitelist:
+ self.whitelist_model.set(self.whitelist_model.append(), 0, ip, 1, True)
diff --git a/deluge/plugins/Blocklist/deluge_blocklist/peerguardian.py b/deluge/plugins/Blocklist/deluge_blocklist/peerguardian.py
new file mode 100644
index 0000000..b5fb181
--- /dev/null
+++ b/deluge/plugins/Blocklist/deluge_blocklist/peerguardian.py
@@ -0,0 +1,66 @@
+#
+# Copyright (C) 2007 Steve 'Tarka' Smith (tarka@internode.on.net)
+#
+# 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 gzip
+import logging
+import socket
+from struct import unpack
+
+log = logging.getLogger(__name__)
+
+
+class PGException(Exception):
+ pass
+
+
+# Incrementally reads PeerGuardian blocklists v1 and v2.
+# See http://wiki.phoenixlabs.org/wiki/P2B_Format
+class PGReader:
+ def __init__(self, filename):
+ log.debug('PGReader loading: %s', filename)
+
+ try:
+ with gzip.open(filename, 'rb') as _file:
+ self.fd = _file
+ except OSError:
+ log.debug('Blocklist: PGReader: Incorrect file type or list is corrupt')
+
+ # 4 bytes, should be 0xffffffff
+ buf = self.fd.read(4)
+ hdr = unpack('l', buf)[0]
+ if hdr != -1:
+ raise PGException(_('Invalid leader') + ' %d' % hdr)
+
+ magic = self.fd.read(3)
+ if magic != 'P2B':
+ raise PGException(_('Invalid magic code'))
+
+ buf = self.fd.read(1)
+ ver = ord(buf)
+ if ver != 1 and ver != 2:
+ raise PGException(_('Invalid version') + ' %d' % ver)
+
+ def __next__(self):
+ # Skip over the string
+ buf = -1
+ while buf != 0:
+ buf = self.fd.read(1)
+ if buf == '': # EOF
+ return False
+ buf = ord(buf)
+
+ buf = self.fd.read(4)
+ start = socket.inet_ntoa(buf)
+
+ buf = self.fd.read(4)
+ end = socket.inet_ntoa(buf)
+
+ return (start, end)
+
+ def close(self):
+ self.fd.close()
diff --git a/deluge/plugins/Blocklist/deluge_blocklist/readers.py b/deluge/plugins/Blocklist/deluge_blocklist/readers.py
new file mode 100644
index 0000000..14230ed
--- /dev/null
+++ b/deluge/plugins/Blocklist/deluge_blocklist/readers.py
@@ -0,0 +1,99 @@
+#
+# Copyright (C) 2009-2010 John Garland <johnnybg+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 logging
+import re
+
+from deluge.common import decode_bytes
+
+from .common import IP, BadIP, raises_errors_as
+
+log = logging.getLogger(__name__)
+
+
+class ReaderParseError(Exception):
+ pass
+
+
+class BaseReader:
+ """Base reader for blocklist files"""
+
+ def __init__(self, _file):
+ """Creates a new BaseReader given a file"""
+ self.file = _file
+
+ def open(self):
+ """Opens the associated file for reading"""
+ return open(self.file)
+
+ def parse(self, line):
+ """Extracts ip range from given line"""
+ raise NotImplementedError
+
+ def read(self, callback):
+ """Calls callback on each ip range in the file"""
+ for start, end in self.readranges():
+ try:
+ callback(IP.parse(start), IP.parse(end))
+ except BadIP as ex:
+ log.error('Failed to parse IP: %s', ex)
+ return self.file
+
+ def is_ignored(self, line):
+ """Ignore commented lines and blank lines"""
+ line = line.strip()
+ return line.startswith('#') or not line
+
+ def is_valid(self):
+ """Determines whether file is valid for this reader"""
+ blocklist = self.open()
+ valid = True
+ for line in blocklist:
+ line = decode_bytes(line)
+ if not self.is_ignored(line):
+ try:
+ (start, end) = self.parse(line)
+ if not re.match(r'^(\d{1,3}\.){4}$', start + '.') or not re.match(
+ r'^(\d{1,3}\.){4}$', end + '.'
+ ):
+ valid = False
+ except Exception:
+ valid = False
+ break
+ blocklist.close()
+ return valid
+
+ @raises_errors_as(ReaderParseError)
+ def readranges(self):
+ """Yields each ip range from the file"""
+ blocklist = self.open()
+ for line in blocklist:
+ line = decode_bytes(line)
+ if not self.is_ignored(line):
+ yield self.parse(line)
+ blocklist.close()
+
+
+class EmuleReader(BaseReader):
+ """Blocklist reader for emule style blocklists"""
+
+ def parse(self, line):
+ return line.strip().split(' , ')[0].split(' - ')
+
+
+class SafePeerReader(BaseReader):
+ """Blocklist reader for SafePeer style blocklists"""
+
+ def parse(self, line):
+ return line.strip().split(':')[-1].split('-')
+
+
+class PeerGuardianReader(SafePeerReader):
+ """Blocklist reader for PeerGuardian style blocklists"""
+
+ pass
diff --git a/deluge/plugins/Blocklist/deluge_blocklist/webui.py b/deluge/plugins/Blocklist/deluge_blocklist/webui.py
new file mode 100644
index 0000000..8ba4911
--- /dev/null
+++ b/deluge/plugins/Blocklist/deluge_blocklist/webui.py
@@ -0,0 +1,27 @@
+#
+# Copyright (C) 2008 Martijn Voncken <mvoncken@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__)
+
+FORMAT_LIST = [
+ ('gzmule', _('Emule IP list (GZip)')),
+ ('spzip', _('SafePeer Text (Zipped)')),
+ ('pgtext', _('PeerGuardian Text (Uncompressed)')),
+ ('p2bgz', _('PeerGuardian P2B (GZip)')),
+]
+
+
+class WebUI(WebPluginBase):
+ scripts = [get_resource('blocklist.js')]
+ debug_scripts = scripts
diff --git a/deluge/plugins/Blocklist/setup.py b/deluge/plugins/Blocklist/setup.py
new file mode 100644
index 0000000..2aa6834
--- /dev/null
+++ b/deluge/plugins/Blocklist/setup.py
@@ -0,0 +1,42 @@
+#
+# 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 setuptools import find_packages, setup
+
+__plugin_name__ = 'Blocklist'
+__author__ = 'John Garland'
+__author_email__ = 'johnnybg+deluge@gmail.com'
+__version__ = '1.4'
+__url__ = 'http://deluge-torrent.org'
+__license__ = 'GPLv3'
+__description__ = 'Download and import IP blocklists'
+__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__,
+ zip_safe=False,
+ 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),
+)
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),
+)
diff --git a/deluge/plugins/Extractor/deluge_extractor/__init__.py b/deluge/plugins/Extractor/deluge_extractor/__init__.py
new file mode 100644
index 0000000..87d1584
--- /dev/null
+++ b/deluge/plugins/Extractor/deluge_extractor/__init__.py
@@ -0,0 +1,37 @@
+#
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-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 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/Extractor/deluge_extractor/common.py b/deluge/plugins/Extractor/deluge_extractor/common.py
new file mode 100644
index 0000000..eb47f13
--- /dev/null
+++ b/deluge/plugins/Extractor/deluge_extractor/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/Extractor/deluge_extractor/core.py b/deluge/plugins/Extractor/deluge_extractor/core.py
new file mode 100644
index 0000000..23b2a00
--- /dev/null
+++ b/deluge/plugins/Extractor/deluge_extractor/core.py
@@ -0,0 +1,186 @@
+#
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-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 errno
+import logging
+import os
+
+from twisted.internet.utils import getProcessOutputAndValue
+from twisted.python.procutils import which
+
+import deluge.component as component
+import deluge.configmanager
+from deluge.common import windows_check
+from deluge.core.rpcserver import export
+from deluge.plugins.pluginbase import CorePluginBase
+
+log = logging.getLogger(__name__)
+
+DEFAULT_PREFS = {'extract_path': '', 'use_name_folder': True}
+
+if windows_check():
+ win_7z_exes = [
+ '7z.exe',
+ 'C:\\Program Files\\7-Zip\\7z.exe',
+ 'C:\\Program Files (x86)\\7-Zip\\7z.exe',
+ ]
+
+ import winreg
+
+ try:
+ hkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'Software\\7-Zip')
+ except OSError:
+ pass
+ else:
+ win_7z_path = os.path.join(winreg.QueryValueEx(hkey, 'Path')[0], '7z.exe')
+ winreg.CloseKey(hkey)
+ win_7z_exes.insert(1, win_7z_path)
+
+ switch_7z = 'x -y'
+ # Future suport:
+ # 7-zip cannot extract tar.* with single command.
+ # ".tar.gz", ".tgz",
+ # ".tar.bz2", ".tbz",
+ # ".tar.lzma", ".tlz",
+ # ".tar.xz", ".txz",
+ exts_7z = ['.rar', '.zip', '.tar', '.7z', '.xz', '.lzma']
+ for win_7z_exe in win_7z_exes:
+ if which(win_7z_exe):
+ EXTRACT_COMMANDS = dict.fromkeys(exts_7z, [win_7z_exe, switch_7z])
+ break
+else:
+ required_cmds = ['unrar', 'unzip', 'tar', 'unxz', 'unlzma', '7zr', 'bunzip2']
+ # Possible future suport:
+ # gunzip: gz (cmd will delete original archive)
+ # the following do not extract to dest dir
+ # ".xz": ["xz", "-d --keep"],
+ # ".lzma": ["xz", "-d --format=lzma --keep"],
+ # ".bz2": ["bzip2", "-d --keep"],
+
+ EXTRACT_COMMANDS = {
+ '.rar': ['unrar', 'x -o+ -y'],
+ '.tar': ['tar', '-xf'],
+ '.zip': ['unzip', ''],
+ '.tar.gz': ['tar', '-xzf'],
+ '.tgz': ['tar', '-xzf'],
+ '.tar.bz2': ['tar', '-xjf'],
+ '.tbz': ['tar', '-xjf'],
+ '.tar.lzma': ['tar', '--lzma -xf'],
+ '.tlz': ['tar', '--lzma -xf'],
+ '.tar.xz': ['tar', '--xz -xf'],
+ '.txz': ['tar', '--xz -xf'],
+ '.7z': ['7zr', 'x'],
+ }
+ # Test command exists and if not, remove.
+ for command in required_cmds:
+ if not which(command):
+ for k, v in list(EXTRACT_COMMANDS.items()):
+ if command in v[0]:
+ log.warning('%s not found, disabling support for %s', command, k)
+ del EXTRACT_COMMANDS[k]
+
+if not EXTRACT_COMMANDS:
+ raise Exception('No archive extracting programs found, plugin will be disabled')
+
+
+class Core(CorePluginBase):
+ def enable(self):
+ self.config = deluge.configmanager.ConfigManager(
+ 'extractor.conf', DEFAULT_PREFS
+ )
+ if not self.config['extract_path']:
+ self.config['extract_path'] = deluge.configmanager.ConfigManager(
+ 'core.conf'
+ )['download_location']
+ component.get('EventManager').register_event_handler(
+ 'TorrentFinishedEvent', self._on_torrent_finished
+ )
+
+ def disable(self):
+ component.get('EventManager').deregister_event_handler(
+ 'TorrentFinishedEvent', self._on_torrent_finished
+ )
+
+ def update(self):
+ pass
+
+ def _on_torrent_finished(self, torrent_id):
+ """
+ This is called when a torrent finishes and checks if any files to extract.
+ """
+ tid = component.get('TorrentManager').torrents[torrent_id]
+ tid_status = tid.get_status(['download_location', 'name'])
+
+ files = tid.get_files()
+ for f in files:
+ file_root, file_ext = os.path.splitext(f['path'])
+ file_ext_sec = os.path.splitext(file_root)[1]
+ if file_ext_sec and file_ext_sec + file_ext in EXTRACT_COMMANDS:
+ file_ext = file_ext_sec + file_ext
+ elif file_ext not in EXTRACT_COMMANDS or file_ext_sec == '.tar':
+ log.debug('Cannot extract file with unknown file type: %s', f['path'])
+ continue
+ elif file_ext == '.rar' and 'part' in file_ext_sec:
+ part_num = file_ext_sec.split('part')[1]
+ if part_num.isdigit() and int(part_num) != 1:
+ log.debug('Skipping remaining multi-part rar files: %s', f['path'])
+ continue
+
+ cmd = EXTRACT_COMMANDS[file_ext]
+ fpath = os.path.join(
+ tid_status['download_location'], os.path.normpath(f['path'])
+ )
+ dest = os.path.normpath(self.config['extract_path'])
+ if self.config['use_name_folder']:
+ dest = os.path.join(dest, tid_status['name'])
+
+ try:
+ os.makedirs(dest)
+ except OSError as ex:
+ if not (ex.errno == errno.EEXIST and os.path.isdir(dest)):
+ log.error('Error creating destination folder: %s', ex)
+ break
+
+ def on_extract(result, torrent_id, fpath):
+ # Check command exit code.
+ if not result[2]:
+ log.info('Extract successful: %s (%s)', fpath, torrent_id)
+ else:
+ log.error(
+ 'Extract failed: %s (%s) %s', fpath, torrent_id, result[1]
+ )
+
+ # Run the command and add callback.
+ log.debug(
+ 'Extracting %s from %s with %s %s to %s',
+ fpath,
+ torrent_id,
+ cmd[0],
+ cmd[1],
+ dest,
+ )
+ d = getProcessOutputAndValue(
+ cmd[0], cmd[1].split() + [str(fpath)], os.environ, str(dest)
+ )
+ d.addCallback(on_extract, torrent_id, fpath)
+
+ @export
+ def set_config(self, config):
+ """Sets the config dictionary."""
+ for key in config:
+ self.config[key] = config[key]
+ self.config.save()
+
+ @export
+ def get_config(self):
+ """Returns the config dictionary."""
+ return self.config.config
diff --git a/deluge/plugins/Extractor/deluge_extractor/data/extractor.js b/deluge/plugins/Extractor/deluge_extractor/data/extractor.js
new file mode 100644
index 0000000..952b645
--- /dev/null
+++ b/deluge/plugins/Extractor/deluge_extractor/data/extractor.js
@@ -0,0 +1,100 @@
+/**
+ * extractor.js
+ *
+ * Copyright (C) Calum Lind 2014 <calumlind@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.preferences');
+
+/**
+ * @class Deluge.ux.preferences.ExtractorPage
+ * @extends Ext.Panel
+ */
+Deluge.ux.preferences.ExtractorPage = Ext.extend(Ext.Panel, {
+ title: _('Extractor'),
+ header: false,
+ layout: 'fit',
+ border: false,
+
+ initComponent: function () {
+ Deluge.ux.preferences.ExtractorPage.superclass.initComponent.call(this);
+
+ this.form = this.add({
+ xtype: 'form',
+ layout: 'form',
+ border: false,
+ autoHeight: true,
+ });
+
+ fieldset = this.form.add({
+ xtype: 'fieldset',
+ border: false,
+ title: '',
+ autoHeight: true,
+ labelAlign: 'top',
+ labelWidth: 80,
+ defaultType: 'textfield',
+ });
+
+ this.extract_path = fieldset.add({
+ fieldLabel: _('Extract to:'),
+ labelSeparator: '',
+ name: 'extract_path',
+ width: '97%',
+ });
+
+ this.use_name_folder = fieldset.add({
+ xtype: 'checkbox',
+ name: 'use_name_folder',
+ height: 22,
+ hideLabel: true,
+ boxLabel: _('Create torrent name sub-folder'),
+ });
+
+ this.on('show', this.updateConfig, this);
+ },
+
+ onApply: function () {
+ // build settings object
+ var config = {};
+
+ config['extract_path'] = this.extract_path.getValue();
+ config['use_name_folder'] = this.use_name_folder.getValue();
+
+ deluge.client.extractor.set_config(config);
+ },
+
+ onOk: function () {
+ this.onApply();
+ },
+
+ updateConfig: function () {
+ deluge.client.extractor.get_config({
+ success: function (config) {
+ this.extract_path.setValue(config['extract_path']);
+ this.use_name_folder.setValue(config['use_name_folder']);
+ },
+ scope: this,
+ });
+ },
+});
+
+Deluge.plugins.ExtractorPlugin = Ext.extend(Deluge.Plugin, {
+ name: 'Extractor',
+
+ onDisable: function () {
+ deluge.preferences.removePage(this.prefsPage);
+ },
+
+ onEnable: function () {
+ this.prefsPage = deluge.preferences.addPage(
+ new Deluge.ux.preferences.ExtractorPage()
+ );
+ },
+});
+Deluge.registerPlugin('Extractor', Deluge.plugins.ExtractorPlugin);
diff --git a/deluge/plugins/Extractor/deluge_extractor/data/extractor_prefs.ui b/deluge/plugins/Extractor/deluge_extractor/data/extractor_prefs.ui
new file mode 100644
index 0000000..9e8070b
--- /dev/null
+++ b/deluge/plugins/Extractor/deluge_extractor/data/extractor_prefs.ui
@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <object class="GtkWindow" id="window1">
+ <property name="can_focus">False</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkBox" id="extractor_prefs_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">5</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkFrame" id="frame1">
+ <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="GtkBox" id="vbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="border_width">5</property>
+ <property name="spacing">5</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="hbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">5</property>
+ <child>
+ <object class="GtkLabel" id="label2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Extract to:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="hbox2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkFileChooserButton" id="folderchooser_path">
+ <property name="can_focus">False</property>
+ <property name="action">select-folder</property>
+ <property name="title" translatable="yes">Select A Folder</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="entry_path">
+ <property name="can_focus">True</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="chk_use_name">
+ <property name="label" translatable="yes">Create torrent name sub-folder</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="tooltip_text" translatable="yes">This option will create a sub-folder using the torrent's name within the selected extract folder and put the extracted files there.</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;b&gt;General&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/deluge/plugins/Extractor/deluge_extractor/gtkui.py b/deluge/plugins/Extractor/deluge_extractor/gtkui.py
new file mode 100644
index 0000000..a754a5f
--- /dev/null
+++ b/deluge/plugins/Extractor/deluge_extractor/gtkui.py
@@ -0,0 +1,93 @@
+#
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-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 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 .common import get_resource
+
+log = logging.getLogger(__name__)
+
+
+class GtkUI(Gtk3PluginBase):
+ def enable(self):
+ self.builder = Gtk.Builder()
+ self.builder.add_from_file(get_resource('extractor_prefs.ui'))
+
+ component.get('Preferences').add_page(
+ _('Extractor'), self.builder.get_object('extractor_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()
+
+ def disable(self):
+ component.get('Preferences').remove_page(_('Extractor'))
+ component.get('PluginManager').deregister_hook(
+ 'on_apply_prefs', self.on_apply_prefs
+ )
+ component.get('PluginManager').deregister_hook(
+ 'on_show_prefs', self.on_show_prefs
+ )
+ del self.builder
+
+ def on_apply_prefs(self):
+ log.debug('applying prefs for Extractor')
+ if client.is_localhost():
+ path = self.builder.get_object('folderchooser_path').get_filename()
+ else:
+ path = self.builder.get_object('entry_path').get_text()
+
+ config = {
+ 'extract_path': path,
+ 'use_name_folder': self.builder.get_object('chk_use_name').get_active(),
+ }
+
+ client.extractor.set_config(config)
+
+ def on_show_prefs(self):
+ if client.is_localhost():
+ self.builder.get_object('folderchooser_path').show()
+ self.builder.get_object('entry_path').hide()
+ else:
+ self.builder.get_object('folderchooser_path').hide()
+ self.builder.get_object('entry_path').show()
+
+ def on_get_config(config):
+ if client.is_localhost():
+ self.builder.get_object('folderchooser_path').set_current_folder(
+ config['extract_path']
+ )
+ else:
+ self.builder.get_object('entry_path').set_text(config['extract_path'])
+
+ self.builder.get_object('chk_use_name').set_active(
+ config['use_name_folder']
+ )
+
+ client.extractor.get_config().addCallback(on_get_config)
diff --git a/deluge/plugins/Extractor/deluge_extractor/webui.py b/deluge/plugins/Extractor/deluge_extractor/webui.py
new file mode 100644
index 0000000..0f58658
--- /dev/null
+++ b/deluge/plugins/Extractor/deluge_extractor/webui.py
@@ -0,0 +1,24 @@
+#
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-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 logging
+
+from deluge.plugins.pluginbase import WebPluginBase
+
+from .common import get_resource
+
+log = logging.getLogger(__name__)
+
+
+class WebUI(WebPluginBase):
+ scripts = [get_resource('extractor.js')]
+ debug_scripts = scripts
diff --git a/deluge/plugins/Extractor/setup.py b/deluge/plugins/Extractor/setup.py
new file mode 100644
index 0000000..09385c6
--- /dev/null
+++ b/deluge/plugins/Extractor/setup.py
@@ -0,0 +1,54 @@
+#
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-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 setuptools import find_packages, setup
+
+__plugin_name__ = 'Extractor'
+__author__ = 'Andrew Resch'
+__author_email__ = 'andrewresch@gmail.com'
+__version__ = '0.7'
+__url__ = 'http://deluge-torrent.org'
+__license__ = 'GPLv3'
+__description__ = 'Extract files upon torrent completion'
+__long_description__ = """
+Extract files upon torrent completion
+
+Supports: .rar, .tar, .zip, .7z .tar.gz, .tgz, .tar.bz2, .tbz .tar.lzma, .tlz, .tar.xz, .txz
+
+Windows support: .rar, .zip, .tar, .7z, .xz, .lzma
+( Requires 7-zip installed: http://www.7-zip.org/ )
+
+Note: Will not extract with 'Move Completed' enabled
+"""
+__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__ if __long_description__ else __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),
+)
diff --git a/deluge/plugins/Label/TODO b/deluge/plugins/Label/TODO
new file mode 100644
index 0000000..c6a5daa
--- /dev/null
+++ b/deluge/plugins/Label/TODO
@@ -0,0 +1,11 @@
+*grey bars are hard-coded , use theme to get bg-color.
+*label sub-menu is broken on 1'st popup.
+*replacing/restoring the sidebar model is a hack
+*config should save a label on bottom ok-button, not a seperate save-button per label
+*filters : add "Traffic" , use label-core for filtering ; needs hooks in torrentview.
+*torrentview: bugs/hacks in adding and removing columns
+*webui is functional but not polished.
+*move_torrent_to is not implemeted
+*no client-side validation (could be solved by a ui.aclient exception-plugin)
+*expand/arrows in sidebar are disabled to save space, fix the space issue or implement an alternative
+*fix and move tracker_host column+status-field to core.
diff --git a/deluge/plugins/Label/deluge_label/__init__.py b/deluge/plugins/Label/deluge_label/__init__.py
new file mode 100644
index 0000000..a6c72f8
--- /dev/null
+++ b/deluge/plugins/Label/deluge_label/__init__.py
@@ -0,0 +1,37 @@
+#
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-2008 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 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/Label/deluge_label/common.py b/deluge/plugins/Label/deluge_label/common.py
new file mode 100644
index 0000000..eb47f13
--- /dev/null
+++ b/deluge/plugins/Label/deluge_label/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/Label/deluge_label/core.py b/deluge/plugins/Label/deluge_label/core.py
new file mode 100644
index 0000000..c28490b
--- /dev/null
+++ b/deluge/plugins/Label/deluge_label/core.py
@@ -0,0 +1,348 @@
+#
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-2008 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.
+#
+
+"""
+torrent-label core plugin.
+adds a status field for tracker.
+"""
+import logging
+import re
+
+import deluge.component as component
+from deluge.configmanager import ConfigManager
+from deluge.core.rpcserver import export
+from deluge.plugins.pluginbase import CorePluginBase
+
+log = logging.getLogger(__name__)
+
+RE_VALID = re.compile(r'[a-z0-9_\-\.]*\Z')
+
+KNOWN_STATES = ['Downloading', 'Seeding', 'Paused', 'Checking', 'Queued', 'Error']
+STATE = 'state'
+TRACKER = 'tracker'
+KEYWORD = 'keyword'
+LABEL = 'label'
+CONFIG_DEFAULTS = {
+ 'torrent_labels': {}, # torrent_id:label_id
+ 'labels': {}, # label_id:{name:value}
+}
+
+CORE_OPTIONS = ['auto_add_trackers']
+
+OPTIONS_DEFAULTS = {
+ 'apply_max': False,
+ 'max_download_speed': -1,
+ 'max_upload_speed': -1,
+ 'max_connections': -1,
+ 'max_upload_slots': -1,
+ 'prioritize_first_last': False,
+ 'apply_queue': False,
+ 'is_auto_managed': False,
+ 'stop_at_ratio': False,
+ 'stop_ratio': 2.0,
+ 'remove_at_ratio': False,
+ 'apply_move_completed': False,
+ 'move_completed': False,
+ 'move_completed_path': '',
+ 'auto_add': False,
+ 'auto_add_trackers': [],
+}
+
+NO_LABEL = 'No Label'
+
+
+def check_input(cond, message):
+ if not cond:
+ raise Exception(message)
+
+
+class Core(CorePluginBase):
+ """
+ self.labels = {label_id:label_options_dict}
+ self.torrent_labels = {torrent_id:label_id}
+ """
+
+ def enable(self):
+ log.info('*** Start Label plugin ***')
+ self.plugin = component.get('CorePluginManager')
+ self.plugin.register_status_field('label', self._status_get_label)
+
+ # __init__
+ core = component.get('Core')
+ self.config = ConfigManager('label.conf', defaults=CONFIG_DEFAULTS)
+ self.core_cfg = ConfigManager('core.conf')
+
+ # reduce typing, assigning some values to self...
+ self.torrents = core.torrentmanager.torrents
+ self.labels = self.config['labels']
+ self.torrent_labels = self.config['torrent_labels']
+
+ self.clean_initial_config()
+
+ component.get('EventManager').register_event_handler(
+ 'TorrentAddedEvent', self.post_torrent_add
+ )
+ component.get('EventManager').register_event_handler(
+ 'TorrentRemovedEvent', self.post_torrent_remove
+ )
+
+ # register tree:
+ component.get('FilterManager').register_tree_field(
+ 'label', self.init_filter_dict
+ )
+
+ log.debug('Label plugin enabled..')
+
+ def disable(self):
+ self.plugin.deregister_status_field('label')
+ component.get('FilterManager').deregister_tree_field('label')
+ component.get('EventManager').deregister_event_handler(
+ 'TorrentAddedEvent', self.post_torrent_add
+ )
+ component.get('EventManager').deregister_event_handler(
+ 'TorrentRemovedEvent', self.post_torrent_remove
+ )
+
+ def update(self):
+ pass
+
+ def init_filter_dict(self):
+ filter_dict = {label: 0 for label in self.labels}
+ filter_dict['All'] = len(self.torrents)
+ return filter_dict
+
+ # Plugin hooks #
+ def post_torrent_add(self, torrent_id, from_state):
+ if from_state:
+ return
+ log.debug('post_torrent_add')
+ torrent = self.torrents[torrent_id]
+
+ for label_id, options in self.labels.items():
+ if options['auto_add']:
+ if self._has_auto_match(torrent, options):
+ self.set_torrent(torrent_id, label_id)
+ return
+
+ def post_torrent_remove(self, torrent_id):
+ log.debug('post_torrent_remove')
+ if torrent_id in self.torrent_labels:
+ del self.torrent_labels[torrent_id]
+ self.config.save()
+
+ # Utils #
+ def clean_config(self):
+ """remove invalid data from config-file"""
+ for torrent_id, label_id in list(self.torrent_labels.items()):
+ if (label_id not in self.labels) or (torrent_id not in self.torrents):
+ log.debug('label: rm %s:%s', torrent_id, label_id)
+ del self.torrent_labels[torrent_id]
+
+ def clean_initial_config(self):
+ """
+ *add any new keys in OPTIONS_DEFAULTS
+ *set all None values to default <-fix development config
+ """
+ log.debug(list(self.labels))
+ for key in self.labels:
+ options = dict(OPTIONS_DEFAULTS)
+ options.update(self.labels[key])
+ self.labels[key] = options
+
+ for label, options in self.labels.items():
+ for key, value in options.items():
+ if value is None:
+ self.labels[label][key] = OPTIONS_DEFAULTS[key]
+
+ def save_config(self):
+ self.clean_config()
+ self.config.save()
+
+ @export
+ def get_labels(self):
+ return sorted(self.labels)
+
+ # Labels:
+ @export
+ def add(self, label_id):
+ """add a label
+ see label_set_options for more options.
+ """
+ label_id = label_id.lower()
+ check_input(
+ RE_VALID.match(label_id), _('Invalid label, valid characters:[a-z0-9_-]')
+ )
+ check_input(label_id, _('Empty Label'))
+ check_input(not (label_id in self.labels), _('Label already exists'))
+
+ self.labels[label_id] = dict(OPTIONS_DEFAULTS)
+ self.config.save()
+
+ @export
+ def remove(self, label_id):
+ """remove a label"""
+ check_input(label_id in self.labels, _('Unknown Label'))
+ del self.labels[label_id]
+ self.save_config()
+
+ def _set_torrent_options(self, torrent_id, label_id):
+ options = self.labels[label_id]
+ torrent = self.torrents[torrent_id]
+
+ if not options['move_completed_path']:
+ options['move_completed_path'] = '' # no None.
+
+ if options['apply_max']:
+ torrent.set_max_download_speed(options['max_download_speed'])
+ torrent.set_max_upload_speed(options['max_upload_speed'])
+ torrent.set_max_connections(options['max_connections'])
+ torrent.set_max_upload_slots(options['max_upload_slots'])
+ torrent.set_prioritize_first_last_pieces(options['prioritize_first_last'])
+
+ if options['apply_queue']:
+ torrent.set_auto_managed(options['is_auto_managed'])
+ torrent.set_stop_at_ratio(options['stop_at_ratio'])
+ torrent.set_stop_ratio(options['stop_ratio'])
+ torrent.set_remove_at_ratio(options['remove_at_ratio'])
+
+ if options['apply_move_completed']:
+ torrent.set_options(
+ {
+ 'move_completed': options['move_completed'],
+ 'move_completed_path': options['move_completed_path'],
+ }
+ )
+
+ def _unset_torrent_options(self, torrent_id, label_id):
+ options = self.labels[label_id]
+ torrent = self.torrents[torrent_id]
+
+ if options['apply_max']:
+ torrent.set_max_download_speed(
+ self.core_cfg.config['max_download_speed_per_torrent']
+ )
+ torrent.set_max_upload_speed(
+ self.core_cfg.config['max_upload_speed_per_torrent']
+ )
+ torrent.set_max_connections(
+ self.core_cfg.config['max_connections_per_torrent']
+ )
+ torrent.set_max_upload_slots(
+ self.core_cfg.config['max_upload_slots_per_torrent']
+ )
+ torrent.set_prioritize_first_last_pieces(
+ self.core_cfg.config['prioritize_first_last_pieces']
+ )
+
+ if options['apply_queue']:
+ torrent.set_auto_managed(self.core_cfg.config['auto_managed'])
+ torrent.set_stop_at_ratio(self.core_cfg.config['stop_seed_at_ratio'])
+ torrent.set_stop_ratio(self.core_cfg.config['stop_seed_ratio'])
+ torrent.set_remove_at_ratio(self.core_cfg.config['remove_seed_at_ratio'])
+
+ if options['apply_move_completed']:
+ torrent.set_options(
+ {
+ 'move_completed': self.core_cfg.config['move_completed'],
+ 'move_completed_path': self.core_cfg.config['move_completed_path'],
+ }
+ )
+
+ def _has_auto_match(self, torrent, label_options):
+ """match for auto_add fields"""
+ for tracker_match in label_options['auto_add_trackers']:
+ for tracker in torrent.trackers:
+ if tracker_match in tracker['url']:
+ return True
+ return False
+
+ @export
+ def set_options(self, label_id, options_dict):
+ """update the label options
+
+ options_dict :
+ {"max_download_speed":float(),
+ "max_upload_speed":float(),
+ "max_connections":int(),
+ "max_upload_slots":int(),
+ #"prioritize_first_last":bool(),
+ "apply_max":bool(),
+ "move_completed_to":string() or None
+ }
+ """
+ check_input(label_id in self.labels, _('Unknown Label'))
+ for key in options_dict:
+ if key not in OPTIONS_DEFAULTS:
+ raise Exception('label: Invalid options_dict key:%s' % key)
+
+ self.labels[label_id].update(options_dict)
+
+ # apply
+ for torrent_id, label in self.torrent_labels.items():
+ if label_id == label and torrent_id in self.torrents:
+ self._set_torrent_options(torrent_id, label_id)
+
+ # auto add
+ options = self.labels[label_id]
+ if options['auto_add']:
+ for torrent_id, torrent in self.torrents.items():
+ if self._has_auto_match(torrent, options):
+ self.set_torrent(torrent_id, label_id)
+
+ self.config.save()
+
+ @export
+ def get_options(self, label_id):
+ """returns the label options"""
+ return self.labels[label_id]
+
+ @export
+ def set_torrent(self, torrent_id, label_id):
+ """
+ assign a label to a torrent
+ removes a label if the label_id parameter is empty.
+ """
+ if label_id == NO_LABEL:
+ label_id = None
+
+ check_input((not label_id) or (label_id in self.labels), _('Unknown Label'))
+ check_input(torrent_id in self.torrents, _('Unknown Torrent'))
+
+ if torrent_id in self.torrent_labels:
+ self._unset_torrent_options(torrent_id, self.torrent_labels[torrent_id])
+ del self.torrent_labels[torrent_id]
+ self.clean_config()
+ if label_id:
+ self.torrent_labels[torrent_id] = label_id
+ self._set_torrent_options(torrent_id, label_id)
+
+ self.config.save()
+
+ @export
+ def get_config(self):
+ """see : label_set_config"""
+ return {
+ key: self.config[key] for key in CORE_OPTIONS if key in self.config.config
+ }
+
+ @export
+ def set_config(self, options):
+ """global_options:"""
+ if options:
+ for key, value in options.items:
+ if key in CORE_OPTIONS:
+ self.config[key] = value
+
+ self.config.save()
+
+ def _status_get_label(self, torrent_id):
+ return self.torrent_labels.get(torrent_id) or ''
diff --git a/deluge/plugins/Label/deluge_label/data/label.js b/deluge/plugins/Label/deluge_label/data/label.js
new file mode 100644
index 0000000..a0327e3
--- /dev/null
+++ b/deluge/plugins/Label/deluge_label/data/label.js
@@ -0,0 +1,635 @@
+/**
+ * label.js
+ *
+ * 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.preferences');
+
+/**
+ * @class Deluge.ux.preferences.LabelPage
+ * @extends Ext.Panel
+ */
+Deluge.ux.preferences.LabelPage = Ext.extend(Ext.Panel, {
+ title: _('Label'),
+ layout: 'fit',
+ border: false,
+
+ initComponent: function () {
+ Deluge.ux.preferences.LabelPage.superclass.initComponent.call(this);
+ fieldset = this.add({
+ xtype: 'fieldset',
+ border: false,
+ title: _('Label Preferences'),
+ autoHeight: true,
+ labelWidth: 1,
+ defaultType: 'panel',
+ });
+ fieldset.add({
+ border: false,
+ bodyCfg: {
+ html: _(
+ '<p>The Label plugin is enabled.</p><br>' +
+ '<p>To add, remove or edit labels right-click on the Label filter ' +
+ 'entry in the sidebar.</p><br>' +
+ '<p>To apply a label right-click on torrent(s).<p>'
+ ),
+ },
+ });
+ },
+});
+
+Ext.ns('Deluge.ux');
+
+/**
+ * @class Deluge.ux.AddLabelWindow
+ * @extends Ext.Window
+ */
+Deluge.ux.AddLabelWindow = Ext.extend(Ext.Window, {
+ title: _('Add Label'),
+ width: 300,
+ height: 100,
+ closeAction: 'hide',
+
+ initComponent: function () {
+ Deluge.ux.AddLabelWindow.superclass.initComponent.call(this);
+ this.addButton(_('Cancel'), this.onCancelClick, this);
+ this.addButton(_('Ok'), this.onOkClick, this);
+
+ this.form = this.add({
+ xtype: 'form',
+ height: 35,
+ baseCls: 'x-plain',
+ bodyStyle: 'padding:5px 5px 0',
+ defaultType: 'textfield',
+ labelWidth: 50,
+ items: [
+ {
+ fieldLabel: _('Name'),
+ name: 'name',
+ allowBlank: false,
+ width: 220,
+ listeners: {
+ specialkey: {
+ fn: function (field, e) {
+ if (e.getKey() == 13) this.onOkClick();
+ },
+ scope: this,
+ },
+ },
+ },
+ ],
+ });
+ },
+
+ onCancelClick: function () {
+ this.hide();
+ },
+
+ onOkClick: function () {
+ var label = this.form.getForm().getValues().name;
+ deluge.client.label.add(label, {
+ success: function () {
+ deluge.ui.update();
+ this.fireEvent('labeladded', label);
+ },
+ scope: this,
+ });
+ this.hide();
+ },
+
+ onHide: function (comp) {
+ Deluge.ux.AddLabelWindow.superclass.onHide.call(this, comp);
+ this.form.getForm().reset();
+ },
+
+ onShow: function (comp) {
+ Deluge.ux.AddLabelWindow.superclass.onShow.call(this, comp);
+ this.form.getForm().findField('name').focus(false, 150);
+ },
+});
+
+/**
+ * @class Deluge.ux.LabelOptionsWindow
+ * @extends Ext.Window
+ */
+Deluge.ux.LabelOptionsWindow = Ext.extend(Ext.Window, {
+ title: _('Label Options'),
+ width: 325,
+ height: 240,
+ closeAction: 'hide',
+
+ initComponent: function () {
+ Deluge.ux.LabelOptionsWindow.superclass.initComponent.call(this);
+ this.addButton(_('Cancel'), this.onCancelClick, this);
+ this.addButton(_('Ok'), this.onOkClick, this);
+
+ this.form = this.add({
+ xtype: 'form',
+ });
+
+ this.tabs = this.form.add({
+ xtype: 'tabpanel',
+ height: 175,
+ border: false,
+ items: [
+ {
+ title: _('Maximum'),
+ items: [
+ {
+ border: false,
+ items: [
+ {
+ xtype: 'fieldset',
+ border: false,
+ labelWidth: 1,
+ style: 'margin-bottom: 0px; padding-bottom: 0px;',
+ items: [
+ {
+ xtype: 'checkbox',
+ name: 'apply_max',
+ fieldLabel: '',
+ boxLabel: _(
+ 'Apply per torrent max settings:'
+ ),
+ listeners: {
+ check: this.onFieldChecked,
+ },
+ },
+ ],
+ },
+ {
+ xtype: 'fieldset',
+ border: false,
+ defaultType: 'spinnerfield',
+ style: 'margin-top: 0px; padding-top: 0px;',
+ items: [
+ {
+ fieldLabel: _('Download Speed'),
+ name: 'max_download_speed',
+ width: 80,
+ disabled: true,
+ value: -1,
+ minValue: -1,
+ },
+ {
+ fieldLabel: _('Upload Speed'),
+ name: 'max_upload_speed',
+ width: 80,
+ disabled: true,
+ value: -1,
+ minValue: -1,
+ },
+ {
+ fieldLabel: _('Upload Slots'),
+ name: 'max_upload_slots',
+ width: 80,
+ disabled: true,
+ value: -1,
+ minValue: -1,
+ },
+ {
+ fieldLabel: _('Connections'),
+ name: 'max_connections',
+ width: 80,
+ disabled: true,
+ value: -1,
+ minValue: -1,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ title: _('Queue'),
+ items: [
+ {
+ border: false,
+ items: [
+ {
+ xtype: 'fieldset',
+ border: false,
+ labelWidth: 1,
+ style: 'margin-bottom: 0px; padding-bottom: 0px;',
+ items: [
+ {
+ xtype: 'checkbox',
+ name: 'apply_queue',
+ fieldLabel: '',
+ boxLabel: _(
+ 'Apply queue settings:'
+ ),
+ listeners: {
+ check: this.onFieldChecked,
+ },
+ },
+ ],
+ },
+ {
+ xtype: 'fieldset',
+ border: false,
+ labelWidth: 1,
+ defaultType: 'checkbox',
+ style: 'margin-top: 0px; padding-top: 0px;',
+ defaults: {
+ style: 'margin-left: 20px',
+ },
+ items: [
+ {
+ boxLabel: _('Auto Managed'),
+ name: 'is_auto_managed',
+ disabled: true,
+ },
+ {
+ boxLabel: _('Stop seed at ratio:'),
+ name: 'stop_at_ratio',
+ disabled: true,
+ },
+ {
+ xtype: 'spinnerfield',
+ name: 'stop_ratio',
+ width: 60,
+ decimalPrecision: 2,
+ incrementValue: 0.1,
+ style: 'position: relative; left: 100px',
+ disabled: true,
+ },
+ {
+ boxLabel: _('Remove at ratio'),
+ name: 'remove_at_ratio',
+ disabled: true,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ title: _('Folders'),
+ items: [
+ {
+ border: false,
+ items: [
+ {
+ xtype: 'fieldset',
+ border: false,
+ labelWidth: 1,
+ style: 'margin-bottom: 0px; padding-bottom: 0px;',
+ items: [
+ {
+ xtype: 'checkbox',
+ name: 'apply_move_completed',
+ fieldLabel: '',
+ boxLabel: _(
+ 'Apply folder settings:'
+ ),
+ listeners: {
+ check: this.onFieldChecked,
+ },
+ },
+ ],
+ },
+ {
+ xtype: 'fieldset',
+ border: false,
+ labelWidth: 1,
+ defaultType: 'checkbox',
+ labelWidth: 1,
+ style: 'margin-top: 0px; padding-top: 0px;',
+ defaults: {
+ style: 'margin-left: 20px',
+ },
+ items: [
+ {
+ boxLabel: _('Move completed to:'),
+ name: 'move_completed',
+ disabled: true,
+ },
+ {
+ xtype: 'textfield',
+ name: 'move_completed_path',
+ width: 250,
+ disabled: true,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ title: _('Trackers'),
+ items: [
+ {
+ border: false,
+ items: [
+ {
+ xtype: 'fieldset',
+ border: false,
+ labelWidth: 1,
+ style: 'margin-bottom: 0px; padding-bottom: 0px;',
+ items: [
+ {
+ xtype: 'checkbox',
+ name: 'auto_add',
+ fieldLabel: '',
+ boxLabel: _(
+ 'Automatically apply label:'
+ ),
+ listeners: {
+ check: this.onFieldChecked,
+ },
+ },
+ ],
+ },
+ {
+ xtype: 'fieldset',
+ border: false,
+ labelWidth: 1,
+ style: 'margin-top: 0px; padding-top: 0px;',
+ defaults: {
+ style: 'margin-left: 20px',
+ },
+ defaultType: 'textarea',
+ items: [
+ {
+ boxLabel: _('Move completed to:'),
+ name: 'auto_add_trackers',
+ width: 250,
+ height: 100,
+ disabled: true,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ });
+ },
+
+ getLabelOptions: function () {
+ deluge.client.label.get_options(this.label, {
+ success: this.gotOptions,
+ scope: this,
+ });
+ },
+
+ gotOptions: function (options) {
+ this.form.getForm().setValues(options);
+ },
+
+ show: function (label) {
+ Deluge.ux.LabelOptionsWindow.superclass.show.call(this);
+ this.label = label;
+ this.setTitle(_('Label Options') + ': ' + this.label);
+ this.tabs.setActiveTab(0);
+ this.getLabelOptions();
+ },
+
+ onCancelClick: function () {
+ this.hide();
+ },
+
+ onOkClick: function () {
+ var values = this.form.getForm().getFieldValues();
+ if (values['auto_add_trackers']) {
+ values['auto_add_trackers'] =
+ values['auto_add_trackers'].split('\n');
+ }
+ deluge.client.label.set_options(this.label, values);
+ this.hide();
+ },
+
+ onFieldChecked: function (field, checked) {
+ var fs = field.ownerCt.nextSibling();
+ fs.items.each(function (field) {
+ field.setDisabled(!checked);
+ });
+ },
+});
+
+Ext.ns('Deluge.plugins');
+
+/**
+ * @class Deluge.plugins.LabelPlugin
+ * @extends Deluge.Plugin
+ */
+Deluge.plugins.LabelPlugin = Ext.extend(Deluge.Plugin, {
+ name: 'Label',
+
+ createMenu: function () {
+ this.labelMenu = new Ext.menu.Menu({
+ items: [
+ {
+ text: _('Add Label'),
+ iconCls: 'icon-add',
+ handler: this.onLabelAddClick,
+ scope: this,
+ },
+ {
+ text: _('Remove Label'),
+ disabled: true,
+ iconCls: 'icon-remove',
+ handler: this.onLabelRemoveClick,
+ scope: this,
+ },
+ {
+ text: _('Label Options'),
+ disabled: true,
+ handler: this.onLabelOptionsClick,
+ scope: this,
+ },
+ ],
+ });
+ },
+
+ setFilter: function (filter) {
+ filter.show_zero = true;
+
+ filter.list.on('contextmenu', this.onLabelContextMenu, this);
+ filter.header.on('contextmenu', this.onLabelHeaderContextMenu, this);
+ this.filter = filter;
+ },
+
+ updateTorrentMenu: function (states) {
+ this.torrentMenu.removeAll(true);
+ this.torrentMenu.addMenuItem({
+ text: _('No Label'),
+ label: '',
+ handler: this.onTorrentMenuClick,
+ scope: this,
+ });
+ for (var state in states) {
+ if (!state || state == 'All') continue;
+ this.torrentMenu.addMenuItem({
+ text: state,
+ label: state,
+ handler: this.onTorrentMenuClick,
+ scope: this,
+ });
+ }
+ },
+
+ onDisable: function () {
+ deluge.sidebar.un('filtercreate', this.onFilterCreate);
+ deluge.sidebar.un('afterfiltercreate', this.onAfterFilterCreate);
+ delete Deluge.FilterPanel.templates.label;
+ this.deregisterTorrentStatus('label');
+ deluge.menus.torrent.remove(this.tmSep);
+ deluge.menus.torrent.remove(this.tm);
+ deluge.preferences.removePage(this.prefsPage);
+ },
+
+ onEnable: function () {
+ this.prefsPage = deluge.preferences.addPage(
+ new Deluge.ux.preferences.LabelPage()
+ );
+ this.torrentMenu = new Ext.menu.Menu();
+
+ this.tmSep = deluge.menus.torrent.add({
+ xtype: 'menuseparator',
+ });
+
+ this.tm = deluge.menus.torrent.add({
+ text: _('Label'),
+ menu: this.torrentMenu,
+ });
+
+ var lbltpl =
+ '<div class="x-deluge-filter">' +
+ '<tpl if="filter">{filter}</tpl>' +
+ '<tpl if="!filter">No Label</tpl>' +
+ ' ({count})' +
+ '</div>';
+
+ if (deluge.sidebar.hasFilter('label')) {
+ var filter = deluge.sidebar.getFilter('label');
+ filter.list.columns[0].tpl = new Ext.XTemplate(lbltpl);
+ this.setFilter(filter);
+ this.updateTorrentMenu(filter.getStates());
+ filter.list.refresh();
+ } else {
+ deluge.sidebar.on('filtercreate', this.onFilterCreate, this);
+ deluge.sidebar.on(
+ 'afterfiltercreate',
+ this.onAfterFilterCreate,
+ this
+ );
+ Deluge.FilterPanel.templates.label = lbltpl;
+ }
+ this.registerTorrentStatus('label', _('Label'));
+ },
+
+ onAfterFilterCreate: function (sidebar, filter) {
+ if (filter.filter != 'label') return;
+ this.updateTorrentMenu(filter.getStates());
+ },
+
+ onFilterCreate: function (sidebar, filter) {
+ if (filter.filter != 'label') return;
+ this.setFilter(filter);
+ },
+
+ onLabelAddClick: function () {
+ if (!this.addWindow) {
+ this.addWindow = new Deluge.ux.AddLabelWindow();
+ this.addWindow.on('labeladded', this.onLabelAdded, this);
+ }
+ this.addWindow.show();
+ },
+
+ onLabelAdded: function (label) {
+ var filter = deluge.sidebar.getFilter('label');
+ var states = filter.getStates();
+ var statesArray = [];
+
+ for (state in states) {
+ if (!state || state == 'All') continue;
+ statesArray.push(state);
+ }
+
+ statesArray.push(label.toLowerCase());
+ statesArray.sort();
+
+ //console.log(states);
+ //console.log(statesArray);
+
+ states = {};
+
+ for (i = 0; i < statesArray.length; ++i) {
+ states[statesArray[i]] = 0;
+ }
+
+ this.updateTorrentMenu(states);
+ },
+
+ onLabelContextMenu: function (dv, i, node, e) {
+ e.preventDefault();
+ if (!this.labelMenu) this.createMenu();
+ var r = dv.getRecord(node).get('filter');
+ if (!r || r == 'All') {
+ this.labelMenu.items.get(1).setDisabled(true);
+ this.labelMenu.items.get(2).setDisabled(true);
+ } else {
+ this.labelMenu.items.get(1).setDisabled(false);
+ this.labelMenu.items.get(2).setDisabled(false);
+ }
+ dv.select(i);
+ this.labelMenu.showAt(e.getXY());
+ },
+
+ onLabelHeaderContextMenu: function (e, t) {
+ e.preventDefault();
+ if (!this.labelMenu) this.createMenu();
+ this.labelMenu.items.get(1).setDisabled(true);
+ this.labelMenu.items.get(2).setDisabled(true);
+ this.labelMenu.showAt(e.getXY());
+ },
+
+ onLabelOptionsClick: function () {
+ if (!this.labelOpts)
+ this.labelOpts = new Deluge.ux.LabelOptionsWindow();
+ this.labelOpts.show(this.filter.getState());
+ },
+
+ onLabelRemoveClick: function () {
+ var state = this.filter.getState();
+ deluge.client.label.remove(state, {
+ success: function () {
+ deluge.ui.update();
+ this.torrentMenu.items.each(function (item) {
+ if (item.text != state) return;
+ this.torrentMenu.remove(item);
+ var i = item;
+ }, this);
+ },
+ scope: this,
+ });
+ },
+
+ onTorrentMenuClick: function (item, e) {
+ var ids = deluge.torrents.getSelectedIds();
+ Ext.each(ids, function (id, i) {
+ if (ids.length == i + 1) {
+ deluge.client.label.set_torrent(id, item.label, {
+ success: function () {
+ deluge.ui.update();
+ },
+ });
+ } else {
+ deluge.client.label.set_torrent(id, item.label);
+ }
+ });
+ },
+});
+Deluge.registerPlugin('Label', Deluge.plugins.LabelPlugin);
diff --git a/deluge/plugins/Label/deluge_label/data/label_add.ui b/deluge/plugins/Label/deluge_label/data/label_add.ui
new file mode 100644
index 0000000..e550675
--- /dev/null
+++ b/deluge/plugins/Label/deluge_label/data/label_add.ui
@@ -0,0 +1,172 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <object class="GtkDialog" id="dlg_label_add">
+ <property name="can_focus">False</property>
+ <property name="border_width">5</property>
+ <property name="title" translatable="yes">Add Label</property>
+ <property name="resizable">False</property>
+ <property name="modal">True</property>
+ <property name="window_position">mouse</property>
+ <property name="destroy_with_parent">True</property>
+ <property name="type_hint">dialog</property>
+ <property name="skip_taskbar_hint">True</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child internal-child="vbox">
+ <object class="GtkBox" id="dialog-vbox2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">2</property>
+ <child internal-child="action_area">
+ <object class="GtkButtonBox" id="dialog-action_area2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="button2">
+ <property name="label">gtk-cancel</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_cancel" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="button1">
+ <property name="label">gtk-ok</property>
+ <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="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="on_add_ok" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="vbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">5</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="hbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">5</property>
+ <child>
+ <object class="GtkImage" id="image1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-add</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label10">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;b&gt;Add Label&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHSeparator" id="hseparator2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="hbox2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">5</property>
+ <child>
+ <object class="GtkLabel" id="label11">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Name:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="txt_add">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="activates_default">True</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="0">button2</action-widget>
+ <action-widget response="0">button1</action-widget>
+ </action-widgets>
+ </object>
+</interface>
diff --git a/deluge/plugins/Label/deluge_label/data/label_options.ui b/deluge/plugins/Label/deluge_label/data/label_options.ui
new file mode 100644
index 0000000..d390865
--- /dev/null
+++ b/deluge/plugins/Label/deluge_label/data/label_options.ui
@@ -0,0 +1,723 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <object class="GtkAdjustment" id="adjustment1">
+ <property name="lower">-1</property>
+ <property name="upper">9999</property>
+ <property name="value">-1</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">10</property>
+ </object>
+ <object class="GtkAdjustment" id="adjustment2">
+ <property name="lower">-1</property>
+ <property name="upper">9999</property>
+ <property name="value">-1</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">10</property>
+ </object>
+ <object class="GtkAdjustment" id="adjustment3">
+ <property name="lower">-1</property>
+ <property name="upper">9999</property>
+ <property name="value">-1</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">10</property>
+ </object>
+ <object class="GtkAdjustment" id="adjustment4">
+ <property name="lower">-1</property>
+ <property name="upper">9999</property>
+ <property name="value">-1</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">10</property>
+ </object>
+ <object class="GtkAdjustment" id="adjustment5">
+ <property name="lower">0.10000000000000001</property>
+ <property name="upper">100</property>
+ <property name="value">2</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">10</property>
+ </object>
+ <object class="GtkTextBuffer" id="textbuffer1">
+ <property name="text" translatable="yes">tracker1.org</property>
+ </object>
+ <object class="GtkDialog" id="dlg_label_options">
+ <property name="can_focus">False</property>
+ <property name="border_width">5</property>
+ <property name="title" translatable="yes">Label Options</property>
+ <property name="modal">True</property>
+ <property name="window_position">mouse</property>
+ <property name="destroy_with_parent">True</property>
+ <property name="type_hint">dialog</property>
+ <property name="skip_taskbar_hint">True</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child internal-child="vbox">
+ <object class="GtkBox" id="dialog-vbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">5</property>
+ <child internal-child="action_area">
+ <object class="GtkButtonBox" id="dialog-action_area1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="button4">
+ <property name="label">gtk-cancel</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_options_cancel" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="button3">
+ <property name="label">gtk-ok</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_options_ok" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="hbox3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">5</property>
+ <child>
+ <object class="GtkImage" id="image2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-preferences</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label_header">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;b&gt;Label Options&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHSeparator" id="hseparator1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkNotebook" id="notebook2">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <child>
+ <object class="GtkFrame" id="frame1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="border_width">2</property>
+ <property name="label_xalign">0</property>
+ <property name="shadow_type">none</property>
+ <child>
+ <object class="GtkAlignment" id="alignment1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="left_padding">12</property>
+ <child>
+ <object class="GtkTable" id="table1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="n_rows">5</property>
+ <property name="n_columns">4</property>
+ <property name="column_spacing">5</property>
+ <property name="row_spacing">5</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="left_attach">3</property>
+ <property name="right_attach">4</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="max_upload_speed">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="adjustment">adjustment1</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="x_options"/>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label24">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">KiB/s</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="max_download_speed">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="adjustment">adjustment2</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="x_options"/>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label6">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Upload Slots:</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Upload Speed:</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">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Download Speed:</property>
+ <property name="use_markup">True</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">KiB/s</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="max_upload_slots">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="adjustment">adjustment3</property>
+ <property name="numeric">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="x_options"/>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Connections:</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="max_connections">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="adjustment">adjustment4</property>
+ <property name="numeric">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ <property name="x_options"/>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label14">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="right_attach">2</property>
+ <property name="top_attach">4</property>
+ <property name="bottom_attach">5</property>
+ <property name="x_options">GTK_FILL</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkCheckButton" id="apply_max">
+ <property name="label" translatable="yes">Apply per torrent max settings:</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="label7">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Maximum</property>
+ </object>
+ <packing>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFrame" id="frame2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="border_width">2</property>
+ <property name="label_xalign">0</property>
+ <property name="shadow_type">none</property>
+ <child>
+ <object class="GtkAlignment" id="alignment2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="left_padding">12</property>
+ <child>
+ <object class="GtkTable" id="table2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="n_rows">4</property>
+ <property name="n_columns">3</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label16">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="is_auto_managed">
+ <property name="label" translatable="yes">Auto Managed</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="stop_at_ratio">
+ <property name="label" translatable="yes">Stop seed at ratio:</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</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">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="remove_at_ratio">
+ <property name="label" translatable="yes">Remove at ratio</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="stop_ratio">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="adjustment">adjustment5</property>
+ <property name="digits">2</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="x_options"/>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label13">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="right_attach">2</property>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ <property name="x_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkCheckButton" id="apply_queue">
+ <property name="label" translatable="yes">Apply Queue settings:</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="label8">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Queue</property>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFrame" id="frame3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="border_width">2</property>
+ <property name="label_xalign">0</property>
+ <property name="shadow_type">none</property>
+ <child>
+ <object class="GtkAlignment" id="alignment3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="left_padding">12</property>
+ <child>
+ <object class="GtkTable" id="table3">
+ <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="GtkCheckButton" id="move_completed">
+ <property name="label" translatable="yes">Move completed to:</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="right_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label9">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="right_attach">2</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFileChooserButton" id="move_completed_path">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="action">select-folder</property>
+ </object>
+ <packing>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="move_completed_path_entry">
+ <property name="can_focus">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">GTK_FILL</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkCheckButton" id="apply_move_completed">
+ <property name="label" translatable="yes">Apply folder settings:</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="label21">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Folders</property>
+ </object>
+ <packing>
+ <property name="position">2</property>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFrame" id="frame4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="border_width">2</property>
+ <property name="label_xalign">0</property>
+ <property name="shadow_type">none</property>
+ <child>
+ <object class="GtkAlignment" id="alignment4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="left_padding">12</property>
+ <child>
+ <object class="GtkBox" id="vbox2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="shadow_type">in</property>
+ <child>
+ <object class="GtkTextView" id="auto_add_trackers">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="buffer">textbuffer1</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label23">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;i&gt;(1 line per tracker)&lt;/i&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkCheckButton" id="auto_add">
+ <property name="label" translatable="yes">Automatically apply label:</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="label99">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Trackers</property>
+ </object>
+ <packing>
+ <property name="position">3</property>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="0">button4</action-widget>
+ <action-widget response="0">button3</action-widget>
+ </action-widgets>
+ </object>
+</interface>
diff --git a/deluge/plugins/Label/deluge_label/data/label_pref.ui b/deluge/plugins/Label/deluge_label/data/label_pref.ui
new file mode 100644
index 0000000..81edc37
--- /dev/null
+++ b/deluge/plugins/Label/deluge_label/data/label_pref.ui
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <object class="GtkWindow" id="window1">
+ <property name="can_focus">False</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkBox" id="label_prefs_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">5</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkFrame" id="frame3">
+ <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="alignment3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="left_padding">12</property>
+ <child>
+ <object class="GtkLabel" id="label6">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;i&gt;Use the sidebar to add,edit and remove labels. &lt;/i&gt;
+</property>
+ <property name="use_markup">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;b&gt;Labels&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/deluge/plugins/Label/deluge_label/gtkui/__init__.py b/deluge/plugins/Label/deluge_label/gtkui/__init__.py
new file mode 100644
index 0000000..6170716
--- /dev/null
+++ b/deluge/plugins/Label/deluge_label/gtkui/__init__.py
@@ -0,0 +1,74 @@
+#
+# Copyright (C) 2008 Martijn Voncken <mvoncken@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 import component # for systray
+from deluge.plugins.pluginbase import Gtk3PluginBase
+
+from . import label_config, sidebar_menu, submenu
+
+log = logging.getLogger(__name__)
+
+NO_LABEL = 'No Label'
+
+
+def cell_data_label(column, cell, model, row, data):
+ cell.set_property('text', str(model.get_value(row, data)))
+
+
+class GtkUI(Gtk3PluginBase):
+ def start(self):
+ if self.label_menu:
+ self.label_menu.on_show()
+
+ def enable(self):
+ self.plugin = component.get('PluginManager')
+ self.torrentmenu = component.get('MenuBar').torrentmenu
+ self.label_menu = None
+ self.labelcfg = None
+ self.sidebar_menu = None
+ self.load_interface()
+
+ def disable(self):
+ if self.label_menu in self.torrentmenu.get_children():
+ self.torrentmenu.remove(self.label_menu)
+
+ self.labelcfg.unload()
+ self.sidebar_menu.unload()
+ del self.sidebar_menu
+
+ component.get('TorrentView').remove_column(_('Label'))
+
+ def load_interface(self):
+ # sidebar
+ # disabled
+ if not self.sidebar_menu:
+ self.sidebar_menu = sidebar_menu.LabelSidebarMenu()
+ # self.sidebar.load()
+
+ # menu:
+ log.debug('add items to torrentview-popup menu.')
+ self.label_menu = submenu.LabelMenu()
+ self.torrentmenu.append(self.label_menu)
+ self.label_menu.show_all()
+
+ # columns:
+ self.load_columns()
+
+ # config:
+ if not self.labelcfg:
+ self.labelcfg = label_config.LabelConfig(self.plugin)
+ self.labelcfg.load()
+
+ log.debug('Finished loading Label plugin')
+
+ def load_columns(self):
+ log.debug('add columns')
+
+ component.get('TorrentView').add_text_column(_('Label'), status_field=['label'])
diff --git a/deluge/plugins/Label/deluge_label/gtkui/label_config.py b/deluge/plugins/Label/deluge_label/gtkui/label_config.py
new file mode 100644
index 0000000..26c827e
--- /dev/null
+++ b/deluge/plugins/Label/deluge_label/gtkui/label_config.py
@@ -0,0 +1,58 @@
+#
+# Copyright (C) 2008 Martijn Voncken <mvoncken@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 gi.repository.Gtk import Builder
+
+from deluge.ui.client import client
+
+from ..common import get_resource
+
+log = logging.getLogger(__name__)
+
+
+class LabelConfig:
+ """
+ there used to be some options here...
+ """
+
+ def __init__(self, plugin):
+ self.plugin = plugin
+
+ def load(self):
+ log.debug('Adding Label Preferences page')
+ builder = Builder()
+ builder.add_from_file(get_resource('label_pref.ui'))
+
+ self.plugin.add_preferences_page(
+ _('Label'), builder.get_object('label_prefs_box')
+ )
+ self.plugin.register_hook('on_show_prefs', self.load_settings)
+ self.plugin.register_hook('on_apply_prefs', self.on_apply_prefs)
+
+ self.load_settings()
+
+ def unload(self):
+ self.plugin.remove_preferences_page(_('Label'))
+ self.plugin.deregister_hook('on_apply_prefs', self.on_apply_prefs)
+ self.plugin.deregister_hook('on_show_prefs', self.load_settings)
+
+ def load_settings(self, widget=None, data=None):
+ client.label.get_config().addCallback(self.cb_global_options)
+
+ def cb_global_options(self, options):
+ log.debug('options=%s', options)
+
+ # for id in self.chk_ids:
+ # self.glade.get_widget(id).set_active(bool(options[id]))
+
+ def on_apply_prefs(self):
+ options = {}
+ # update options dict here.
+ client.label.set_config(options)
diff --git a/deluge/plugins/Label/deluge_label/gtkui/sidebar_menu.py b/deluge/plugins/Label/deluge_label/gtkui/sidebar_menu.py
new file mode 100644
index 0000000..9d164b2
--- /dev/null
+++ b/deluge/plugins/Label/deluge_label/gtkui/sidebar_menu.py
@@ -0,0 +1,259 @@
+#
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007 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 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.ui.client import client
+
+# isort:imports-localfolder
+from ..common import get_resource
+
+log = logging.getLogger(__name__)
+
+NO_LABEL = 'No Label'
+
+
+# menu
+class LabelSidebarMenu:
+ def __init__(self):
+ self.treeview = component.get('FilterTreeView')
+ self.menu = self.treeview.menu
+ self.items = []
+
+ # add items, in reverse order, because they are prepended.
+ sep = Gtk.SeparatorMenuItem()
+ self.items.append(sep)
+ self.menu.prepend(sep)
+ self._add_item('options', _('Label _Options'))
+ self._add_item('remove', _('_Remove Label'))
+ self._add_item('add', _('_Add Label'))
+
+ self.menu.show_all()
+ # dialogs:
+ self.add_dialog = AddDialog()
+ self.options_dialog = OptionsDialog()
+ # hooks:
+ self.menu.connect('show', self.on_show, None)
+
+ def _add_item(self, item_id, label):
+ """
+ id is automatically-added as self.item_<id>
+ """
+ item = Gtk.MenuItem.new_with_mnemonic(label)
+ func = getattr(self, 'on_%s' % item_id)
+ item.connect('activate', func)
+ self.menu.prepend(item)
+ setattr(self, 'item_%s' % item_id, item)
+ self.items.append(item)
+ return item
+
+ def on_add(self, event=None):
+ self.add_dialog.show()
+
+ def on_remove(self, event=None):
+ client.label.remove(self.treeview.value)
+
+ def on_options(self, event=None):
+ self.options_dialog.show(self.treeview.value)
+
+ def on_show(self, widget=None, data=None):
+ """No Label:disable options/del."""
+ log.debug('label-sidebar-popup:on-show')
+
+ cat = self.treeview.cat
+ label = self.treeview.value
+ if cat == 'label' or (cat == 'cat' and label == 'label'):
+ # is a label : show menu-items
+ for item in self.items:
+ item.show()
+ # default items
+ sensitive = (label not in (NO_LABEL, None, '', 'All')) and (cat != 'cat')
+ for item in self.items:
+ item.set_sensitive(sensitive)
+
+ # add is always enabled.
+ self.item_add.set_sensitive(True)
+ else:
+ # not a label -->hide everything.
+ for item in self.items:
+ item.hide()
+
+ def unload(self):
+ log.debug('disable01')
+ for item in list(self.items):
+ item.hide()
+ item.destroy()
+ log.debug('disable02')
+ self.items = []
+
+
+# dialogs:
+class AddDialog:
+ def __init__(self):
+ pass
+
+ def show(self):
+ self.builder = Gtk.Builder()
+ self.builder.add_from_file(get_resource('label_add.ui'))
+ self.dialog = self.builder.get_object('dlg_label_add')
+ self.dialog.set_transient_for(component.get('MainWindow').window)
+
+ self.builder.connect_signals(self)
+ self.dialog.run()
+
+ def on_add_ok(self, event=None):
+ value = self.builder.get_object('txt_add').get_text()
+ client.label.add(value)
+ self.dialog.destroy()
+
+ def on_add_cancel(self, event=None):
+ self.dialog.destroy()
+
+
+class OptionsDialog:
+ spin_ids = ['max_download_speed', 'max_upload_speed', 'stop_ratio']
+ spin_int_ids = ['max_upload_slots', 'max_connections']
+ chk_ids = [
+ 'apply_max',
+ 'apply_queue',
+ 'stop_at_ratio',
+ 'apply_queue',
+ 'remove_at_ratio',
+ 'apply_move_completed',
+ 'move_completed',
+ 'is_auto_managed',
+ 'auto_add',
+ ]
+
+ # list of tuples, because order matters when nesting.
+ sensitive_groups = [
+ (
+ 'apply_max',
+ [
+ 'max_download_speed',
+ 'max_upload_speed',
+ 'max_upload_slots',
+ 'max_connections',
+ ],
+ ),
+ ('apply_queue', ['is_auto_managed', 'stop_at_ratio']),
+ ('stop_at_ratio', ['remove_at_ratio', 'stop_ratio']), # nested
+ ('apply_move_completed', ['move_completed']),
+ ('move_completed', ['move_completed_path']), # nested
+ ('auto_add', ['auto_add_trackers']),
+ ]
+
+ def __init__(self):
+ pass
+
+ def show(self, label):
+ self.label = label
+ self.builder = Gtk.Builder()
+ self.builder.add_from_file(get_resource('label_options.ui'))
+ self.dialog = self.builder.get_object('dlg_label_options')
+ self.dialog.set_transient_for(component.get('MainWindow').window)
+ self.builder.connect_signals(self)
+ # Show the label name in the header label
+ self.builder.get_object('label_header').set_markup(
+ '<b>{}:</b> {}'.format(_('Label Options'), self.label)
+ )
+
+ for chk_id, group in self.sensitive_groups:
+ chk = self.builder.get_object(chk_id)
+ chk.connect('toggled', self.apply_sensitivity)
+
+ client.label.get_options(self.label).addCallback(self.load_options)
+
+ self.dialog.run()
+
+ def load_options(self, options):
+ log.debug(list(options))
+
+ for spin_id in self.spin_ids + self.spin_int_ids:
+ self.builder.get_object(spin_id).set_value(options[spin_id])
+ for chk_id in self.chk_ids:
+ self.builder.get_object(chk_id).set_active(bool(options[chk_id]))
+
+ if client.is_localhost():
+ self.builder.get_object('move_completed_path').set_filename(
+ options['move_completed_path']
+ )
+ self.builder.get_object('move_completed_path').show()
+ self.builder.get_object('move_completed_path_entry').hide()
+ else:
+ self.builder.get_object('move_completed_path_entry').set_text(
+ options['move_completed_path']
+ )
+ self.builder.get_object('move_completed_path_entry').show()
+ self.builder.get_object('move_completed_path').hide()
+
+ self.builder.get_object('auto_add_trackers').get_buffer().set_text(
+ '\n'.join(options['auto_add_trackers'])
+ )
+
+ self.apply_sensitivity()
+
+ def on_options_ok(self, event=None):
+ """Save options."""
+ options = {}
+
+ for spin_id in self.spin_ids:
+ options[spin_id] = self.builder.get_object(spin_id).get_value()
+ for spin_int_id in self.spin_int_ids:
+ options[spin_int_id] = self.builder.get_object(
+ spin_int_id
+ ).get_value_as_int()
+ for chk_id in self.chk_ids:
+ options[chk_id] = self.builder.get_object(chk_id).get_active()
+
+ if client.is_localhost():
+ options['move_completed_path'] = self.builder.get_object(
+ 'move_completed_path'
+ ).get_filename()
+ else:
+ options['move_completed_path'] = self.builder.get_object(
+ 'move_completed_path_entry'
+ ).get_text()
+
+ buff = self.builder.get_object(
+ 'auto_add_trackers'
+ ).get_buffer() # sometimes I hate gtk...
+ tracker_lst = (
+ buff.get_text(
+ buff.get_start_iter(), buff.get_end_iter(), include_hidden_chars=False
+ )
+ .strip()
+ .split('\n')
+ )
+ options['auto_add_trackers'] = [
+ x for x in tracker_lst if x
+ ] # filter out empty lines.
+
+ log.debug(options)
+ client.label.set_options(self.label, options)
+ self.dialog.destroy()
+
+ def apply_sensitivity(self, event=None):
+ for chk_id, sensitive_list in self.sensitive_groups:
+ chk = self.builder.get_object(chk_id)
+ sens = chk.get_active() and chk.get_property('sensitive')
+ for widget_id in sensitive_list:
+ self.builder.get_object(widget_id).set_sensitive(sens)
+
+ def on_options_cancel(self, event=None):
+ self.dialog.destroy()
diff --git a/deluge/plugins/Label/deluge_label/gtkui/submenu.py b/deluge/plugins/Label/deluge_label/gtkui/submenu.py
new file mode 100644
index 0000000..54b6594
--- /dev/null
+++ b/deluge/plugins/Label/deluge_label/gtkui/submenu.py
@@ -0,0 +1,62 @@
+#
+# Copyright (C) 2008 Martijn Voncken <mvoncken@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 gi.repository.Gtk import Menu, MenuItem
+
+from deluge import component # for systray
+from deluge.ui.client import client
+
+log = logging.getLogger(__name__)
+
+
+# Deferred Translation
+def _(message):
+ return message
+
+
+NO_LABEL = _('No Label')
+del _
+
+
+class LabelMenu(MenuItem):
+ def __init__(self):
+ MenuItem.__init__(self, _('Label')) # noqa: F821
+
+ self.sub_menu = Menu()
+ self.set_submenu(self.sub_menu)
+ self.items = []
+
+ # attach..
+ self.sub_menu.connect('show', self.on_show, None)
+
+ def get_torrent_ids(self):
+ return component.get('TorrentView').get_selected_torrents()
+
+ def on_show(self, widget=None, data=None):
+ log.debug('label-on-show')
+ client.label.get_labels().addCallback(self.cb_labels)
+
+ def cb_labels(self, labels):
+ for child in self.sub_menu.get_children():
+ self.sub_menu.remove(child)
+ for label in [NO_LABEL] + list(labels):
+ if label == NO_LABEL:
+ item = MenuItem(_(NO_LABEL)) # noqa: F821
+ else:
+ item = MenuItem(label)
+ item.connect('activate', self.on_select_label, label)
+ self.sub_menu.append(item)
+ self.show_all()
+
+ def on_select_label(self, widget=None, label_id=None):
+ log.debug('select label:%s,%s', label_id, self.get_torrent_ids())
+ for torrent_id in self.get_torrent_ids():
+ client.label.set_torrent(torrent_id, label_id)
diff --git a/deluge/plugins/Label/deluge_label/test.py b/deluge/plugins/Label/deluge_label/test.py
new file mode 100644
index 0000000..739bae4
--- /dev/null
+++ b/deluge/plugins/Label/deluge_label/test.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+#
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2008 Martijn Voncken <mvoncken@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.ui.client import sclient
+
+sclient.set_core_uri()
+
+print(sclient.get_enabled_plugins())
+
+# enable plugin.
+if 'label' not in sclient.get_enabled_plugins():
+ sclient.enable_plugin('label')
+
+
+# test labels.
+print('#init labels')
+try:
+ sclient.label_remove('test')
+except Exception:
+ pass
+sess_id = sclient.get_session_state()[0]
+
+print('#add')
+sclient.label_add('test')
+print('#set')
+sclient.label_set_torrent(id, 'test')
+
+print(sclient.get_torrents_status({'label': 'test'}, 'name'))
+
+
+print('#set options')
+sclient.label_set_options('test', {'max_download_speed': 999}, True)
+print(sclient.get_torrent_status(sess_id, ['max_download_speed']), '999')
+sclient.label_set_options('test', {'max_download_speed': 9}, True)
+print(sclient.get_torrent_status(sess_id, ['max_download_speed']), '9')
+sclient.label_set_options('test', {'max_download_speed': 888}, False)
+print(sclient.get_torrent_status(sess_id, ['max_download_speed']), '9 (888)')
+
+print(sclient.get_torrent_status(sess_id, ['name', 'tracker_host', 'label']))
diff --git a/deluge/plugins/Label/deluge_label/webui.py b/deluge/plugins/Label/deluge_label/webui.py
new file mode 100644
index 0000000..9ccfa92
--- /dev/null
+++ b/deluge/plugins/Label/deluge_label/webui.py
@@ -0,0 +1,24 @@
+#
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-2008 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 logging
+
+from deluge.plugins.pluginbase import WebPluginBase
+
+from .common import get_resource
+
+log = logging.getLogger(__name__)
+
+
+class WebUI(WebPluginBase):
+ scripts = [get_resource('label.js')]
+ debug_scripts = scripts
diff --git a/deluge/plugins/Label/setup.py b/deluge/plugins/Label/setup.py
new file mode 100644
index 0000000..f8f2c5d
--- /dev/null
+++ b/deluge/plugins/Label/setup.py
@@ -0,0 +1,45 @@
+#
+# Copyright (C) 2008 Martijn Voncken <mvoncken@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__ = 'Label'
+__author__ = 'Martijn Voncken'
+__author_email__ = 'mvoncken@gmail.com'
+__version__ = '0.3'
+__url__ = 'http://deluge-torrent.org'
+__license__ = 'GPLv3'
+__description__ = 'Allows labels to be assigned to torrents'
+__long_description__ = """
+Allows labels to be assigned to torrents
+
+Also offers filters on state, tracker and keywords
+"""
+__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),
+)
diff --git a/deluge/plugins/Notifications/create_dev_link.sh b/deluge/plugins/Notifications/create_dev_link.sh
new file mode 100755
index 0000000..5e04057
--- /dev/null
+++ b/deluge/plugins/Notifications/create_dev_link.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+BASEDIR=$(cd `dirname $0` && pwd)
+CONFIG_DIR=$( test -z $1 && echo "" || echo "$1")
+[ -d "$CONFIG_DIR/plugins" ] || echo "Config dir "$CONFIG_DIR" is either not a directory or is not a proper deluge config directory. Exiting"
+[ -d "$CONFIG_DIR/plugins" ] || exit 1
+cd $BASEDIR
+test -d $BASEDIR/temp || mkdir $BASEDIR/temp
+export PYTHONPATH=$BASEDIR/temp
+python setup.py build develop --install-dir $BASEDIR/temp
+cp $BASEDIR/temp/*.egg-link $CONFIG_DIR/plugins
+rm -fr $BASEDIR/temp
diff --git a/deluge/plugins/Notifications/deluge_notifications/__init__.py b/deluge/plugins/Notifications/deluge_notifications/__init__.py
new file mode 100644
index 0000000..d52b48d
--- /dev/null
+++ b/deluge/plugins/Notifications/deluge_notifications/__init__.py
@@ -0,0 +1,38 @@
+#
+# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
+# 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/Notifications/deluge_notifications/common.py b/deluge/plugins/Notifications/deluge_notifications/common.py
new file mode 100644
index 0000000..9993f5c
--- /dev/null
+++ b/deluge/plugins/Notifications/deluge_notifications/common.py
@@ -0,0 +1,114 @@
+#
+# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
+# 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 os.path
+
+from pkg_resources import resource_filename
+from twisted.internet import defer
+
+from deluge import component
+from deluge.event import known_events
+
+log = logging.getLogger(__name__)
+
+
+def get_resource(filename):
+ return resource_filename(__package__, os.path.join('data', filename))
+
+
+class CustomNotifications:
+ def __init__(self, plugin_name=None):
+ self.custom_notifications = {'email': {}, 'popup': {}, 'blink': {}, 'sound': {}}
+
+ def enable(self):
+ pass
+
+ def disable(self):
+ for kind in self.custom_notifications:
+ for eventtype in list(self.custom_notifications[kind]):
+ wrapper, handler = self.custom_notifications[kind][eventtype]
+ self._deregister_custom_provider(kind, eventtype)
+
+ def _handle_custom_providers(self, kind, eventtype, *args, **kwargs):
+ log.debug(
+ 'Calling CORE custom %s providers for %s: %s %s',
+ kind,
+ eventtype,
+ args,
+ kwargs,
+ )
+ if eventtype in self.config['subscriptions'][kind]:
+ wrapper, handler = self.custom_notifications[kind][eventtype]
+ log.debug('Found handler for kind %s: %s', kind, handler)
+ custom_notif_func = getattr(self, 'handle_custom_%s_notification' % kind)
+ d = defer.maybeDeferred(handler, *args, **kwargs)
+ d.addCallback(custom_notif_func, eventtype)
+ d.addCallback(self._on_notify_sucess, kind)
+ d.addErrback(self._on_notify_failure, kind)
+ return d
+
+ def _register_custom_provider(self, kind, eventtype, handler):
+ if not self._handled_eventtype(eventtype, handler):
+ return defer.succeed('Event not handled')
+ if eventtype not in self.custom_notifications:
+
+ def wrapper(*args, **kwargs):
+ return self._handle_custom_providers(kind, eventtype, *args, **kwargs)
+
+ self.custom_notifications[kind][eventtype] = (wrapper, handler)
+ else:
+ wrapper, handler = self.custom_notifications[kind][eventtype]
+ try:
+ component.get('EventManager').register_event_handler(eventtype, wrapper)
+ except KeyError:
+ from deluge.ui.client import client
+
+ client.register_event_handler(eventtype, wrapper)
+
+ def _deregister_custom_provider(self, kind, eventtype):
+ try:
+ wrapper, handler = self.custom_notifications[kind][eventtype]
+ try:
+ component.get('EventManager').deregister_event_handler(
+ eventtype, wrapper
+ )
+ except KeyError:
+ from deluge.ui.client import client
+
+ client.deregister_event_handler(eventtype, wrapper)
+ self.custom_notifications[kind].pop(eventtype)
+ except KeyError:
+ pass
+
+ def _handled_eventtype(self, eventtype, handler):
+ if eventtype not in known_events:
+ log.error('The event "%s" is not known', eventtype)
+ return False
+ if known_events[eventtype].__module__.startswith('deluge.event'):
+ if handler.__self__ is self:
+ return True
+ log.error(
+ 'You cannot register custom notification providers '
+ 'for built-in event types.'
+ )
+ return False
+ return True
+
+ def _on_notify_sucess(self, result, kind):
+ log.debug('Notification success using %s: %s', kind, result)
+ return result
+
+ def _on_notify_failure(self, failure, kind):
+ log.debug('Notification failure using %s: %s', kind, failure)
+ return failure
diff --git a/deluge/plugins/Notifications/deluge_notifications/core.py b/deluge/plugins/Notifications/deluge_notifications/core.py
new file mode 100644
index 0000000..aa200f9
--- /dev/null
+++ b/deluge/plugins/Notifications/deluge_notifications/core.py
@@ -0,0 +1,228 @@
+#
+# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
+# 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 smtplib
+from email.utils import formatdate
+
+from twisted.internet import defer, threads
+
+import deluge.configmanager
+from deluge import component
+from deluge.core.rpcserver import export
+from deluge.event import known_events
+from deluge.plugins.pluginbase import CorePluginBase
+
+from .common import CustomNotifications
+
+log = logging.getLogger(__name__)
+
+DEFAULT_PREFS = {
+ 'smtp_enabled': False,
+ 'smtp_host': '',
+ 'smtp_port': 25,
+ 'smtp_user': '',
+ 'smtp_pass': '',
+ 'smtp_from': '',
+ 'smtp_tls': False, # SSL or TLS
+ 'smtp_recipients': [],
+ # Subscriptions
+ 'subscriptions': {'email': []},
+}
+
+
+class CoreNotifications(CustomNotifications):
+ def __init__(self, plugin_name=None):
+ CustomNotifications.__init__(self, plugin_name)
+
+ def enable(self):
+ CustomNotifications.enable(self)
+ self.register_custom_email_notification(
+ 'TorrentFinishedEvent', self._on_torrent_finished_event
+ )
+
+ def disable(self):
+ self.deregister_custom_email_notification('TorrentFinishedEvent')
+ CustomNotifications.disable(self)
+
+ def register_custom_email_notification(self, eventtype, handler):
+ """This is used to register email notifications for custom event types.
+
+ :param event: str, the event name
+ :param handler: function, to be called when `:param:event` is emitted
+
+ Your handler should return a tuple of (email_subject, email_contents).
+ """
+ self._register_custom_provider('email', eventtype, handler)
+
+ def deregister_custom_email_notification(self, eventtype):
+ self._deregister_custom_provider('email', eventtype)
+
+ def handle_custom_email_notification(self, result, eventtype):
+ if not self.config['smtp_enabled']:
+ return defer.succeed('SMTP notification not enabled.')
+ subject, message = result
+ log.debug(
+ 'Spawning new thread to send email with subject: %s: %s', subject, message
+ )
+ # Spawn thread because we don't want Deluge to lock up while we send the
+ # email.
+ return threads.deferToThread(self._notify_email, subject, message)
+
+ def get_handled_events(self):
+ handled_events = []
+ for evt in sorted(known_events):
+ if known_events[evt].__module__.startswith('deluge.event'):
+ if evt not in ('TorrentFinishedEvent',):
+ # Skip all un-handled built-in events
+ continue
+ classdoc = known_events[evt].__doc__.strip()
+ handled_events.append((evt, classdoc))
+ log.debug('Handled Notification Events: %s', handled_events)
+ return handled_events
+
+ def _notify_email(self, subject='', message=''):
+ log.debug('Email prepared')
+ to_addrs = self.config['smtp_recipients']
+ to_addrs_str = ', '.join(self.config['smtp_recipients'])
+ headers_dict = {
+ 'smtp_from': self.config['smtp_from'],
+ 'subject': subject,
+ 'smtp_recipients': to_addrs_str,
+ 'date': formatdate(),
+ }
+ headers = (
+ """\
+From: %(smtp_from)s
+To: %(smtp_recipients)s
+Subject: %(subject)s
+Date: %(date)s
+
+
+"""
+ % headers_dict
+ )
+
+ message = '\r\n'.join((headers + message).splitlines())
+
+ try:
+ server = smtplib.SMTP(
+ self.config['smtp_host'], self.config['smtp_port'], timeout=60
+ )
+ except Exception as ex:
+ err_msg = _('There was an error sending the notification email: %s') % ex
+ log.error(err_msg)
+ return ex
+
+ security_enabled = self.config['smtp_tls']
+
+ if security_enabled:
+ server.ehlo()
+ if 'starttls' not in server.esmtp_features:
+ log.warning('TLS/SSL enabled but server does not support it')
+ else:
+ server.starttls()
+ server.ehlo()
+
+ if self.config['smtp_user'] and self.config['smtp_pass']:
+ try:
+ server.login(self.config['smtp_user'], self.config['smtp_pass'])
+ except smtplib.SMTPHeloError as ex:
+ err_msg = _('Server did not reply properly to HELO greeting: %s') % ex
+ log.error(err_msg)
+ return ex
+ except smtplib.SMTPAuthenticationError as ex:
+ err_msg = _('Server refused username/password combination: %s') % ex
+ log.error(err_msg)
+ return ex
+
+ try:
+ try:
+ server.sendmail(self.config['smtp_from'], to_addrs, message.encode())
+ except smtplib.SMTPException as ex:
+ err_msg = (
+ _('There was an error sending the notification email: %s') % ex
+ )
+ log.error(err_msg)
+ return ex
+ finally:
+ if security_enabled:
+ # avoid false failure detection when the server closes
+ # the SMTP connection with TLS enabled
+ import socket
+
+ try:
+ server.quit()
+ except socket.sslerror:
+ pass
+ else:
+ server.quit()
+ return _('Notification email sent.')
+
+ def _on_torrent_finished_event(self, torrent_id):
+ log.debug('Handler for TorrentFinishedEvent called for CORE')
+ torrent = component.get('TorrentManager')[torrent_id]
+ torrent_status = torrent.get_status(['name', 'num_files'])
+ # Email
+ subject = _('Finished Torrent "%(name)s"') % torrent_status
+ message = (
+ _(
+ 'This email is to inform you that Deluge has finished '
+ 'downloading "%(name)s", which includes %(num_files)i files.'
+ '\nTo stop receiving these alerts, simply turn off email '
+ "notification in Deluge's preferences.\n\n"
+ 'Thank you,\nDeluge.'
+ )
+ % torrent_status
+ )
+ return subject, message
+
+ # d = defer.maybeDeferred(self.handle_custom_email_notification,
+ # [subject, message],
+ # 'TorrentFinishedEvent')
+ # d.addCallback(self._on_notify_sucess, 'email')
+ # d.addErrback(self._on_notify_failure, 'email')
+ # return d
+
+
+class Core(CorePluginBase, CoreNotifications):
+ def __init__(self, plugin_name):
+ CorePluginBase.__init__(self, plugin_name)
+ CoreNotifications.__init__(self)
+
+ def enable(self):
+ CoreNotifications.enable(self)
+ self.config = deluge.configmanager.ConfigManager(
+ 'notifications-core.conf', DEFAULT_PREFS
+ )
+ log.debug('ENABLING CORE NOTIFICATIONS')
+
+ def disable(self):
+ log.debug('DISABLING CORE NOTIFICATIONS')
+ CoreNotifications.disable(self)
+
+ @export
+ def set_config(self, config):
+ """Sets the config dictionary."""
+ for key in config:
+ self.config[key] = config[key]
+ self.config.save()
+
+ @export
+ def get_config(self):
+ """Returns the config dictionary."""
+ return self.config.config
+
+ @export
+ def get_handled_events(self):
+ return CoreNotifications.get_handled_events(self)
diff --git a/deluge/plugins/Notifications/deluge_notifications/data/config.ui b/deluge/plugins/Notifications/deluge_notifications/data/config.ui
new file mode 100644
index 0000000..399cc9e
--- /dev/null
+++ b/deluge/plugins/Notifications/deluge_notifications/data/config.ui
@@ -0,0 +1,641 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <object class="GtkAdjustment" id="adjustment1">
+ <property name="lower">1</property>
+ <property name="upper">65535</property>
+ <property name="value">25</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">10</property>
+ </object>
+ <object class="GtkWindow" id="window">
+ <property name="can_focus">False</property>
+ <child type="titlebar">
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkBox" id="prefs_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkViewport" id="viewport1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="resize_mode">queue</property>
+ <child>
+ <object class="GtkNotebook" id="notebook1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="show_border">False</property>
+ <child>
+ <object class="GtkBox" id="vbox2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">start</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkFrame" id="frame2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">start</property>
+ <property name="margin_left">9</property>
+ <property name="label_xalign">0</property>
+ <property name="shadow_type">none</property>
+ <child>
+ <object class="GtkAlignment" id="alignment2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="left_padding">12</property>
+ <property name="right_padding">10</property>
+ <child>
+ <object class="GtkBox" id="vbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkCheckButton" id="blink_enabled">
+ <property name="label" translatable="yes">Tray icon blinks enabled</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="popup_enabled">
+ <property name="label" translatable="yes">Popups enabled</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="hbox2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkCheckButton" id="sound_enabled">
+ <property name="label" translatable="yes">Sound enabled</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="halign">start</property>
+ <property name="draw_indicator">True</property>
+ <signal name="toggled" handler="on_sound_enabled_toggled" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFileChooserButton" id="sound_path">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="create_folders">False</property>
+ <signal name="update-preview" handler="on_sound_path_update_preview" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="padding">2</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label6">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin_top">5</property>
+ <property name="xpad">5</property>
+ <property name="label" translatable="yes">&lt;b&gt;UI Notifications&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFrame" id="frame3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">start</property>
+ <property name="margin_left">10</property>
+ <property name="margin_top">7</property>
+ <property name="label_xalign">0</property>
+ <property name="shadow_type">none</property>
+ <child>
+ <object class="GtkAlignment" id="alignment3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="left_padding">12</property>
+ <property name="right_padding">10</property>
+ <child>
+ <object class="GtkTable" id="prefs_table">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="n_rows">7</property>
+ <property name="n_columns">4</property>
+ <property name="column_spacing">2</property>
+ <property name="row_spacing">2</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Hostname:</property>
+ </object>
+ <packing>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="smtp_host">
+ <property name="visible">True</property>
+ <property name="can_focus">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>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Port:</property>
+ <property name="justify">right</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="smtp_port">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="max_length">5</property>
+ <property name="width_chars">5</property>
+ <property name="adjustment">adjustment1</property>
+ <property name="climb_rate">1</property>
+ <property name="numeric">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">3</property>
+ <property name="right_attach">4</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Username:</property>
+ </object>
+ <packing>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="smtp_user">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">4</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Password:</property>
+ </object>
+ <packing>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="smtp_pass">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="visibility">False</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">4</property>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFrame" id="frame1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">start</property>
+ <property name="label_xalign">0</property>
+ <property name="shadow_type">none</property>
+ <child>
+ <object class="GtkAlignment" id="alignment4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="bottom_padding">10</property>
+ <child>
+ <object class="GtkBox" id="hbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">2</property>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="shadow_type">in</property>
+ <child>
+ <object class="GtkTreeView" id="smtp_recipients">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="headers_visible">False</property>
+ <property name="enable_grid_lines">horizontal</property>
+ <child internal-child="selection">
+ <object class="GtkTreeSelection"/>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkVButtonBox" id="vbuttonbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">5</property>
+ <property name="layout_style">start</property>
+ <child>
+ <object class="GtkButton" id="add_button">
+ <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>
+ <child>
+ <object class="GtkButton" id="delete_button">
+ <property name="label">gtk-delete</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="on_delete_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">3</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_bottom">3</property>
+ <property name="ypad">0</property>
+ <property name="label" translatable="yes">&lt;b&gt;Recipients&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="right_attach">4</property>
+ <property name="top_attach">6</property>
+ <property name="bottom_attach">7</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="smtp_tls">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="halign">start</property>
+ <property name="draw_indicator">True</property>
+ <child>
+ <object class="GtkLabel" id="label9">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Server requires TLS/SSL</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">4</property>
+ <property name="top_attach">5</property>
+ <property name="bottom_attach">6</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label8">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">From:</property>
+ </object>
+ <packing>
+ <property name="top_attach">4</property>
+ <property name="bottom_attach">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="smtp_from">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">4</property>
+ <property name="top_attach">4</property>
+ <property name="bottom_attach">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="smtp_enabled">
+ <property name="label" translatable="yes">Enabled</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ <signal name="toggled" handler="on_enabled_toggled" swapped="no"/>
+ </object>
+ <packing>
+ <property name="right_attach">4</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label7">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="xpad">5</property>
+ <property name="label" translatable="yes">&lt;b&gt;Email Notifications&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="settings_page_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Settings</property>
+ </object>
+ <packing>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="vbox5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">15</property>
+ <property name="margin_right">10</property>
+ <property name="margin_bottom">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow2">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <child>
+ <object class="GtkTreeView" id="subscriptions_treeview">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="enable_grid_lines">horizontal</property>
+ <child internal-child="selection">
+ <object class="GtkTreeSelection"/>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label12">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">This configuration does not mean that you'll actually receive notifications for all these events.</property>
+ <property name="justify">fill</property>
+ <property name="wrap">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">2</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="subscriptions_page_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Subscriptions</property>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="sounds_page">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">15</property>
+ <property name="margin_right">10</property>
+ <property name="margin_bottom">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow3">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <child>
+ <object class="GtkTreeView" id="sounds_treeview">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <child internal-child="selection">
+ <object class="GtkTreeSelection"/>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHButtonBox" id="hbuttonbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">10</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="sounds_revert_button">
+ <property name="label">gtk-revert-to-saved</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="on_sounds_revert_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="sounds_edit_button">
+ <property name="label">gtk-edit</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="on_sounds_edit_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="sounds_page_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Sound Customization</property>
+ </object>
+ <packing>
+ <property name="position">2</property>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/deluge/plugins/Notifications/deluge_notifications/data/notifications.js b/deluge/plugins/Notifications/deluge_notifications/data/notifications.js
new file mode 100644
index 0000000..4b87e55
--- /dev/null
+++ b/deluge/plugins/Notifications/deluge_notifications/data/notifications.js
@@ -0,0 +1,522 @@
+/**
+ * notifications.js
+ *
+ * Copyright (c) Omar Alvarez 2014 <omar.alvarez@udc.es>
+ *
+ * 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.preferences');
+
+/**
+ * @class Deluge.ux.preferences.NotificationsPage
+ * @extends Ext.Panel
+ */
+Deluge.ux.preferences.NotificationsPage = Ext.extend(Ext.Panel, {
+ title: _('Notifications'),
+ header: false,
+ layout: 'fit',
+ border: false,
+
+ initComponent: function () {
+ Deluge.ux.preferences.NotificationsPage.superclass.initComponent.call(
+ this
+ );
+
+ this.emailNotiFset = new Ext.form.FieldSet({
+ xtype: 'fieldset',
+ border: false,
+ title: _('Email Notifications'),
+ autoHeight: true,
+ defaultType: 'textfield',
+ style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
+ width: '85%',
+ labelWidth: 1,
+ });
+
+ this.chkEnableEmail = this.emailNotiFset.add({
+ fieldLabel: '',
+ labelSeparator: '',
+ name: 'enable_email',
+ xtype: 'checkbox',
+ boxLabel: _('Enabled'),
+ listeners: {
+ check: function (object, checked) {
+ this.setSmtpDisabled(!checked);
+ },
+ scope: this,
+ },
+ });
+
+ this.hBoxHost = this.emailNotiFset.add({
+ fieldLabel: '',
+ labelSeparator: '',
+ name: 'host',
+ xtype: 'container',
+ layout: 'hbox',
+ disabled: true,
+ items: [
+ {
+ xtype: 'label',
+ text: _('Hostname:'),
+ margins: '6 0 0 6',
+ },
+ {
+ xtype: 'textfield',
+ margins: '2 0 0 4',
+ },
+ ],
+ });
+
+ this.hBoxPort = this.emailNotiFset.add({
+ fieldLabel: '',
+ labelSeparator: '',
+ name: 'port',
+ xtype: 'container',
+ layout: 'hbox',
+ disabled: true,
+ items: [
+ {
+ xtype: 'label',
+ text: _('Port:'),
+ margins: '6 0 0 6',
+ },
+ {
+ xtype: 'spinnerfield',
+ margins: '2 0 0 34',
+ width: 64,
+ decimalPrecision: 0,
+ minValue: 0,
+ maxValue: 65535,
+ },
+ ],
+ });
+
+ this.hBoxUser = this.emailNotiFset.add({
+ fieldLabel: '',
+ labelSeparator: '',
+ name: 'username',
+ xtype: 'container',
+ layout: 'hbox',
+ disabled: true,
+ items: [
+ {
+ xtype: 'label',
+ text: _('Username:'),
+ margins: '6 0 0 6',
+ },
+ {
+ xtype: 'textfield',
+ margins: '2 0 0 3',
+ },
+ ],
+ });
+
+ this.hBoxPassword = this.emailNotiFset.add({
+ fieldLabel: '',
+ labelSeparator: '',
+ name: 'password',
+ xtype: 'container',
+ layout: 'hbox',
+ disabled: true,
+ items: [
+ {
+ xtype: 'label',
+ text: _('Password:'),
+ margins: '6 0 0 6',
+ },
+ {
+ xtype: 'textfield',
+ inputType: 'password',
+ margins: '2 0 0 5',
+ },
+ ],
+ });
+
+ this.hBoxFrom = this.emailNotiFset.add({
+ fieldLabel: '',
+ labelSeparator: '',
+ name: 'from',
+ xtype: 'container',
+ layout: 'hbox',
+ disabled: true,
+ items: [
+ {
+ xtype: 'label',
+ text: _('From:'),
+ margins: '6 0 0 6',
+ },
+ {
+ xtype: 'textfield',
+ margins: '2 0 0 28',
+ },
+ ],
+ });
+
+ this.chkTLS = this.emailNotiFset.add({
+ fieldLabel: '',
+ labelSeparator: '',
+ name: 'enable_tls_ssl',
+ xtype: 'checkbox',
+ disabled: true,
+ boxLabel: _('Server requires TLS/SSL'),
+ });
+
+ this.recipientsFset = new Ext.form.FieldSet({
+ xtype: 'fieldset',
+ border: false,
+ title: _('Recipients'),
+ autoHeight: true,
+ defaultType: 'editorgrid',
+ style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
+ autoWidth: true,
+ items: [
+ {
+ fieldLabel: '',
+ name: 'recipients',
+ margins: '2 0 5 5',
+ height: 130,
+ hideHeaders: true,
+ width: 260,
+ disabled: true,
+ autoExpandColumn: 'recipient',
+ bbar: {
+ items: [
+ {
+ text: _('Add'),
+ iconCls: 'icon-add',
+ handler: this.onAddClick,
+ scope: this,
+ },
+ {
+ text: _('Remove'),
+ iconCls: 'icon-remove',
+ handler: this.onRemoveClick,
+ scope: this,
+ },
+ ],
+ },
+ viewConfig: {
+ emptyText: _('Add an recipient...'),
+ deferEmptyText: false,
+ },
+ colModel: new Ext.grid.ColumnModel({
+ columns: [
+ {
+ id: 'recipient',
+ header: _('Recipient'),
+ dataIndex: 'recipient',
+ sortable: true,
+ hideable: false,
+ editable: true,
+ editor: {
+ xtype: 'textfield',
+ },
+ },
+ ],
+ }),
+ selModel: new Ext.grid.RowSelectionModel({
+ singleSelect: false,
+ moveEditorOnEnter: false,
+ }),
+ store: new Ext.data.ArrayStore({
+ autoDestroy: true,
+ fields: [{ name: 'recipient' }],
+ }),
+ listeners: {
+ afteredit: function (e) {
+ e.record.commit();
+ },
+ },
+ setEmptyText: function (text) {
+ if (this.viewReady) {
+ this.getView().emptyText = text;
+ this.getView().refresh();
+ } else {
+ Ext.apply(this.viewConfig, { emptyText: text });
+ }
+ },
+ loadData: function (data) {
+ this.getStore().loadData(data);
+ if (this.viewReady) {
+ this.getView().updateHeaders();
+ }
+ },
+ },
+ ],
+ });
+
+ this.edGridSubs = new Ext.grid.EditorGridPanel({
+ xtype: 'editorgrid',
+ autoHeight: true,
+ autoExpandColumn: 'event',
+ viewConfig: {
+ emptyText: _('Loading events...'),
+ deferEmptyText: false,
+ },
+ colModel: new Ext.grid.ColumnModel({
+ defaults: {
+ renderer: function (
+ value,
+ meta,
+ record,
+ rowIndex,
+ colIndex,
+ store
+ ) {
+ if (Ext.isNumber(value) && parseInt(value) !== value) {
+ return value.toFixed(6);
+ } else if (Ext.isBoolean(value)) {
+ return (
+ '<div class="x-grid3-check-col' +
+ (value ? '-on' : '') +
+ '" style="width: 20px;">&#160;</div>'
+ );
+ }
+ return value;
+ },
+ },
+ columns: [
+ {
+ id: 'event',
+ header: 'Event',
+ dataIndex: 'event',
+ sortable: true,
+ hideable: false,
+ },
+ {
+ id: 'email',
+ header: _('Email'),
+ dataIndex: 'email',
+ sortable: true,
+ hideable: false,
+ menuDisabled: true,
+ width: 40,
+ },
+ ],
+ }),
+ store: new Ext.data.ArrayStore({
+ autoDestroy: true,
+ fields: [
+ {
+ name: 'event',
+ },
+ {
+ name: 'email',
+ },
+ ],
+ }),
+ listeners: {
+ cellclick: function (grid, rowIndex, colIndex, e) {
+ var record = grid.getStore().getAt(rowIndex);
+ var field = grid.getColumnModel().getDataIndex(colIndex);
+ var value = record.get(field);
+
+ if (colIndex == 1) {
+ if (Ext.isBoolean(value)) {
+ record.set(field, !value);
+ record.commit();
+ }
+ }
+ },
+ beforeedit: function (e) {
+ if (Ext.isBoolean(e.value)) {
+ return false;
+ }
+
+ return e.record.get('enabled');
+ },
+ afteredit: function (e) {
+ e.record.commit();
+ },
+ },
+ setEmptyText: function (text) {
+ if (this.viewReady) {
+ this.getView().emptyText = text;
+ this.getView().refresh();
+ } else {
+ Ext.apply(this.viewConfig, { emptyText: text });
+ }
+ },
+ setSub: function (eventName) {
+ var store = this.getStore();
+ var index = store.find('event', eventName);
+ store.getAt(index).set('email', true);
+ store.getAt(index).commit();
+ },
+ loadData: function (data) {
+ this.getStore().loadData(data);
+ if (this.viewReady) {
+ this.getView().updateHeaders();
+ }
+ },
+ });
+
+ this.tabPanSettings = this.add({
+ xtype: 'tabpanel',
+ activeTab: 0,
+ items: [
+ {
+ title: _('Settings'),
+ items: [this.emailNotiFset, this.recipientsFset],
+ autoScroll: true,
+ },
+ {
+ title: _('Subscriptions'),
+ items: this.edGridSubs,
+ },
+ ],
+ });
+
+ this.on('show', this.updateConfig, this);
+ },
+
+ updateConfig: function () {
+ deluge.client.notifications.get_handled_events({
+ success: function (events) {
+ var data = [];
+ var keys = Ext.keys(events);
+ for (var i = 0; i < keys.length; i++) {
+ var key = keys[i];
+ data.push([events[key][0], false]);
+ }
+ this.edGridSubs.loadData(data);
+ },
+ scope: this,
+ });
+ deluge.client.notifications.get_config({
+ success: function (config) {
+ this.chkEnableEmail.setValue(config['smtp_enabled']);
+ this.setSmtpDisabled(!config['smtp_enabled']);
+
+ this.hBoxHost.getComponent(1).setValue(config['smtp_host']);
+ this.hBoxPort.getComponent(1).setValue(config['smtp_port']);
+ this.hBoxUser.getComponent(1).setValue(config['smtp_user']);
+ this.hBoxPassword.getComponent(1).setValue(config['smtp_pass']);
+ this.hBoxFrom.getComponent(1).setValue(config['smtp_from']);
+ this.chkTLS.setValue(config['smtp_tls']);
+
+ var data = [];
+ var keys = Ext.keys(config['smtp_recipients']);
+ for (var i = 0; i < keys.length; i++) {
+ var key = keys[i];
+ data.push([config['smtp_recipients'][key]]);
+ }
+ this.recipientsFset.getComponent(0).loadData(data);
+
+ data = [];
+ keys = Ext.keys(config['subscriptions']['email']);
+ for (var i = 0; i < keys.length; i++) {
+ var key = keys[i];
+ this.edGridSubs.setSub(
+ config['subscriptions']['email'][key]
+ );
+ }
+ },
+ scope: this,
+ });
+ },
+
+ onApply: function () {
+ var config = {};
+
+ config['smtp_enabled'] = this.chkEnableEmail.getValue();
+ config['smtp_host'] = this.hBoxHost.getComponent(1).getValue();
+ config['smtp_port'] = Number(this.hBoxPort.getComponent(1).getValue());
+ config['smtp_user'] = this.hBoxUser.getComponent(1).getValue();
+ config['smtp_pass'] = this.hBoxPassword.getComponent(1).getValue();
+ config['smtp_from'] = this.hBoxFrom.getComponent(1).getValue();
+ config['smtp_tls'] = this.chkTLS.getValue();
+
+ var recipientsList = [];
+ var store = this.recipientsFset.getComponent(0).getStore();
+
+ for (var i = 0; i < store.getCount(); i++) {
+ var record = store.getAt(i);
+ var recipient = record.get('recipient');
+ recipientsList.push(recipient);
+ }
+
+ config['smtp_recipients'] = recipientsList;
+
+ var subscriptions = {};
+ var eventList = [];
+ store = this.edGridSubs.getStore();
+
+ for (var i = 0; i < store.getCount(); i++) {
+ var record = store.getAt(i);
+ var ev = record.get('event');
+ var email = record.get('email');
+ if (email) {
+ eventList.push(ev);
+ }
+ }
+
+ subscriptions['email'] = eventList;
+ config['subscriptions'] = subscriptions;
+
+ deluge.client.notifications.set_config(config);
+ },
+
+ onOk: function () {
+ this.onApply();
+ },
+
+ onAddClick: function () {
+ var store = this.recipientsFset.getComponent(0).getStore();
+ var Recipient = store.recordType;
+ var i = new Recipient({
+ recipient: '',
+ });
+ this.recipientsFset.getComponent(0).stopEditing();
+ store.insert(0, i);
+ this.recipientsFset.getComponent(0).startEditing(0, 0);
+ },
+
+ onRemoveClick: function () {
+ var selections = this.recipientsFset
+ .getComponent(0)
+ .getSelectionModel()
+ .getSelections();
+ var store = this.recipientsFset.getComponent(0).getStore();
+
+ this.recipientsFset.getComponent(0).stopEditing();
+ for (var i = 0; i < selections.length; i++) store.remove(selections[i]);
+ store.commitChanges();
+ },
+
+ setSmtpDisabled: function (disable) {
+ this.hBoxHost.setDisabled(disable);
+ this.hBoxPort.setDisabled(disable);
+ this.hBoxUser.setDisabled(disable);
+ this.hBoxPassword.setDisabled(disable);
+ this.hBoxFrom.setDisabled(disable);
+ this.chkTLS.setDisabled(disable);
+ this.recipientsFset.getComponent(0).setDisabled(disable);
+ },
+
+ onDestroy: function () {
+ deluge.preferences.un('show', this.updateConfig, this);
+
+ Deluge.ux.preferences.NotificationsPage.superclass.onDestroy.call(this);
+ },
+});
+
+Deluge.plugins.NotificationsPlugin = Ext.extend(Deluge.Plugin, {
+ name: 'Notifications',
+
+ onDisable: function () {
+ deluge.preferences.removePage(this.prefsPage);
+ },
+
+ onEnable: function () {
+ this.prefsPage = deluge.preferences.addPage(
+ new Deluge.ux.preferences.NotificationsPage()
+ );
+ },
+});
+
+Deluge.registerPlugin('Notifications', Deluge.plugins.NotificationsPlugin);
diff --git a/deluge/plugins/Notifications/deluge_notifications/gtkui.py b/deluge/plugins/Notifications/deluge_notifications/gtkui.py
new file mode 100644
index 0000000..4dc5ff8
--- /dev/null
+++ b/deluge/plugins/Notifications/deluge_notifications/gtkui.py
@@ -0,0 +1,741 @@
+#
+# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
+# 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 os.path import basename
+
+from gi import require_version
+from gi.repository import Gtk
+from twisted.internet import defer
+
+import deluge.common
+import deluge.component as component
+import deluge.configmanager
+from deluge.plugins.pluginbase import Gtk3PluginBase
+from deluge.ui.client import client
+
+from .common import CustomNotifications, get_resource
+
+# Relative imports
+
+log = logging.getLogger(__name__)
+
+try:
+ import pygame
+
+ SOUND_AVAILABLE = True
+except ImportError:
+ SOUND_AVAILABLE = False
+
+try:
+ require_version('Notify', '0.7')
+ from gi.repository import GLib, Notify
+except (ValueError, ImportError):
+ POPUP_AVAILABLE = False
+else:
+ POPUP_AVAILABLE = not deluge.common.windows_check()
+
+
+DEFAULT_PREFS = {
+ # BLINK
+ 'blink_enabled': False,
+ # FLASH
+ 'flash_enabled': False,
+ # POPUP
+ 'popup_enabled': False,
+ # SOUND
+ 'sound_enabled': False,
+ 'sound_path': '',
+ 'custom_sounds': {},
+ # Subscriptions
+ 'subscriptions': {'popup': [], 'blink': [], 'sound': []},
+}
+
+RECIPIENT_FIELD, RECIPIENT_EDIT = list(range(2))
+(
+ SUB_EVENT,
+ SUB_EVENT_DOC,
+ SUB_NOT_EMAIL,
+ SUB_NOT_POPUP,
+ SUB_NOT_BLINK,
+ SUB_NOT_SOUND,
+) = list(range(6))
+SND_EVENT, SND_EVENT_DOC, SND_NAME, SND_PATH = list(range(4))
+
+
+class GtkUiNotifications(CustomNotifications):
+ def __init__(self, plugin_name=None):
+ CustomNotifications.__init__(self, plugin_name)
+
+ def enable(self):
+ CustomNotifications.enable(self)
+ self.register_custom_blink_notification(
+ 'TorrentFinishedEvent', self._on_torrent_finished_event_blink
+ )
+ self.register_custom_sound_notification(
+ 'TorrentFinishedEvent', self._on_torrent_finished_event_sound
+ )
+ self.register_custom_popup_notification(
+ 'TorrentFinishedEvent', self._on_torrent_finished_event_popup
+ )
+
+ def disable(self):
+ self.deregister_custom_blink_notification('TorrentFinishedEvent')
+ self.deregister_custom_sound_notification('TorrentFinishedEvent')
+ self.deregister_custom_popup_notification('TorrentFinishedEvent')
+ CustomNotifications.disable(self)
+
+ def register_custom_popup_notification(self, eventtype, handler):
+ """This is used to register popup notifications for custom event types.
+
+ :param event: the event name
+ :param type: string
+ :param handler: function, to be called when `:param:event` is emitted
+
+ Your handler should return a tuple of (popup_title, popup_contents).
+ """
+ self._register_custom_provider('popup', eventtype, handler)
+
+ def deregister_custom_popup_notification(self, eventtype):
+ self._deregister_custom_provider('popup', eventtype)
+
+ def register_custom_blink_notification(self, eventtype, handler):
+ """This is used to register blink notifications for custom event types.
+
+ :param event: str, the event name
+ :param handler: function, to be called when `:param:event` is emitted
+
+ Your handler should return `True` or `False` to blink or not the
+ trayicon.
+ """
+ self._register_custom_provider('blink', eventtype, handler)
+
+ def deregister_custom_blink_notification(self, eventtype):
+ self._deregister_custom_provider('blink', eventtype)
+
+ def register_custom_sound_notification(self, eventtype, handler):
+ """This is used to register sound notifications for custom event types.
+
+ :param event: the event name
+ :type event: string
+ :param handler: function to be called when `:param:event` is emitted
+
+ Your handler should return either '' to use the sound defined on the
+ notification preferences, the path to a sound file, which will then be
+ played or None, where no sound will be played at all.
+ """
+ self._register_custom_provider('sound', eventtype, handler)
+
+ def deregister_custom_sound_notification(self, eventtype):
+ self._deregister_custom_provider('sound', eventtype)
+
+ def handle_custom_popup_notification(self, result, eventtype):
+ title, message = result
+ return defer.maybeDeferred(self.__popup, title, message)
+
+ def handle_custom_blink_notification(self, result, eventtype):
+ if result:
+ return defer.maybeDeferred(self.__blink)
+ return defer.succeed(
+ 'Will not blink. The returned value from the custom '
+ 'handler was: %s' % result
+ )
+
+ def handle_custom_sound_notification(self, result, eventtype):
+ if isinstance(result, ''.__class__):
+ if not result and eventtype in self.config['custom_sounds']:
+ return defer.maybeDeferred(
+ self.__play_sound, self.config['custom_sounds'][eventtype]
+ )
+ return defer.maybeDeferred(self.__play_sound, result)
+ return defer.succeed(
+ 'Will not play sound. The returned value from the '
+ 'custom handler was: %s' % result
+ )
+
+ def __blink(self):
+ self.systray.blink(True)
+ return defer.succeed(_('Notification Blink shown'))
+
+ def __popup(self, title='', message=''):
+ if not self.config['popup_enabled']:
+ return defer.succeed(_('Popup notification is not enabled.'))
+ if not POPUP_AVAILABLE:
+ err_msg = _('libnotify is not installed')
+ log.warning(err_msg)
+ return defer.fail(ImportError(err_msg))
+
+ if Notify.init('Deluge'):
+ self.note = Notify.Notification.new(title, message, 'deluge-panel')
+ self.note.set_hint('desktop-entry', GLib.Variant.new_string('deluge'))
+ if not self.note.show():
+ err_msg = _('Failed to popup notification')
+ log.warning(err_msg)
+ return defer.fail(Exception(err_msg))
+ return defer.succeed(_('Notification popup shown'))
+
+ def __play_sound(self, sound_path=''):
+ if not self.config['sound_enabled']:
+ return defer.succeed(_('Sound notification not enabled'))
+ if not SOUND_AVAILABLE:
+ err_msg = _('pygame is not installed')
+ log.warning(err_msg)
+ return defer.fail(ImportError(err_msg))
+
+ pygame.init()
+ try:
+ if not sound_path:
+ sound_path = self.config['sound_path']
+ alert_sound = pygame.mixer.music
+ alert_sound.load(sound_path)
+ alert_sound.play()
+ except pygame.error as ex:
+ err_msg = _('Sound notification failed %s') % ex
+ log.warning(err_msg)
+ return defer.fail(ex)
+ else:
+ msg = _('Sound notification Success')
+ log.info(msg)
+ return defer.succeed(msg)
+
+ def _on_torrent_finished_event_blink(self, torrent_id):
+ return True # Yes, Blink
+
+ def _on_torrent_finished_event_sound(self, torrent_id):
+ # Since there's no custom sound hardcoded, just return ''
+ return ''
+
+ def _on_torrent_finished_event_popup(self, torrent_id):
+ d = client.core.get_torrent_status(torrent_id, ['name', 'file_progress'])
+ d.addCallback(self._on_torrent_finished_event_got_torrent_status)
+ d.addErrback(self._on_torrent_finished_event_torrent_status_failure)
+ return d
+
+ def _on_torrent_finished_event_torrent_status_failure(self, failure):
+ log.debug('Failed to get torrent status to be able to show the popup')
+
+ def _on_torrent_finished_event_got_torrent_status(self, torrent_status):
+ log.debug(
+ 'Handler for TorrentFinishedEvent GTKUI called. ' 'Got Torrent Status'
+ )
+ title = _('Finished Torrent')
+ torrent_status['num_files'] = torrent_status['file_progress'].count(1.0)
+ message = (
+ _(
+ 'The torrent "%(name)s" including %(num_files)i file(s) '
+ 'has finished downloading.'
+ )
+ % torrent_status
+ )
+ return title, message
+
+
+class GtkUI(Gtk3PluginBase, GtkUiNotifications):
+ def __init__(self, plugin_name):
+ Gtk3PluginBase.__init__(self, plugin_name)
+ GtkUiNotifications.__init__(self)
+
+ def enable(self):
+ self.config = deluge.configmanager.ConfigManager(
+ 'notifications-gtk.conf', DEFAULT_PREFS
+ )
+ self.builder = Gtk.Builder()
+ self.builder.add_from_file(get_resource('config.ui'))
+ self.builder.get_object('smtp_port').set_value(25)
+ self.prefs = self.builder.get_object('prefs_box')
+ self.prefs.show_all()
+
+ self.build_recipients_model_populate_treeview()
+ self.build_sounds_model_populate_treeview()
+ self.build_notifications_model_populate_treeview()
+
+ client.notifications.get_handled_events().addCallback(
+ self.popuplate_what_needs_handled_events
+ )
+
+ self.builder.connect_signals(
+ {
+ 'on_add_button_clicked': (
+ self.on_add_button_clicked,
+ self.recipients_treeview,
+ ),
+ 'on_delete_button_clicked': (
+ self.on_delete_button_clicked,
+ self.recipients_treeview,
+ ),
+ 'on_enabled_toggled': self.on_enabled_toggled,
+ 'on_sound_enabled_toggled': self.on_sound_enabled_toggled,
+ 'on_sounds_edit_button_clicked': self.on_sounds_edit_button_clicked,
+ 'on_sounds_revert_button_clicked': self.on_sounds_revert_button_clicked,
+ 'on_sound_path_update_preview': self.on_sound_path_update_preview,
+ }
+ )
+
+ component.get('Preferences').add_page(_('Notifications'), self.prefs)
+
+ component.get('PluginManager').register_hook(
+ 'on_apply_prefs', self.on_apply_prefs
+ )
+ component.get('PluginManager').register_hook(
+ 'on_show_prefs', self.on_show_prefs
+ )
+
+ if not POPUP_AVAILABLE:
+ self.builder.get_object('popup_enabled').set_property('sensitive', False)
+ if not SOUND_AVAILABLE:
+ # for widget_name in ('sound_enabled', 'sound_path', 'sounds_page', 'sounds_page_label'):
+ # self.builder.get_object(widget_name).set_property('sensitive', False)
+ self.builder.get_object('sound_enabled').set_property('sensitive', False)
+ self.builder.get_object('sound_path').set_property('sensitive', False)
+ self.builder.get_object('sounds_page').set_property('sensitive', False)
+ self.builder.get_object('sounds_page_label').set_property(
+ 'sensitive', False
+ )
+
+ self.systray = component.get('SystemTray')
+ if not hasattr(self.systray, 'tray'):
+ # Tray is not beeing used
+ self.builder.get_object('blink_enabled').set_property('sensitive', False)
+
+ GtkUiNotifications.enable(self)
+
+ def disable(self):
+ GtkUiNotifications.disable(self)
+ component.get('Preferences').remove_page(_('Notifications'))
+ 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 build_recipients_model_populate_treeview(self):
+ # SMTP Recipients treeview/model
+ self.recipients_treeview = self.builder.get_object('smtp_recipients')
+ treeview_selection = self.recipients_treeview.get_selection()
+ treeview_selection.connect(
+ 'changed', self.on_recipients_treeview_selection_changed
+ )
+ self.recipients_model = Gtk.ListStore(str, bool)
+
+ renderer = Gtk.CellRendererText()
+ renderer.connect('edited', self.on_cell_edited, self.recipients_model)
+ renderer.recipient = RECIPIENT_FIELD
+ column = Gtk.TreeViewColumn(
+ 'Recipients', renderer, text=RECIPIENT_FIELD, editable=RECIPIENT_EDIT
+ )
+ column.set_expand(True)
+ self.recipients_treeview.append_column(column)
+ self.recipients_treeview.set_model(self.recipients_model)
+
+ def build_sounds_model_populate_treeview(self):
+ # Sound customisation treeview/model
+ self.sounds_treeview = self.builder.get_object('sounds_treeview')
+ sounds_selection = self.sounds_treeview.get_selection()
+ sounds_selection.connect('changed', self.on_sounds_treeview_selection_changed)
+
+ self.sounds_treeview.set_tooltip_column(SND_EVENT_DOC)
+ self.sounds_model = Gtk.ListStore(str, str, str, str)
+
+ renderer = Gtk.CellRendererText()
+ renderer.event = SND_EVENT
+ column = Gtk.TreeViewColumn('Event', renderer, text=SND_EVENT)
+ column.set_expand(True)
+ self.sounds_treeview.append_column(column)
+
+ renderer = Gtk.CellRendererText()
+ renderer.event_doc = SND_EVENT_DOC
+ column = Gtk.TreeViewColumn('Doc', renderer, text=SND_EVENT_DOC)
+ column.set_property('visible', False)
+ self.sounds_treeview.append_column(column)
+
+ renderer = Gtk.CellRendererText()
+ renderer.sound_name = SND_NAME
+ column = Gtk.TreeViewColumn('Name', renderer, text=SND_NAME)
+ self.sounds_treeview.append_column(column)
+
+ renderer = Gtk.CellRendererText()
+ renderer.sound_path = SND_PATH
+ column = Gtk.TreeViewColumn('Path', renderer, text=SND_PATH)
+ column.set_property('visible', False)
+ self.sounds_treeview.append_column(column)
+
+ self.sounds_treeview.set_model(self.sounds_model)
+
+ def build_notifications_model_populate_treeview(self):
+ # Notification Subscriptions treeview/model
+ self.subscriptions_treeview = self.builder.get_object('subscriptions_treeview')
+ subscriptions_selection = self.subscriptions_treeview.get_selection()
+ subscriptions_selection.connect(
+ 'changed', self.on_subscriptions_treeview_selection_changed
+ )
+ self.subscriptions_treeview.set_tooltip_column(SUB_EVENT_DOC)
+ self.subscriptions_model = Gtk.ListStore(str, str, bool, bool, bool, bool)
+
+ renderer = Gtk.CellRendererText()
+ setattr(renderer, 'event', SUB_EVENT)
+ column = Gtk.TreeViewColumn('Event', renderer, text=SUB_EVENT)
+ column.set_expand(True)
+ self.subscriptions_treeview.append_column(column)
+
+ renderer = Gtk.CellRendererText()
+ setattr(renderer, 'event_doc', SUB_EVENT)
+ column = Gtk.TreeViewColumn('Doc', renderer, text=SUB_EVENT_DOC)
+ column.set_property('visible', False)
+ self.subscriptions_treeview.append_column(column)
+
+ renderer = Gtk.CellRendererToggle()
+ renderer.set_property('activatable', True)
+ renderer.connect('toggled', self._on_email_col_toggled)
+ column = Gtk.TreeViewColumn('Email', renderer, active=SUB_NOT_EMAIL)
+ column.set_clickable(True)
+ self.subscriptions_treeview.append_column(column)
+
+ renderer = Gtk.CellRendererToggle()
+ renderer.set_property('activatable', True)
+ renderer.connect('toggled', self._on_popup_col_toggled)
+ column = Gtk.TreeViewColumn('Popup', renderer, active=SUB_NOT_POPUP)
+ column.set_clickable(True)
+ self.subscriptions_treeview.append_column(column)
+
+ renderer = Gtk.CellRendererToggle()
+ renderer.set_property('activatable', True)
+ renderer.connect('toggled', self._on_blink_col_toggled)
+ column = Gtk.TreeViewColumn('Blink', renderer, active=SUB_NOT_BLINK)
+ column.set_clickable(True)
+ self.subscriptions_treeview.append_column(column)
+
+ renderer = Gtk.CellRendererToggle()
+ renderer.set_property('activatable', True)
+ renderer.connect('toggled', self._on_sound_col_toggled)
+ column = Gtk.TreeViewColumn('Sound', renderer, active=SUB_NOT_SOUND)
+ column.set_clickable(True)
+ self.subscriptions_treeview.append_column(column)
+ self.subscriptions_treeview.set_model(self.subscriptions_model)
+
+ def popuplate_what_needs_handled_events(
+ self, handled_events, email_subscriptions=None
+ ):
+ if email_subscriptions is None:
+ email_subscriptions = []
+ self.populate_subscriptions(handled_events, email_subscriptions)
+ self.populate_sounds(handled_events)
+
+ def populate_sounds(self, handled_events):
+ self.sounds_model.clear()
+ for event_name, event_doc in handled_events:
+ if event_name in self.config['custom_sounds']:
+ snd_path = self.config['custom_sounds'][event_name]
+ else:
+ snd_path = self.config['sound_path']
+
+ if snd_path:
+ self.sounds_model.set(
+ self.sounds_model.append(),
+ SND_EVENT,
+ event_name,
+ SND_EVENT_DOC,
+ event_doc,
+ SND_NAME,
+ basename(snd_path),
+ SND_PATH,
+ snd_path,
+ )
+
+ def populate_subscriptions(self, handled_events, email_subscriptions=None):
+ if email_subscriptions is None:
+ email_subscriptions = []
+ subscriptions_dict = self.config['subscriptions']
+ self.subscriptions_model.clear()
+ # self.handled_events = handled_events
+ for event_name, event_doc in handled_events:
+ self.subscriptions_model.set(
+ self.subscriptions_model.append(),
+ SUB_EVENT,
+ event_name,
+ SUB_EVENT_DOC,
+ event_doc,
+ SUB_NOT_EMAIL,
+ event_name in email_subscriptions,
+ SUB_NOT_POPUP,
+ event_name in subscriptions_dict['popup'],
+ SUB_NOT_BLINK,
+ event_name in subscriptions_dict['blink'],
+ SUB_NOT_SOUND,
+ event_name in subscriptions_dict['sound'],
+ )
+
+ def on_apply_prefs(self):
+ log.debug('applying prefs for Notifications')
+
+ current_popup_subscriptions = []
+ current_blink_subscriptions = []
+ current_sound_subscriptions = []
+ current_email_subscriptions = []
+ for event, doc, email, popup, blink, sound in self.subscriptions_model:
+ if email:
+ current_email_subscriptions.append(event)
+ if popup:
+ current_popup_subscriptions.append(event)
+ if blink:
+ current_blink_subscriptions.append(event)
+ if sound:
+ current_sound_subscriptions.append(event)
+
+ old_sound_file = self.config['sound_path']
+ new_sound_file = self.builder.get_object('sound_path').get_filename()
+ log.debug(
+ 'Old Default sound file: %s New one: %s', old_sound_file, new_sound_file
+ )
+ custom_sounds = {}
+ for event_name, event_doc, filename, filepath in self.sounds_model:
+ log.debug('Custom sound for event "%s": %s', event_name, filename)
+ if filepath == old_sound_file:
+ continue
+ custom_sounds[event_name] = filepath
+
+ self.config.config.update(
+ {
+ 'popup_enabled': self.builder.get_object('popup_enabled').get_active(),
+ 'blink_enabled': self.builder.get_object('blink_enabled').get_active(),
+ 'sound_enabled': self.builder.get_object('sound_enabled').get_active(),
+ 'sound_path': new_sound_file,
+ 'subscriptions': {
+ 'popup': current_popup_subscriptions,
+ 'blink': current_blink_subscriptions,
+ 'sound': current_sound_subscriptions,
+ },
+ 'custom_sounds': custom_sounds,
+ }
+ )
+ self.config.save()
+
+ core_config = {
+ 'smtp_enabled': self.builder.get_object('smtp_enabled').get_active(),
+ 'smtp_host': self.builder.get_object('smtp_host').get_text(),
+ 'smtp_port': self.builder.get_object('smtp_port').get_value(),
+ 'smtp_user': self.builder.get_object('smtp_user').get_text(),
+ 'smtp_pass': self.builder.get_object('smtp_pass').get_text(),
+ 'smtp_from': self.builder.get_object('smtp_from').get_text(),
+ 'smtp_tls': self.builder.get_object('smtp_tls').get_active(),
+ 'smtp_recipients': [
+ dest[0] for dest in self.recipients_model if dest[0] != 'USER@HOST'
+ ],
+ 'subscriptions': {'email': current_email_subscriptions},
+ }
+
+ client.notifications.set_config(core_config)
+ client.notifications.get_config().addCallback(self.cb_get_config)
+
+ def on_show_prefs(self):
+ client.notifications.get_config().addCallback(self.cb_get_config)
+
+ def cb_get_config(self, core_config):
+ """Callback for on show_prefs."""
+ self.builder.get_object('smtp_host').set_text(core_config['smtp_host'])
+ self.builder.get_object('smtp_port').set_value(core_config['smtp_port'])
+ self.builder.get_object('smtp_user').set_text(core_config['smtp_user'])
+ self.builder.get_object('smtp_pass').set_text(core_config['smtp_pass'])
+ self.builder.get_object('smtp_from').set_text(core_config['smtp_from'])
+ self.builder.get_object('smtp_tls').set_active(core_config['smtp_tls'])
+ self.recipients_model.clear()
+ for recipient in core_config['smtp_recipients']:
+ self.recipients_model.set(
+ self.recipients_model.append(),
+ RECIPIENT_FIELD,
+ recipient,
+ RECIPIENT_EDIT,
+ False,
+ )
+ self.builder.get_object('smtp_enabled').set_active(core_config['smtp_enabled'])
+ self.builder.get_object('sound_enabled').set_active(
+ self.config['sound_enabled']
+ )
+ self.builder.get_object('popup_enabled').set_active(
+ self.config['popup_enabled']
+ )
+ self.builder.get_object('blink_enabled').set_active(
+ self.config['blink_enabled']
+ )
+ if self.config['sound_path']:
+ sound_path = self.config['sound_path']
+ else:
+ sound_path = deluge.common.get_default_download_dir()
+ self.builder.get_object('sound_path').set_filename(sound_path)
+ # Force toggle
+ self.on_enabled_toggled(self.builder.get_object('smtp_enabled'))
+ self.on_sound_enabled_toggled(self.builder.get_object('sound_enabled'))
+
+ client.notifications.get_handled_events().addCallback(
+ self.popuplate_what_needs_handled_events,
+ core_config['subscriptions']['email'],
+ )
+
+ def on_sound_path_update_preview(self, filechooser):
+ client.notifications.get_handled_events().addCallback(self.populate_sounds)
+
+ def on_add_button_clicked(self, widget, treeview):
+ model = treeview.get_model()
+ model.set(model.append(), RECIPIENT_FIELD, 'USER@HOST', RECIPIENT_EDIT, True)
+
+ def on_delete_button_clicked(self, widget, treeview):
+ selection = treeview.get_selection()
+ model, selected_iter = selection.get_selected()
+ if selected_iter:
+ model.remove(selected_iter)
+
+ def on_cell_edited(self, cell, path_string, new_text, model):
+ str_iter = model.get_iter_from_string(path_string)
+ model.set(str_iter, RECIPIENT_FIELD, new_text)
+
+ def on_recipients_treeview_selection_changed(self, selection):
+ model, selected_connection_iter = selection.get_selected()
+ if selected_connection_iter:
+ self.builder.get_object('delete_button').set_property('sensitive', True)
+ else:
+ self.builder.get_object('delete_button').set_property('sensitive', False)
+
+ def on_subscriptions_treeview_selection_changed(self, selection):
+ model, selected_connection_iter = selection.get_selected()
+ if selected_connection_iter:
+ self.builder.get_object('delete_button').set_property('sensitive', True)
+ else:
+ self.builder.get_object('delete_button').set_property('sensitive', False)
+
+ def on_sounds_treeview_selection_changed(self, selection):
+ model, selected_iter = selection.get_selected()
+ if selected_iter:
+ self.builder.get_object('sounds_edit_button').set_property(
+ 'sensitive', True
+ )
+ path = model.get(selected_iter, SND_PATH)[0]
+ log.debug('Sound selection changed: %s', path)
+ if path != self.config['sound_path']:
+ self.builder.get_object('sounds_revert_button').set_property(
+ 'sensitive', True
+ )
+ else:
+ self.builder.get_object('sounds_revert_button').set_property(
+ 'sensitive', False
+ )
+ else:
+ self.builder.get_object('sounds_edit_button').set_property(
+ 'sensitive', False
+ )
+ self.builder.get_object('sounds_revert_button').set_property(
+ 'sensitive', False
+ )
+
+ def on_sounds_revert_button_clicked(self, widget):
+ log.debug('on_sounds_revert_button_clicked')
+ selection = self.sounds_treeview.get_selection()
+ model, selected_iter = selection.get_selected()
+ if selected_iter:
+ log.debug('on_sounds_revert_button_clicked: got iter')
+ model.set(
+ selected_iter,
+ SND_PATH,
+ self.config['sound_path'],
+ SND_NAME,
+ basename(self.config['sound_path']),
+ )
+
+ def on_sounds_edit_button_clicked(self, widget):
+ log.debug('on_sounds_edit_button_clicked')
+ selection = self.sounds_treeview.get_selection()
+ model, selected_iter = selection.get_selected()
+ if selected_iter:
+ path = model.get(selected_iter, SND_PATH)[0]
+ dialog = Gtk.FileChooserDialog(
+ title=_('Choose Sound File'),
+ buttons=(
+ Gtk.STOCK_CANCEL,
+ Gtk.ResponseType.CANCEL,
+ Gtk.STOCK_OPEN,
+ Gtk.ResponseType.OK,
+ ),
+ )
+ dialog.set_filename(path)
+
+ def update_model(response):
+ if response == Gtk.ResponseType.OK:
+ new_filename = dialog.get_filename()
+ dialog.destroy()
+ log.debug(new_filename)
+ model.set(
+ selected_iter,
+ SND_PATH,
+ new_filename,
+ SND_NAME,
+ basename(new_filename),
+ )
+
+ d = defer.maybeDeferred(dialog.run)
+ d.addCallback(update_model)
+
+ log.debug('dialog should have been shown')
+
+ def on_enabled_toggled(self, widget):
+ for widget_name in (
+ 'smtp_host',
+ 'smtp_port',
+ 'smtp_user',
+ 'smtp_pass',
+ 'smtp_pass',
+ 'smtp_tls',
+ 'smtp_from',
+ 'smtp_recipients',
+ ):
+ self.builder.get_object(widget_name).set_property(
+ 'sensitive', widget.get_active()
+ )
+
+ def on_sound_enabled_toggled(self, widget):
+ if widget.get_active():
+ self.builder.get_object('sound_path').set_property('sensitive', True)
+ self.builder.get_object('sounds_page').set_property('sensitive', True)
+ self.builder.get_object('sounds_page_label').set_property('sensitive', True)
+ else:
+ self.builder.get_object('sound_path').set_property('sensitive', False)
+ self.builder.get_object('sounds_page').set_property('sensitive', False)
+ self.builder.get_object('sounds_page_label').set_property(
+ 'sensitive', False
+ )
+
+ # for widget_name in ('sounds_path', 'sounds_page', 'sounds_page_label'):
+ # self.builder.get_object(widget_name).set_property('sensitive',
+ # widget.get_active())
+
+ def _on_email_col_toggled(self, cell, path):
+ self.subscriptions_model[path][SUB_NOT_EMAIL] = not self.subscriptions_model[
+ path
+ ][SUB_NOT_EMAIL]
+ return
+
+ def _on_popup_col_toggled(self, cell, path):
+ self.subscriptions_model[path][SUB_NOT_POPUP] = not self.subscriptions_model[
+ path
+ ][SUB_NOT_POPUP]
+ return
+
+ def _on_blink_col_toggled(self, cell, path):
+ self.subscriptions_model[path][SUB_NOT_BLINK] = not self.subscriptions_model[
+ path
+ ][SUB_NOT_BLINK]
+ return
+
+ def _on_sound_col_toggled(self, cell, path):
+ self.subscriptions_model[path][SUB_NOT_SOUND] = not self.subscriptions_model[
+ path
+ ][SUB_NOT_SOUND]
+ return
diff --git a/deluge/plugins/Notifications/deluge_notifications/test.py b/deluge/plugins/Notifications/deluge_notifications/test.py
new file mode 100644
index 0000000..013cdbf
--- /dev/null
+++ b/deluge/plugins/Notifications/deluge_notifications/test.py
@@ -0,0 +1,86 @@
+# vim: sw=4 ts=4 fenc=utf-8 et
+# ==============================================================================
+# Copyright © 2009-2010 UfSoft.org - Pedro Algarvio <pedro@algarvio.me>
+#
+# License: BSD - Please view the LICENSE file for additional information.
+# ==============================================================================
+
+import logging
+
+from twisted.internet import task
+
+from deluge import component
+from deluge.event import DelugeEvent
+
+log = logging.getLogger(__name__)
+
+
+class FooEvent(DelugeEvent):
+ """foo Event"""
+
+
+class CustomEvent(DelugeEvent):
+ """Just a custom event to test"""
+
+
+class TestEmailNotifications(component.Component):
+ def __init__(self, imp):
+ component.Component.__init__(self, self.__class__.__name__, 5)
+ self.__imp = imp
+ self.lc = task.LoopingCall(self.update)
+ self.n = 1
+ self.events = [FooEvent(), CustomEvent()]
+ self.events_classes = []
+
+ def enable(self):
+ log.debug('\n\nEnabling %s', self.__class__.__name__)
+ for event in self.events:
+ if self.__imp == 'core':
+ # component.get('CorePlugin.Notifications').register_custom_email_notification(
+ component.get('Notifications').register_custom_email_notification(
+ event.__class__.__name__, self.custom_email_message_provider
+ )
+ elif self.__imp == 'gtk':
+ notifications_component = component.get('Notifications')
+ notifications_component.register_custom_popup_notification(
+ event.__class__.__name__, self.custom_popup_message_provider
+ )
+ notifications_component.register_custom_blink_notification(
+ event.__class__.__name__, self.custom_blink_message_provider
+ )
+ notifications_component.register_custom_sound_notification(
+ event.__class__.__name__, self.custom_sound_message_provider
+ )
+
+ self.lc.start(60, False)
+
+ def disable(self):
+ log.debug('\n\nDisabling %s', self.__class__.__name__)
+ self.lc.stop()
+
+ def update(self):
+ if self.__imp == 'core':
+ log.debug('\n\nUpdating %s', self.__class__.__name__)
+ self.events.append(self.events.pop(0)) # Re-Queue
+ self.n += 1
+ component.get('EventManager').emit(self.events[0])
+
+ def custom_email_message_provider(self, *evt_args, **evt_kwargs):
+ log.debug('Running custom email message provider: %s %s', evt_args, evt_kwargs)
+ subject = f'{self.events[0].__class__.__name__} Email Subject: {self.n}'
+ message = f'{self.events[0].__class__.__name__} Email Message: {self.n}'
+ return subject, message
+
+ def custom_popup_message_provider(self, *evt_args, **evt_kwargs):
+ log.debug('Running custom popup message provider: %s %s', evt_args, evt_kwargs)
+ title = f'{self.events[0].__class__.__name__} Popup Title: {self.n}'
+ message = f'{self.events[0].__class__.__name__} Popup Message: {self.n}'
+ return title, message
+
+ def custom_blink_message_provider(self, *evt_args, **evt_kwargs):
+ log.debug('Running custom blink message provider: %s %s', evt_args, evt_kwargs)
+ return True
+
+ def custom_sound_message_provider(self, *evt_args, **evt_kwargs):
+ log.debug('Running custom sound message provider: %s %s', evt_args, evt_kwargs)
+ return ''
diff --git a/deluge/plugins/Notifications/deluge_notifications/webui.py b/deluge/plugins/Notifications/deluge_notifications/webui.py
new file mode 100644
index 0000000..bf3e829
--- /dev/null
+++ b/deluge/plugins/Notifications/deluge_notifications/webui.py
@@ -0,0 +1,31 @@
+#
+# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
+# 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('notifications.js')]
+ debug_scripts = scripts
+
+ def enable(self):
+ log.debug('Enabling Web UI notifications')
+
+ def disable(self):
+ log.debug('Disabling Web UI notifications')
diff --git a/deluge/plugins/Notifications/setup.py b/deluge/plugins/Notifications/setup.py
new file mode 100755
index 0000000..3d87423
--- /dev/null
+++ b/deluge/plugins/Notifications/setup.py
@@ -0,0 +1,53 @@
+#
+# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
+# 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__ = 'Notifications'
+__author__ = 'Pedro Algarvio'
+__author_email__ = 'pedro@algarvio.me'
+__version__ = '0.4'
+__url__ = 'http://dev.deluge-torrent.org/'
+__license__ = 'GPLv3'
+__description__ = 'Plugin which provides notifications to Deluge.'
+__long_description__ = """
+Plugin which provides notifications to Deluge
+
+Email, Popup, Blink and Sound notifications
+
+The plugin also allows other plugins to make
+ use of itself for their own custom notifications
+"""
+__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__ if __long_description__ else __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),
+)
diff --git a/deluge/plugins/Scheduler/deluge_scheduler/__init__.py b/deluge/plugins/Scheduler/deluge_scheduler/__init__.py
new file mode 100644
index 0000000..87d1584
--- /dev/null
+++ b/deluge/plugins/Scheduler/deluge_scheduler/__init__.py
@@ -0,0 +1,37 @@
+#
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-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 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/Scheduler/deluge_scheduler/common.py b/deluge/plugins/Scheduler/deluge_scheduler/common.py
new file mode 100644
index 0000000..eb47f13
--- /dev/null
+++ b/deluge/plugins/Scheduler/deluge_scheduler/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/Scheduler/deluge_scheduler/core.py b/deluge/plugins/Scheduler/deluge_scheduler/core.py
new file mode 100644
index 0000000..10798ba
--- /dev/null
+++ b/deluge/plugins/Scheduler/deluge_scheduler/core.py
@@ -0,0 +1,167 @@
+#
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-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 logging
+import time
+
+from twisted.internet import reactor
+
+import deluge.component as component
+import deluge.configmanager
+from deluge.core.rpcserver import export
+from deluge.event import DelugeEvent
+from deluge.plugins.pluginbase import CorePluginBase
+
+log = logging.getLogger(__name__)
+
+DEFAULT_PREFS = {
+ 'low_down': -1.0,
+ 'low_up': -1.0,
+ 'low_active': -1,
+ 'low_active_down': -1,
+ 'low_active_up': -1,
+ 'button_state': [[0] * 7 for dummy in range(24)],
+}
+
+STATES = {0: 'Green', 1: 'Yellow', 2: 'Red'}
+
+CONTROLLED_SETTINGS = [
+ 'max_download_speed',
+ 'max_upload_speed',
+ 'max_active_limit',
+ 'max_active_downloading',
+ 'max_active_seeding',
+]
+
+
+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']
+ DEFAULT_PREFS['low_active_down'] = core_config['max_active_downloading']
+ DEFAULT_PREFS['low_active_up'] = core_config['max_active_seeding']
+
+ 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)
+
+ # Register for config changes so state isn't overridden
+ component.get('EventManager').register_event_handler(
+ 'ConfigValueChangedEvent', self.on_config_value_changed
+ )
+
+ def disable(self):
+ if self.timer.active():
+ self.timer.cancel()
+ component.get('EventManager').deregister_event_handler(
+ 'ConfigValueChangedEvent', self.on_config_value_changed
+ )
+ self.__apply_set_functions()
+
+ def update(self):
+ pass
+
+ def on_config_value_changed(self, key, value):
+ if key in CONTROLLED_SETTINGS:
+ self.do_schedule(False)
+
+ def __apply_set_functions(self):
+ """
+ Have the core apply it's bandwidth settings as specified in core.conf.
+ """
+ core_config = deluge.configmanager.ConfigManager('core.conf')
+ for setting in CONTROLLED_SETTINGS:
+ component.get('PreferencesManager').do_config_set_func(
+ setting, core_config[setting]
+ )
+ # Resume the session if necessary
+ component.get('Core').resume_session()
+
+ 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
+ self.__apply_set_functions()
+ elif state == 'Yellow':
+ # This is Yellow (Slow), so use the settings provided from the user
+ settings = {
+ 'active_limit': self.config['low_active'],
+ 'active_downloads': self.config['low_active_down'],
+ 'active_seeds': self.config['low_active_up'],
+ 'download_rate_limit': int(self.config['low_down'] * 1024),
+ 'upload_rate_limit': int(self.config['low_up'] * 1024),
+ }
+ component.get('Core').apply_session_settings(settings)
+ # Resume the session if necessary
+ component.get('Core').resume_session()
+ elif state == 'Red':
+ # This is Red (Stop), so pause the libtorrent session
+ component.get('Core').pause_session()
+
+ 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:
+ 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/deluge_scheduler/data/green.svg b/deluge/plugins/Scheduler/deluge_scheduler/data/green.svg
new file mode 100644
index 0000000..ff3f5d6
--- /dev/null
+++ b/deluge/plugins/Scheduler/deluge_scheduler/data/green.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 16 16" width="16" height="16"><defs><clipPath id="_clipPath_Ng8EWXQF95Gs7ywflmaKe8f73244LGgx"><rect width="16" height="16"/></clipPath></defs><g clip-path="url(#_clipPath_Ng8EWXQF95Gs7ywflmaKe8f73244LGgx)"><clipPath id="_clipPath_fEKXMbJwavdXcwSIsEYpsxFyYERCnK2c"><rect x="0" y="0" width="16" height="16" transform="matrix(1,0,0,1,0,0)" fill="rgb(255,255,255)"/></clipPath><g clip-path="url(#_clipPath_fEKXMbJwavdXcwSIsEYpsxFyYERCnK2c)"><g><g><radialGradient id="_rgradient_9" fx="0.5" fy="0.5" cx="0.5" cy="0.5" r="0.5" gradientTransform="matrix(12,0,0,12,1.5,1.5)" gradientUnits="userSpaceOnUse"><stop offset="0%" stop-opacity="1" style="stop-color:rgb(22,200,22)"/><stop offset="100%" stop-opacity="1" style="stop-color:rgb(22,200,22)"/></radialGradient><circle vector-effect="non-scaling-stroke" cx="7.5" cy="7.5" r="6" fill="url(#_rgradient_9)"/><path d=" M 7.5 0.013 C 3.37 0.013 0.013 3.37 0.013 7.5 C 0.013 11.63 3.37 14.987 7.5 14.987 C 11.63 14.987 14.987 11.63 14.987 7.5 C 14.987 3.37 11.63 0.013 7.5 0.013 Z M 7.5 1.987 C 10.549 1.987 13.013 4.451 13.013 7.5 C 13.013 10.549 10.549 13.013 7.5 13.013 C 4.451 13.013 1.987 10.549 1.987 7.5 C 1.987 4.451 4.451 1.987 7.5 1.987 Z " fill="rgb(18,155,0)"/><path d=" M 10.406 4 C 10.309 4.026 10.222 4.08 10.156 4.156 L 7.5 6.813 L 5.844 5.156 C 5.736 4.98 5.53 4.888 5.326 4.925 C 5.123 4.963 4.963 5.123 4.925 5.326 C 4.888 5.53 4.98 5.736 5.156 5.844 L 7.156 7.844 C 7.349 8.026 7.651 8.026 7.844 7.844 L 10.844 4.844 C 10.995 4.689 11.03 4.455 10.931 4.263 C 10.831 4.071 10.62 3.965 10.406 4 Z " fill="rgb(43,46,57)"/></g></g></g></g></svg>
diff --git a/deluge/plugins/Scheduler/deluge_scheduler/data/red.svg b/deluge/plugins/Scheduler/deluge_scheduler/data/red.svg
new file mode 100644
index 0000000..ccb0822
--- /dev/null
+++ b/deluge/plugins/Scheduler/deluge_scheduler/data/red.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 16 16" width="16" height="16"><defs><clipPath id="_clipPath_b9idtDkK5ON8Jka415AjKueNrp89rRRq"><rect width="16" height="16"/></clipPath></defs><g clip-path="url(#_clipPath_b9idtDkK5ON8Jka415AjKueNrp89rRRq)"><clipPath id="_clipPath_vxaOVU0QEXAkOxrpA9AlU4ChkMqnhw1h"><rect x="0" y="0" width="16" height="16" transform="matrix(1,0,0,1,0,0)" fill="rgb(255,255,255)"/></clipPath><g clip-path="url(#_clipPath_vxaOVU0QEXAkOxrpA9AlU4ChkMqnhw1h)"><g><g><g style="opacity:0.98;"><g opacity="0.98"><circle vector-effect="non-scaling-stroke" cx="7.5" cy="7.5" r="6" fill="rgb(230,56,31)"/></g></g><path d=" M 7.5 0.013 C 3.37 0.013 0.013 3.37 0.013 7.5 C 0.013 11.63 3.37 14.987 7.5 14.987 C 11.63 14.987 14.987 11.63 14.987 7.5 C 14.987 3.37 11.63 0.013 7.5 0.013 Z M 7.5 1.987 C 10.549 1.987 13.013 4.451 13.013 7.5 C 13.013 10.549 10.549 13.013 7.5 13.013 C 4.451 13.013 1.987 10.549 1.987 7.5 C 1.987 4.451 4.451 1.987 7.5 1.987 Z " fill="rgb(166,14,14)"/><path d=" M 10.406 4 C 10.309 4.026 10.222 4.08 10.156 4.156 L 7.5 6.813 L 5.844 5.156 C 5.736 4.98 5.53 4.888 5.326 4.925 C 5.123 4.963 4.963 5.123 4.925 5.326 C 4.888 5.53 4.98 5.736 5.156 5.844 L 7.156 7.844 C 7.349 8.026 7.651 8.026 7.844 7.844 L 10.844 4.844 C 10.995 4.689 11.03 4.455 10.931 4.263 C 10.831 4.071 10.62 3.965 10.406 4 Z " fill="rgb(43,46,57)"/></g></g></g></g></svg>
diff --git a/deluge/plugins/Scheduler/deluge_scheduler/data/scheduler.js b/deluge/plugins/Scheduler/deluge_scheduler/data/scheduler.js
new file mode 100644
index 0000000..f59068c
--- /dev/null
+++ b/deluge/plugins/Scheduler/deluge_scheduler/data/scheduler.js
@@ -0,0 +1,621 @@
+/**
+ * scheduler.js
+ * The client-side javascript code for the Scheduler plugin.
+ *
+ * Copyright (C) samuel337 2011
+ *
+ * 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.ScheduleSelector = Ext.extend(Ext.form.FieldSet, {
+ title: _('Schedule'),
+ autoHeight: true,
+ style: 'margin-bottom: 0px; padding-bottom: 0px;',
+ border: false,
+
+ states: [
+ {
+ name: 'Normal',
+ backgroundColor: 'LightGreen',
+ borderColor: 'DarkGreen',
+ value: 0,
+ },
+ {
+ name: 'Throttled',
+ backgroundColor: 'Yellow',
+ borderColor: 'Gold',
+ value: 1,
+ },
+ {
+ name: 'Paused',
+ backgroundColor: 'OrangeRed',
+ borderColor: 'FireBrick',
+ value: 2,
+ },
+ ],
+ daysOfWeek: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
+
+ initComponent: function () {
+ Deluge.ux.ScheduleSelector.superclass.initComponent.call(this);
+
+ // ExtJS' radiogroup implementation is very broken for styling.
+ /*this.stateBrush = this.add({
+ xtype: 'radiogroup',
+ fieldLabel: _('State Brush'),
+ name: 'current_state_brush',
+ submitValue: false,
+ items: [
+ { boxLabel: 'Normal', name: 'current_state_brush', inputValue: 0 },
+ { boxLabel: 'Throttled', name: 'current_state_brush', inputValue: 1, checked: true },
+ { boxLabel: 'Paused', name: 'current_state_brush', inputValue: 2 },
+ ]
+ });*/
+ },
+
+ onRender: function (ct, position) {
+ Deluge.ux.ScheduleSelector.superclass.onRender.call(this, ct, position);
+
+ var dom = this.body.dom;
+
+ function createEl(parent, type) {
+ var el = document.createElement(type);
+ parent.appendChild(el);
+ return el;
+ }
+
+ // create state brushes
+ // tack a random number to the end to avoid clashes
+ this.stateBrushName =
+ 'schedule-state-brush-' + Math.round(Math.random() * 10000);
+
+ var el1 = createEl(dom, 'div');
+
+ var el2 = createEl(el1, 'div');
+ this.stateBrush = el2;
+ el2.id = this.stateBrushName;
+
+ // for webkit
+ var floatAttr = 'float';
+ if (el2.style.float == undefined) {
+ // for firefox
+ if (el2.style.cssFloat != undefined) floatAttr = 'cssFloat';
+ // for IE
+ if (el2.style.styleFloat != undefined) floatAttr = 'styleFloat';
+ }
+ el2.style[floatAttr] = 'right';
+
+ for (var i = 0; i < this.states.length; i++) {
+ var el3 = createEl(el2, 'input');
+ el3.type = 'radio';
+ el3.value = this.states[i].value;
+ el3.name = this.stateBrushName;
+ el3.id = this.stateBrushName + '-' + this.states[i].name;
+
+ // isn't the first one
+ if (i > 0) el3.style.marginLeft = '7px';
+
+ // assume the first is the default state, so make the 2nd one the default brush
+ if (i == 1) el3.checked = true;
+
+ var el4 = createEl(el2, 'label');
+ el4.appendChild(document.createTextNode(this.states[i].name));
+ el4.htmlFor = el3.id;
+ el4.style.backgroundColor = this.states[i].backgroundColor;
+ el4.style.borderBottom = '2px solid ' + this.states[i].borderColor;
+ el4.style.padding = '2px 3px';
+ el4.style.marginLeft = '3px';
+ }
+
+ el1.appendChild(document.createTextNode('Select a state brush:'));
+
+ el1.style.marginBottom = '10px';
+
+ // keep the radio buttons separate from the time bars
+ createEl(dom, 'div').style.clear = 'both';
+
+ var table = createEl(dom, 'table');
+ table.cellSpacing = 0;
+
+ // cache access to cells for easier access later
+ this.scheduleCells = {};
+
+ Ext.each(
+ this.daysOfWeek,
+ function (day) {
+ var cells = [];
+ var row = createEl(table, 'tr');
+ var label = createEl(row, 'th');
+ label.setAttribute(
+ 'style',
+ 'font-weight: bold; padding-right: 5px;'
+ );
+ label.appendChild(document.createTextNode(day));
+ for (var hour = 0; hour < 24; hour++) {
+ var cell = createEl(row, 'td');
+
+ // assume the first state is the default state
+ cell.currentValue = cell.oldValue = this.states[0].value;
+ cell.day = day;
+ cell.hour = hour;
+
+ cell.width = '16px';
+ cell.height = '20px';
+
+ cell.style.border = '1px solid #999999';
+ // don't repeat borders in between cells
+ if (hour != 23)
+ // not the last cell
+ cell.style.borderRight = 'none';
+
+ this.updateCell(cell);
+
+ cells.push(cell);
+
+ cell = Ext.get(cell);
+ cell.on('click', this.onCellClick, this);
+ cell.on('mouseover', this.onCellMouseOver, this);
+ cell.on('mouseout', this.onCellMouseOut, this);
+ cell.on('mousedown', this.onCellMouseDown, this);
+ cell.on('mouseup', this.onCellMouseUp, this);
+ }
+
+ // insert gap row to provide visual separation
+ row = createEl(table, 'tr');
+ // blank cell to create gap
+ createEl(row, 'td').height = '3px';
+
+ this.scheduleCells[day] = cells;
+ },
+ this
+ );
+ },
+
+ updateCell: function (cell) {
+ // sanity check
+ if (cell.currentValue == undefined) return;
+
+ for (var i in this.states) {
+ var curState = this.states[i];
+ if (curState.value == cell.currentValue) {
+ cell.style.background = curState.backgroundColor;
+ break;
+ }
+ }
+ },
+
+ getCurrentBrushValue: function () {
+ var v = null;
+ var brushes = Ext.get(this.body.dom).findParent('form').elements[
+ this.stateBrushName
+ ];
+ Ext.each(brushes, function (b) {
+ if (b.checked) v = b.value;
+ });
+
+ return v;
+ },
+
+ onCellClick: function (event, cell) {
+ cell.oldValue = cell.currentValue;
+
+ this.dragAnchor = null;
+ },
+
+ onCellMouseDown: function (event, cell) {
+ this.dragAnchor = cell;
+ },
+
+ onCellMouseUp: function (event, cell) {
+ // if we're dragging...
+ if (this.dragAnchor) {
+ // set all those between here and the anchor to the new values
+ if (cell.hour > this.dragAnchor.hour)
+ this.confirmCells(cell.day, this.dragAnchor.hour, cell.hour);
+ else if (cell.hour < this.dragAnchor.hour)
+ this.confirmCells(cell.day, cell.hour, this.dragAnchor.hour);
+ else this.confirmCells(cell.day, cell.hour, cell.hour);
+
+ this.hideCellLeftTooltip();
+ this.hideCellRightTooltip();
+ this.dragAnchor = null;
+ }
+ },
+
+ onCellMouseOver: function (event, cell) {
+ // LEFT TOOL TIP
+ // if it isn't showing and we're dragging, show it.
+ // otherwise if dragging, leave it alone unless we're dragging to the left.
+ // if we're not dragging, show it.
+ var leftTooltipCell = null;
+ if (!this.dragAnchor) leftTooltipCell = cell;
+ else if (
+ (this.dragAnchor && this.isCellLeftTooltipHidden()) ||
+ (this.dragAnchor && this.dragAnchor.hour > cell.hour)
+ )
+ leftTooltipCell = this.dragAnchor;
+
+ if (leftTooltipCell) {
+ var hour = leftTooltipCell.hour;
+ var pm = false;
+
+ // convert to 12-hour time
+ if (hour >= 12) {
+ pm = true;
+ if (hour > 12) hour -= 12;
+ } else if (hour == 0) {
+ // change 0 hour to 12am
+ hour = 12;
+ }
+ this.showCellLeftTooltip(
+ hour + ' ' + (pm ? 'pm' : 'am'),
+ leftTooltipCell
+ );
+ }
+
+ // RIGHT TOOL TIP
+ var rightTooltipCell = null;
+ if (this.dragAnchor) {
+ if (this.dragAnchor.hour == cell.hour) this.hideCellRightTooltip();
+ else if (
+ this.dragAnchor.hour > cell.hour &&
+ this.isCellRightTooltipHidden()
+ )
+ rightTooltipCell = this.dragAnchor;
+ // cell.hour > this.dragAnchor.hour
+ else rightTooltipCell = cell;
+ }
+
+ if (rightTooltipCell) {
+ var hour = rightTooltipCell.hour;
+ var pm = false;
+
+ // convert to 12-hour time
+ if (hour >= 12) {
+ pm = true;
+ if (hour > 12) hour -= 12;
+ } else if (hour == 0) {
+ // change 0 hour to 12am
+ hour = 12;
+ }
+ this.showCellRightTooltip(
+ hour + ' ' + (pm ? 'pm' : 'am'),
+ rightTooltipCell
+ );
+ }
+
+ // preview colour change and
+ // revert state for all those on the outer side of the drag if dragging
+ if (this.dragAnchor) {
+ if (cell.day != this.dragAnchor.day) {
+ // dragged into another day. Abort! Abort!
+ Ext.each(
+ this.daysOfWeek,
+ function (day) {
+ this.revertCells(day, 0, 23);
+ },
+ this
+ );
+ this.dragAnchor = null;
+ this.hideCellLeftTooltip();
+ this.hideCellRightTooltip();
+ } else if (cell.hour > this.dragAnchor.hour) {
+ // dragging right
+ this.revertCells(cell.day, cell.hour + 1, 23);
+ this.previewCells(cell.day, this.dragAnchor.hour, cell.hour);
+ } else if (cell.hour < this.dragAnchor.hour) {
+ // dragging left
+ this.revertCells(cell.day, 0, cell.hour - 1);
+ this.previewCells(cell.day, cell.hour, this.dragAnchor.hour);
+ } else {
+ // back to anchor cell
+ // don't know if it is from right or left, so revert all except this
+ this.revertCells(cell.day, cell.hour + 1, 23);
+ this.revertCells(cell.day, 0, cell.hour - 1);
+ }
+ } else {
+ // not dragging, just preview this cell
+ this.previewCells(cell.day, cell.hour, cell.hour);
+ }
+ },
+
+ onCellMouseOut: function (event, cell) {
+ if (!this.dragAnchor) this.hideCellLeftTooltip();
+
+ // revert state. If new state has been set, old and new will be equal.
+ // if dragging, this will be handled by the next mouse over
+ if (this.dragAnchor == null && cell.oldValue != cell.currentValue) {
+ this.revertCells(cell.day, cell.hour, cell.hour);
+ }
+ },
+
+ previewCells: function (day, fromHour, toHour) {
+ var cells = this.scheduleCells[day];
+ var curBrushValue = this.getCurrentBrushValue();
+
+ if (toHour > cells.length) toHour = cells.length;
+
+ for (var i = fromHour; i <= toHour; i++) {
+ if (cells[i].currentValue != curBrushValue) {
+ cells[i].oldValue = cells[i].currentValue;
+ cells[i].currentValue = curBrushValue;
+ this.updateCell(cells[i]);
+ }
+ }
+ },
+
+ revertCells: function (day, fromHour, toHour) {
+ var cells = this.scheduleCells[day];
+
+ if (toHour > cells.length) toHour = cells.length;
+
+ for (var i = fromHour; i <= toHour; i++) {
+ cells[i].currentValue = cells[i].oldValue;
+ this.updateCell(cells[i]);
+ }
+ },
+
+ confirmCells: function (day, fromHour, toHour) {
+ var cells = this.scheduleCells[day];
+
+ if (toHour > cells.length) toHour = cells.length;
+
+ for (var i = fromHour; i <= toHour; i++) {
+ if (cells[i].currentValue != cells[i].oldValue) {
+ cells[i].oldValue = cells[i].currentValue;
+ }
+ }
+ },
+
+ showCellLeftTooltip: function (text, cell) {
+ var tooltip = this.cellLeftTooltip;
+
+ if (!tooltip) {
+ // no cached left tooltip exists, create one
+ tooltip = document.createElement('div');
+ this.cellLeftTooltip = tooltip;
+ this.body.dom.appendChild(tooltip);
+ tooltip.style.position = 'absolute';
+ tooltip.style.backgroundColor = '#F2F2F2';
+ tooltip.style.border = '1px solid #333333';
+ tooltip.style.padding = '1px 3px';
+ tooltip.style.opacity = 0.8;
+ }
+
+ // remove all existing children
+ while (tooltip.childNodes.length > 0) {
+ tooltip.removeChild(tooltip.firstChild);
+ }
+ // add the requested text
+ tooltip.appendChild(document.createTextNode(text));
+
+ // place the tooltip
+ Ext.get(tooltip).alignTo(cell, 'br-tr');
+
+ // make it visible
+ tooltip.style.visibility = 'visible';
+ },
+
+ hideCellLeftTooltip: function () {
+ if (this.cellLeftTooltip) {
+ this.cellLeftTooltip.style.visibility = 'hidden';
+ }
+ },
+
+ isCellLeftTooltipHidden: function () {
+ if (this.cellLeftTooltip)
+ return this.cellLeftTooltip.style.visibility == 'hidden';
+ else return true;
+ },
+
+ showCellRightTooltip: function (text, cell) {
+ var tooltip = this.cellRightTooltip;
+
+ if (!tooltip) {
+ // no cached left tooltip exists, create one
+ tooltip = document.createElement('div');
+ this.cellRightTooltip = tooltip;
+ this.body.dom.appendChild(tooltip);
+ tooltip.style.position = 'absolute';
+ tooltip.style.backgroundColor = '#F2F2F2';
+ tooltip.style.border = '1px solid #333333';
+ tooltip.style.padding = '1px 3px';
+ tooltip.style.opacity = 0.8;
+ }
+
+ // remove all existing children
+ while (tooltip.childNodes.length > 0) {
+ tooltip.removeChild(tooltip.firstChild);
+ }
+ // add the requested text
+ tooltip.appendChild(document.createTextNode(text));
+
+ // place the tooltip
+ Ext.get(tooltip).alignTo(cell, 'bl-tl');
+
+ // make it visible
+ tooltip.style.visibility = 'visible';
+ },
+
+ hideCellRightTooltip: function () {
+ if (this.cellRightTooltip) {
+ this.cellRightTooltip.style.visibility = 'hidden';
+ }
+ },
+
+ isCellRightTooltipHidden: function () {
+ if (this.cellRightTooltip)
+ return this.cellRightTooltip.style.visibility == 'hidden';
+ else return true;
+ },
+
+ getConfig: function () {
+ var config = [];
+
+ for (var i = 0; i < 24; i++) {
+ var hourConfig = [0, 0, 0, 0, 0, 0, 0];
+
+ for (var j = 0; j < this.daysOfWeek.length; j++) {
+ hourConfig[j] = parseInt(
+ this.scheduleCells[this.daysOfWeek[j]][i].currentValue
+ );
+ }
+
+ config.push(hourConfig);
+ }
+
+ return config;
+ },
+
+ setConfig: function (config) {
+ for (var i = 0; i < 24; i++) {
+ var hourConfig = config[i];
+
+ for (var j = 0; j < this.daysOfWeek.length; j++) {
+ if (this.scheduleCells == undefined) {
+ var cell = hourConfig[j];
+ } else {
+ var cell = this.scheduleCells[this.daysOfWeek[j]][i];
+ }
+ cell.currentValue = cell.oldValue = hourConfig[j];
+ this.updateCell(cell);
+ }
+ }
+ },
+});
+
+Ext.ns('Deluge.ux.preferences');
+
+Deluge.ux.preferences.SchedulerPage = Ext.extend(Ext.Panel, {
+ border: false,
+ title: _('Scheduler'),
+ header: false,
+ layout: 'fit',
+
+ initComponent: function () {
+ Deluge.ux.preferences.SchedulerPage.superclass.initComponent.call(this);
+
+ this.form = this.add({
+ xtype: 'form',
+ layout: 'form',
+ border: false,
+ autoHeight: true,
+ });
+
+ this.schedule = this.form.add(new Deluge.ux.ScheduleSelector());
+
+ this.slowSettings = this.form.add({
+ xtype: 'fieldset',
+ border: false,
+ title: _('Throttled Settings'),
+ autoHeight: true,
+ defaultType: 'spinnerfield',
+ defaults: {
+ minValue: -1,
+ maxValue: 99999,
+ },
+ style: 'margin-top: 5px; margin-bottom: 0px; padding-bottom: 0px;',
+ labelWidth: 200,
+ });
+
+ this.downloadLimit = this.slowSettings.add({
+ fieldLabel: _('Maximum Download Speed (KiB/s)'),
+ name: 'download_limit',
+ width: 80,
+ value: -1,
+ decimalPrecision: 0,
+ });
+ this.uploadLimit = this.slowSettings.add({
+ fieldLabel: _('Maximum Upload Speed (KiB/s)'),
+ name: 'upload_limit',
+ width: 80,
+ value: -1,
+ decimalPrecision: 0,
+ });
+ this.activeTorrents = this.slowSettings.add({
+ fieldLabel: _('Active Torrents'),
+ name: 'active_torrents',
+ width: 80,
+ value: -1,
+ decimalPrecision: 0,
+ });
+ this.activeDownloading = this.slowSettings.add({
+ fieldLabel: _('Active Downloading'),
+ name: 'active_downloading',
+ width: 80,
+ value: -1,
+ decimalPrecision: 0,
+ });
+ this.activeSeeding = this.slowSettings.add({
+ fieldLabel: _('Active Seeding'),
+ name: 'active_seeding',
+ width: 80,
+ value: -1,
+ decimalPrecision: 0,
+ });
+
+ this.on('show', this.updateConfig, this);
+ },
+
+ onRender: function (ct, position) {
+ Deluge.ux.preferences.SchedulerPage.superclass.onRender.call(
+ this,
+ ct,
+ position
+ );
+ this.form.layout = new Ext.layout.FormLayout();
+ this.form.layout.setContainer(this);
+ this.form.doLayout();
+ },
+
+ onApply: function () {
+ // build settings object
+ var config = {};
+
+ config['button_state'] = this.schedule.getConfig();
+ config['low_down'] = this.downloadLimit.getValue();
+ config['low_up'] = this.uploadLimit.getValue();
+ config['low_active'] = this.activeTorrents.getValue();
+ config['low_active_down'] = this.activeDownloading.getValue();
+ config['low_active_up'] = this.activeSeeding.getValue();
+
+ deluge.client.scheduler.set_config(config);
+ },
+
+ onOk: function () {
+ this.onApply();
+ },
+
+ updateConfig: function () {
+ deluge.client.scheduler.get_config({
+ success: function (config) {
+ this.schedule.setConfig(config['button_state']);
+ this.downloadLimit.setValue(config['low_down']);
+ this.uploadLimit.setValue(config['low_up']);
+ this.activeTorrents.setValue(config['low_active']);
+ this.activeDownloading.setValue(config['low_active_down']);
+ this.activeSeeding.setValue(config['low_active_up']);
+ },
+ scope: this,
+ });
+ },
+});
+
+Deluge.plugins.SchedulerPlugin = Ext.extend(Deluge.Plugin, {
+ name: 'Scheduler',
+
+ onDisable: function () {
+ deluge.preferences.removePage(this.prefsPage);
+ },
+
+ onEnable: function () {
+ this.prefsPage = deluge.preferences.addPage(
+ new Deluge.ux.preferences.SchedulerPage()
+ );
+ },
+});
+Deluge.registerPlugin('Scheduler', Deluge.plugins.SchedulerPlugin);
diff --git a/deluge/plugins/Scheduler/deluge_scheduler/data/yellow.svg b/deluge/plugins/Scheduler/deluge_scheduler/data/yellow.svg
new file mode 100644
index 0000000..8881a8c
--- /dev/null
+++ b/deluge/plugins/Scheduler/deluge_scheduler/data/yellow.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 16 16" width="16" height="16"><defs><clipPath id="_clipPath_FTEVJ02JqQbaAkGq0zxCClZ8ovSf28LF"><rect width="16" height="16"/></clipPath></defs><g clip-path="url(#_clipPath_FTEVJ02JqQbaAkGq0zxCClZ8ovSf28LF)"><clipPath id="_clipPath_kMUc2qeZPnpfemB5VN1mID2bTbctQK6V"><rect x="0" y="0" width="16" height="16" transform="matrix(1,0,0,1,0,0)" fill="rgb(255,255,255)"/></clipPath><g clip-path="url(#_clipPath_kMUc2qeZPnpfemB5VN1mID2bTbctQK6V)"><g><clipPath id="_clipPath_O3gUc8WX8CfJdh8CMbdOfLtRtmBkIQPk"><rect x="0" y="0" width="16" height="16" transform="matrix(1,0,0,1,0,0)" fill="rgb(255,255,255)"/></clipPath><g clip-path="url(#_clipPath_O3gUc8WX8CfJdh8CMbdOfLtRtmBkIQPk)"><g><g><g><path d=" M 7.5 0.013 C 3.37 0.013 0.013 3.37 0.013 7.5 C 0.013 11.63 3.37 14.987 7.5 14.987 C 11.63 14.987 14.987 11.63 14.987 7.5 C 14.987 3.37 11.63 0.013 7.5 0.013 Z M 7.5 1.987 C 10.549 1.987 13.013 4.451 13.013 7.5 C 13.013 10.549 10.549 13.013 7.5 13.013 C 4.451 13.013 1.987 10.549 1.987 7.5 C 1.987 4.451 4.451 1.987 7.5 1.987 Z " fill="rgb(180,180,0)"/><g style="opacity:0.99;"><g style="opacity:0.99;"><g opacity="0.99"><circle vector-effect="non-scaling-stroke" cx="7.5" cy="7.5" r="6" fill="rgb(220,220,0)"/></g></g></g><path d=" M 10.406 4 C 10.309 4.026 10.222 4.08 10.156 4.156 L 7.5 6.813 L 5.844 5.156 C 5.736 4.98 5.53 4.888 5.326 4.925 C 5.123 4.963 4.963 5.123 4.925 5.326 C 4.888 5.53 4.98 5.736 5.156 5.844 L 7.156 7.844 C 7.349 8.026 7.651 8.026 7.844 7.844 L 10.844 4.844 C 10.995 4.689 11.03 4.455 10.931 4.263 C 10.831 4.071 10.62 3.965 10.406 4 Z " fill="rgb(43,46,57)"/></g></g></g></g></g></g></g></svg>
diff --git a/deluge/plugins/Scheduler/deluge_scheduler/gtkui.py b/deluge/plugins/Scheduler/deluge_scheduler/gtkui.py
new file mode 100644
index 0000000..16222c8
--- /dev/null
+++ b/deluge/plugins/Scheduler/deluge_scheduler/gtkui.py
@@ -0,0 +1,356 @@
+#
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-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 logging
+
+from gi.repository import Gdk, Gtk
+
+import deluge.component as component
+from deluge.plugins.pluginbase import Gtk3PluginBase
+from deluge.ui.client import client
+
+from .common import get_resource
+
+log = logging.getLogger(__name__)
+
+DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+
+
+class SchedulerSelectWidget(Gtk.DrawingArea):
+ def __init__(self, hover):
+ super().__init__()
+ self.set_events(
+ Gdk.EventMask.BUTTON_PRESS_MASK
+ | Gdk.EventMask.BUTTON_RELEASE_MASK
+ | Gdk.EventMask.POINTER_MOTION_MASK
+ | Gdk.EventMask.LEAVE_NOTIFY_MASK
+ )
+
+ self.connect('draw', self.draw)
+ 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 / 255, 210 / 255, 22 / 255],
+ [237 / 255, 212 / 255, 0 / 255],
+ [204 / 255, 0 / 255, 0 / 255],
+ ]
+ self.button_state = [[0] * 7 for dummy in range(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 draw(self, widget, context):
+ width = widget.get_allocated_width()
+ height = widget.get_allocated_height()
+ context.rectangle(0, 0, width, height)
+ context.clip()
+
+ for y in range(7):
+ for x in range(24):
+ 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.5,
+ )
+ context.rectangle(
+ width * (6 * x / 145 + 1 / 145),
+ height * (6 * y / 43 + 1 / 43),
+ 6 * width / 145,
+ 5 * height / 43,
+ )
+ context.fill_preserve()
+ context.set_source_rgba(0, 0, 0, 0.7)
+ context.set_line_width(1)
+ context.stroke()
+
+ # coordinates --> which box
+ def get_point(self, event):
+ width = self.get_allocated_width()
+ height = self.get_allocated_height()
+ x = int((event.x - width * 0.5 / 145) / (6 * width / 145))
+ y = int((event.y - height * 0.5 / 43) / (6 * height / 43))
+
+ 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:
+ points = [
+ [self.hover_point[0], self.start_point[0]],
+ [self.hover_point[1], self.start_point[1]],
+ ]
+
+ for x in range(min(points[0]), max(points[0]) + 1):
+ for y in range(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(Gtk3PluginBase):
+ 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.statusbar = component.get('StatusBar')
+ self.status_item = self.statusbar.add_item(
+ image=get_resource('green.svg'),
+ text='',
+ callback=self.on_status_item_clicked,
+ tooltip='Scheduler',
+ )
+
+ def on_state_deferred(state):
+ self.state = state
+ self.on_scheduler_event(state)
+
+ self.on_show_prefs()
+
+ client.scheduler.get_state().addCallback(on_state_deferred)
+ client.register_event_handler('SchedulerEvent', self.on_scheduler_event)
+
+ def disable(self):
+ component.get('Preferences').remove_page(_('Scheduler'))
+ # Reset statusbar dict.
+ self.statusbar.config_value_changed_dict[
+ 'max_download_speed'
+ ] = self.statusbar._on_max_download_speed
+ self.statusbar.config_value_changed_dict[
+ 'max_upload_speed'
+ ] = self.statusbar._on_max_upload_speed
+ # Remove statusbar item.
+ self.statusbar.remove_item(self.status_item)
+ del self.status_item
+
+ 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['low_active_down'] = self.spin_active_down.get_value_as_int()
+ config['low_active_up'] = self.spin_active_up.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'])
+ self.spin_active_down.set_value(config['low_active_down'])
+ self.spin_active_up.set_value(config['low_active_up'])
+
+ client.scheduler.get_config().addCallback(on_get_config)
+
+ def on_scheduler_event(self, state):
+ self.state = state
+ self.status_item.set_image_from_file(get_resource(self.state.lower() + '.svg'))
+ if self.state == 'Yellow':
+ # Prevent func calls in Statusbar if the config changes.
+ self.statusbar.config_value_changed_dict.pop('max_download_speed', None)
+ self.statusbar.config_value_changed_dict.pop('max_upload_speed', None)
+ try:
+ self.statusbar._on_max_download_speed(self.spin_download.get_value())
+ self.statusbar._on_max_upload_speed(self.spin_upload.get_value())
+ except AttributeError:
+ # Skip error due to Plugin being enabled before statusbar items created on startup.
+ pass
+ else:
+ self.statusbar.config_value_changed_dict[
+ 'max_download_speed'
+ ] = self.statusbar._on_max_download_speed
+ self.statusbar.config_value_changed_dict[
+ 'max_upload_speed'
+ ] = self.statusbar._on_max_upload_speed
+
+ def update_config_values(config):
+ try:
+ self.statusbar._on_max_download_speed(config['max_download_speed'])
+ self.statusbar._on_max_upload_speed(config['max_upload_speed'])
+ except AttributeError:
+ # Skip error due to Plugin being enabled before statusbar items created on startup.
+ pass
+
+ client.core.get_config_values(
+ ['max_download_speed', 'max_upload_speed']
+ ).addCallback(update_config_values)
+
+ 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.Box.new(Gtk.Orientation.VERTICAL, spacing=5)
+ hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, spacing=5)
+ vbox_days = Gtk.Box.new(Gtk.Orientation.VERTICAL, spacing=0)
+ for day in DAYS:
+ vbox_days.pack_start(Gtk.Label(day, xalign=0), True, False, 0)
+ hbox.pack_start(vbox_days, False, False, 15)
+ hbox.pack_start(self.scheduler_select, True, True, 0)
+ frame = Gtk.Frame()
+ label = Gtk.Label()
+ label.set_markup(_('<b>Schedule</b>'))
+ frame.set_label_widget(label)
+ frame.set_shadow_type(Gtk.ShadowType.NONE)
+ frame.set_margin_left(15)
+ frame.add(hbox)
+
+ vbox.pack_start(frame, False, False, 0)
+ vbox.pack_start(hover, False, False, 0)
+
+ table = Gtk.Table(5, 2)
+ table.set_margin_left(15)
+
+ label = Gtk.Label(_('Download Limit:'))
+ label.set_alignment(0.0, 0.6)
+ table.attach_defaults(label, 0, 1, 0, 1)
+ 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_defaults(self.spin_download, 1, 2, 0, 1)
+
+ label = Gtk.Label(_('Upload Limit:'))
+ label.set_alignment(0.0, 0.6)
+ table.attach_defaults(label, 0, 1, 1, 2)
+ 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_defaults(self.spin_upload, 1, 2, 1, 2)
+
+ label = Gtk.Label(_('Active Torrents:'))
+ label.set_alignment(0.0, 0.6)
+ table.attach_defaults(label, 0, 1, 2, 3)
+ 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_defaults(self.spin_active, 1, 2, 2, 3)
+
+ label = Gtk.Label(_('Active Downloading:'))
+ label.set_alignment(0.0, 0.6)
+ table.attach_defaults(label, 0, 1, 3, 4)
+ self.spin_active_down = Gtk.SpinButton()
+ self.spin_active_down.set_numeric(True)
+ self.spin_active_down.set_range(-1, 9999)
+ self.spin_active_down.set_increments(1, 10)
+ table.attach_defaults(self.spin_active_down, 1, 2, 3, 4)
+
+ label = Gtk.Label(_('Active Seeding:'))
+ label.set_alignment(0.0, 0.6)
+ table.attach_defaults(label, 0, 1, 4, 5)
+ self.spin_active_up = Gtk.SpinButton()
+ self.spin_active_up.set_numeric(True)
+ self.spin_active_up.set_range(-1, 9999)
+ self.spin_active_up.set_increments(1, 10)
+ table.attach_defaults(self.spin_active_up, 1, 2, 4, 5)
+
+ eventbox = Gtk.EventBox()
+ eventbox.add(table)
+ frame = Gtk.Frame()
+ label = Gtk.Label()
+ label.set_markup(_('<b>Slow Settings</b>'))
+ label.modify_bg(Gtk.StateFlags.NORMAL, Gdk.color_parse('#EDD400'))
+ frame.set_label_widget(label)
+ frame.set_margin_left(15)
+ frame.set_border_width(2)
+ frame.add(eventbox)
+ vbox.pack_start(frame, False, False, 0)
+
+ vbox.show_all()
+ component.get('Preferences').add_page(_('Scheduler'), vbox)
diff --git a/deluge/plugins/Scheduler/deluge_scheduler/webui.py b/deluge/plugins/Scheduler/deluge_scheduler/webui.py
new file mode 100644
index 0000000..e417916
--- /dev/null
+++ b/deluge/plugins/Scheduler/deluge_scheduler/webui.py
@@ -0,0 +1,23 @@
+#
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-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 logging
+
+from deluge.plugins.pluginbase import WebPluginBase
+
+from .common import get_resource
+
+log = logging.getLogger(__name__)
+
+
+class WebUI(WebPluginBase):
+ scripts = [get_resource('scheduler.js')]
+ debug_scripts = scripts
diff --git a/deluge/plugins/Scheduler/setup.py b/deluge/plugins/Scheduler/setup.py
new file mode 100644
index 0000000..3ac181d
--- /dev/null
+++ b/deluge/plugins/Scheduler/setup.py
@@ -0,0 +1,45 @@
+#
+# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-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 setuptools import find_packages, setup
+
+__plugin_name__ = 'Scheduler'
+__author__ = 'Andrew Resch'
+__author_email__ = 'andrewresch@gmail.com'
+__version__ = '0.3'
+__url__ = 'http://deluge-torrent.org'
+__license__ = 'GPLv3'
+__description__ = 'Schedule limits on a per-hour per-day basis.'
+__long_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__ if __long_description__ else __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),
+)
diff --git a/deluge/plugins/Stats/create_dev_link.sh b/deluge/plugins/Stats/create_dev_link.sh
new file mode 100755
index 0000000..5e04057
--- /dev/null
+++ b/deluge/plugins/Stats/create_dev_link.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+BASEDIR=$(cd `dirname $0` && pwd)
+CONFIG_DIR=$( test -z $1 && echo "" || echo "$1")
+[ -d "$CONFIG_DIR/plugins" ] || echo "Config dir "$CONFIG_DIR" is either not a directory or is not a proper deluge config directory. Exiting"
+[ -d "$CONFIG_DIR/plugins" ] || exit 1
+cd $BASEDIR
+test -d $BASEDIR/temp || mkdir $BASEDIR/temp
+export PYTHONPATH=$BASEDIR/temp
+python setup.py build develop --install-dir $BASEDIR/temp
+cp $BASEDIR/temp/*.egg-link $CONFIG_DIR/plugins
+rm -fr $BASEDIR/temp
diff --git a/deluge/plugins/Stats/deluge_stats/__init__.py b/deluge/plugins/Stats/deluge_stats/__init__.py
new file mode 100644
index 0000000..ca7b0bb
--- /dev/null
+++ b/deluge/plugins/Stats/deluge_stats/__init__.py
@@ -0,0 +1,37 @@
+#
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-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 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/Stats/deluge_stats/common.py b/deluge/plugins/Stats/deluge_stats/common.py
new file mode 100644
index 0000000..eb47f13
--- /dev/null
+++ b/deluge/plugins/Stats/deluge_stats/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/Stats/deluge_stats/core.py b/deluge/plugins/Stats/deluge_stats/core.py
new file mode 100644
index 0000000..1be51e6
--- /dev/null
+++ b/deluge/plugins/Stats/deluge_stats/core.py
@@ -0,0 +1,218 @@
+#
+# Copyright (C) 2009 Ian Martin <ianmartin@cantab.net>
+# Copyright (C) 2008 Damien Churchill <damoxc@gmail.com>
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007 Marcos Mobley <markybob@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 time
+
+from twisted.internet.task import LoopingCall
+
+from deluge import component, configmanager
+from deluge.core.rpcserver import export
+from deluge.plugins.pluginbase import CorePluginBase
+
+DEFAULT_PREFS = {
+ 'test': 'NiNiNi',
+ 'update_interval': 1, # 2 seconds.
+ 'length': 150, # 2 seconds * 150 --> 5 minutes.
+}
+
+DEFAULT_TOTALS = {
+ 'total_upload': 0,
+ 'total_download': 0,
+ 'total_payload_upload': 0,
+ 'total_payload_download': 0,
+ 'stats': {},
+}
+
+log = logging.getLogger(__name__)
+
+
+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.length = self.config['length']
+
+ # self.stats = get_key(self.saved_stats, 'stats') or {}
+
+ # keys needed from core.get_session_status
+ self.stat_keys = [
+ 'upload_rate',
+ 'download_rate',
+ 'dht.dht_nodes',
+ 'dht.dht_node_cache',
+ 'dht.dht_torrents',
+ 'peer.num_peers_connected',
+ 'peer.num_peers_half_open',
+ ]
+ # collected statistics and functions to get them
+ self.stat_getters = {
+ 'upload_rate': lambda s: s['upload_rate'],
+ 'download_rate': lambda s: s['download_rate'],
+ 'dht_nodes': lambda s: s['dht.dht_nodes'],
+ 'dht_cache_nodes': lambda s: s['dht.dht_node_cache'],
+ 'dht_torrents': lambda s: s['dht.dht_torrents'],
+ 'num_peers': lambda s: s['peer.num_peers_connected'],
+ 'num_connections': lambda s: s['peer.num_peers_connected']
+ + s['peer.num_peers_half_open'],
+ }
+
+ # initialize stats object
+ for key in self.stat_getters.keys():
+ for i in self.intervals:
+ if key not in self.stats[i]:
+ self.stats[i][key] = []
+
+ self.update_stats()
+
+ self.update_timer = LoopingCall(self.update_stats)
+ self.update_timer.start(self.config['update_interval'])
+
+ self.save_timer = LoopingCall(self.save_stats)
+ self.save_timer.start(60)
+
+ def disable(self):
+ self.update_timer.stop() if self.update_timer.running else None
+ self.save_timer.stop() if self.save_timer.running else None
+ self.save_stats()
+
+ def update_stats(self):
+ # Get all possible stats!
+ stats = {}
+ raw_stats = self.core.get_session_status(self.stat_keys)
+ for name, fn in self.stat_getters.items():
+ stats[name] = fn(raw_stats)
+
+ # 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].items():
+ 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()
+
+ 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].items():
+ 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)
+
+ def save_stats(self):
+ self.saved_stats['stats'] = self.stats
+ self.saved_stats.config.update(self.get_totals())
+ self.saved_stats.save()
+
+ # export:
+ @export
+ 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[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
+ def get_totals(self):
+ result = {}
+ session_totals = self.get_session_totals()
+ for key in session_totals:
+ result[key] = self.totals[key] + session_totals[key]
+ return result
+
+ @export
+ def get_session_totals(self):
+ return self.core.get_session_status(
+ [
+ 'total_upload',
+ 'total_download',
+ 'total_payload_upload',
+ 'total_payload_download',
+ ]
+ )
+
+ @export
+ def set_config(self, config):
+ """Sets the config dictionary."""
+ for key in config:
+ self.config[key] = config[key]
+ self.config.save()
+
+ @export
+ 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_stats/data/config.ui b/deluge/plugins/Stats/deluge_stats/data/config.ui
new file mode 100644
index 0000000..326598b
--- /dev/null
+++ b/deluge/plugins/Stats/deluge_stats/data/config.ui
@@ -0,0 +1,284 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <object class="GtkWindow" id="window1">
+ <property name="can_focus">False</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkBox" id="prefs_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkFrame" id="frame1">
+ <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="alignment4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="left_padding">15</property>
+ <child>
+ <object class="GtkTable" id="table2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="n_rows">10</property>
+ <property name="n_columns">2</property>
+ <property name="column_spacing">15</property>
+ <child>
+ <object class="GtkColorButton" id="bandwidth_graph_download_rate_color">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="color">#000000000000</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="x_options">GTK_EXPAND</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Download color:</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Upload color:</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkColorButton" id="bandwidth_graph_upload_rate_color">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="color">#000000000000</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="x_options">GTK_EXPAND</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label8">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;b&gt;Connections Graph&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="right_attach">2</property>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label9">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;b&gt;Bandwidth Graph&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="right_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkColorButton" id="connections_graph_dht_nodes_color">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="color">#000000000000</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">4</property>
+ <property name="bottom_attach">5</property>
+ <property name="x_options">GTK_EXPAND</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label10">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">DHT nodes:</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="top_attach">4</property>
+ <property name="bottom_attach">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkColorButton" id="connections_graph_dht_cache_nodes_color">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="color">#000000000000</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">5</property>
+ <property name="bottom_attach">6</property>
+ <property name="x_options">GTK_EXPAND</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label11">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Cached DHT nodes:</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="top_attach">5</property>
+ <property name="bottom_attach">6</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label12">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">DHT torrents:</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="top_attach">6</property>
+ <property name="bottom_attach">7</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label13">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Connections:</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="top_attach">7</property>
+ <property name="bottom_attach">8</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkColorButton" id="connections_graph_dht_torrents_color">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="color">#000000000000</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">6</property>
+ <property name="bottom_attach">7</property>
+ <property name="x_options">GTK_EXPAND</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkColorButton" id="connections_graph_num_connections_color">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="color">#000000000000</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">7</property>
+ <property name="bottom_attach">8</property>
+ <property name="x_options">GTK_EXPAND</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label16">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;b&gt;Seeds / Peers&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="right_attach">2</property>
+ <property name="top_attach">8</property>
+ <property name="bottom_attach">9</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkColorButton" id="seeds_graph_num_peers_color">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="color">#000000000000</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">9</property>
+ <property name="bottom_attach">10</property>
+ <property name="x_options">GTK_EXPAND</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label17">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Peers:</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="top_attach">9</property>
+ <property name="bottom_attach">10</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;b&gt;Graph Colors&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/deluge/plugins/Stats/deluge_stats/data/stats.js b/deluge/plugins/Stats/deluge_stats/data/stats.js
new file mode 100644
index 0000000..7ba3d27
--- /dev/null
+++ b/deluge/plugins/Stats/deluge_stats/data/stats.js
@@ -0,0 +1,27 @@
+/**
+ * Script: stats.js
+ * The javascript client-side code for the Stats plugin.
+ *
+ * Copyright (c) Damien Churchill 2009-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.
+ */
+
+StatsPlugin = Ext.extend(Deluge.Plugin, {
+ constructor: function (config) {
+ config = Ext.apply(
+ {
+ name: 'Stats',
+ },
+ config
+ );
+ StatsPlugin.superclass.constructor.call(this, config);
+ },
+
+ onDisable: function () {},
+
+ onEnable: function () {},
+});
+new StatsPlugin();
diff --git a/deluge/plugins/Stats/deluge_stats/data/tabs.ui b/deluge/plugins/Stats/deluge_stats/data/tabs.ui
new file mode 100644
index 0000000..4b35765
--- /dev/null
+++ b/deluge/plugins/Stats/deluge_stats/data/tabs.ui
@@ -0,0 +1,169 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <object class="GtkWindow" id="window1">
+ <property name="can_focus">False</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkBox" id="vbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="graph_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkLabel" id="graph_label_text">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Stats</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="graph_tab">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <child>
+ <object class="GtkViewport" id="viewport1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="resize_mode">queue</property>
+ <property name="shadow_type">none</property>
+ <child>
+ <object class="GtkBox" id="vbox2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="hbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkLabel" id="label1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Resolution</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBox" id="combo_intervals">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkNotebook" id="graph_notebook">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="tab_pos">left</property>
+ <child>
+ <object class="GtkDrawingArea" id="bandwidth_graph">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="bandwidth_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Bandwidth</property>
+ </object>
+ <packing>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkDrawingArea" id="connections_graph">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="connections_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Connections</property>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkDrawingArea" id="seeds_graph">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="seeds_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Seeds/Peers</property>
+ </object>
+ <packing>
+ <property name="position">2</property>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/deluge/plugins/Stats/deluge_stats/graph.py b/deluge/plugins/Stats/deluge_stats/graph.py
new file mode 100644
index 0000000..ddb8f54
--- /dev/null
+++ b/deluge/plugins/Stats/deluge_stats/graph.py
@@ -0,0 +1,343 @@
+#
+# Copyright (C) 2009 Ian Martin <ianmartin@cantab.net>
+# Copyright (C) 2008 Damien Churchill <damoxc@gmail.com>
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007 Marcos Mobley <markybob@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.
+#
+
+"""
+port of old plugin by markybob.
+"""
+
+import logging
+import math
+import time
+
+import gi
+
+gi.require_foreign('cairo')
+
+import cairo # isort:skip (gi checks required before import).
+
+log = logging.getLogger(__name__)
+
+black = (0, 0, 0)
+gray = (0.75, 0.75, 0.75)
+white = (1.0, 1.0, 1.0)
+darkred = (0.65, 0, 0)
+red = (1.0, 0, 0)
+green = (0, 1.0, 0)
+blue = (0, 0, 1.0)
+orange = (1.0, 0.74, 0)
+
+
+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 opactiy of a color inorder to draw the
+ fills.
+ """
+ color = list(color)
+ if len(color) == 4:
+ color[3] = opactiy
+ else:
+ color.append(opactiy)
+ return tuple(color)
+
+
+class Graph:
+ def __init__(self):
+ self.width = 100
+ self.height = 100
+ 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
+ self.black = (0, 0, 0)
+ self.interval = 2 # 2 secs
+ self.text_bg = (255, 255, 255, 128) # prototyping
+ self.set_left_axis()
+
+ def set_left_axis(self, **kargs):
+ self.left_axis = kargs
+
+ def add_stat(self, stat, label='', axis='left', line=True, fill=True, color=None):
+ self.stat_info[stat] = {
+ 'axis': axis,
+ 'label': label,
+ 'line': line,
+ 'fill': fill,
+ 'color': color,
+ }
+
+ def set_stats(self, stats):
+ self.last_update = stats['_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_interval(self, interval):
+ self.interval = interval
+
+ def draw_to_context(self, ctx, width, height):
+ self.width, self.height = width, height
+ self.draw_rect(ctx, white, 0, 0, self.width, self.height)
+ self.draw_graph(ctx)
+
+ def draw(self, width, height):
+ """Create surface with context for use in tests"""
+ surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
+ ctx = cairo.Context(surface)
+ self.draw_to_context(ctx, width, height)
+ return surface
+
+ def draw_x_axis(self, ctx, bounds):
+ (left, top, right, bottom) = bounds
+ duration = self.length * self.interval
+ start = self.last_update - duration
+ ratio = (right - left) / 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
+
+ # this doesnt allow for dst and timezones...
+ seconds_to_step = math.ceil(start / x_step) * x_step - start
+
+ for i in range(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(ctx, text, x, bottom)
+ self.draw_dotted_line(ctx, gray, x, top - 0.5, x, bottom + 0.5)
+
+ self.draw_line(ctx, gray, left, bottom + 0.5, right, bottom + 0.5)
+
+ def draw_graph(self, ctx):
+ font_extents = ctx.font_extents()
+ x_axis_space = font_extents[2] + 2 + self.line_size / 2
+ 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]
+
+ 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(ctx, text):
+ te = ctx.text_extents(text)
+ return math.ceil(te[4] - te[0])
+
+ y_tick_width = max(space_required(ctx, text) for text in y_tick_text)
+
+ top = font_extents[2] / 2
+ # bounds(left, top, right, bottom)
+ bounds = (y_tick_width + 4, top + 2, self.width, self.height - x_axis_space)
+
+ self.draw_x_axis(ctx, bounds)
+ self.draw_left_axis(ctx, 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 / scale
+
+ # Find the largest power of 10 less than x
+ comm_log = math.log10(x)
+ intbit = math.floor(comm_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 / limit
+ if multi > 2:
+ interval = interval * 5
+ else:
+ interval = interval * 2
+
+ intervals = [
+ i * interval * scale for i in range(1 + int(math.ceil(x / interval)))
+ ]
+ return intervals
+
+ def draw_left_axis(self, ctx, bounds, y_ticks, y_tick_text):
+ (left, top, right, bottom) = bounds
+ stats = {}
+ 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)
+
+ height = bottom - top
+ max_value = y_ticks[-1]
+ ratio = height / max_value
+
+ for i, y_val in enumerate(y_ticks):
+ y = int(bottom - y_val * ratio) - 0.5
+ if i != 0:
+ self.draw_dotted_line(ctx, gray, left, y, right, y)
+ self.draw_y_text(ctx, y_tick_text[i], left, y)
+ self.draw_line(ctx, gray, left, top, left, bottom)
+
+ for stat, info in stats.items():
+ if len(info['values']) > 0:
+ self.draw_value_poly(
+ ctx, info['values'], info['color'], max_value, bounds
+ )
+ self.draw_value_poly(
+ ctx,
+ info['values'],
+ info['fill_color'],
+ max_value,
+ bounds,
+ info['fill'],
+ )
+
+ def draw_legend(self):
+ pass
+
+ def trace_path(self, ctx, values, max_value, bounds):
+ (left, top, right, bottom) = bounds
+ ratio = (bottom - top) / max_value
+ line_width = self.line_size
+
+ ctx.set_line_width(line_width)
+ ctx.move_to(right, bottom)
+
+ ctx.line_to(right, int(bottom - values[0] * ratio))
+
+ x = right
+ step = (right - left) / (self.length - 1)
+ for i, value in enumerate(values):
+ if i == self.length - 1:
+ x = left
+
+ ctx.line_to(x, int(bottom - value * ratio))
+ x -= step
+
+ ctx.line_to(int(right - (len(values) - 1) * step), bottom)
+ ctx.close_path()
+
+ def draw_value_poly(self, ctx, values, color, max_value, bounds, fill=False):
+ self.trace_path(ctx, values, max_value, bounds)
+ ctx.set_source_rgba(*color)
+
+ if fill:
+ ctx.fill()
+ else:
+ ctx.stroke()
+
+ def draw_x_text(self, ctx, text, x, y):
+ """Draws text below and horizontally centered about x,y"""
+ fe = ctx.font_extents()
+ te = ctx.text_extents(text)
+ height = fe[2]
+ x_bearing = te[0]
+ width = te[2]
+ ctx.move_to(int(x - width / 2 + x_bearing), int(y + height))
+ ctx.set_source_rgba(*self.black)
+ ctx.show_text(text)
+
+ def draw_y_text(self, ctx, text, x, y):
+ """Draws text left of and vertically centered about x,y"""
+ fe = ctx.font_extents()
+ te = ctx.text_extents(text)
+ descent = fe[1]
+ ascent = fe[0]
+ x_bearing = te[0]
+ width = te[4]
+ ctx.move_to(int(x - width - x_bearing - 2), int(y + (ascent - descent) / 2))
+ ctx.set_source_rgba(*self.black)
+ ctx.show_text(text)
+
+ def draw_rect(self, ctx, color, x, y, height, width):
+ ctx.set_source_rgba(*color)
+ ctx.rectangle(x, y, height, width)
+ ctx.fill()
+
+ def draw_line(self, ctx, color, x1, y1, x2, y2):
+ ctx.set_source_rgba(*color)
+ ctx.set_line_width(1)
+ ctx.move_to(x1, y1)
+ ctx.line_to(x2, y2)
+ ctx.stroke()
+
+ def draw_dotted_line(self, ctx, color, x1, y1, x2, y2):
+ ctx.set_source_rgba(*color)
+ ctx.set_line_width(1)
+ dash, offset = ctx.get_dash()
+ ctx.set_dash(self.dash_length, 0)
+ ctx.move_to(x1, y1)
+ ctx.line_to(x2, y2)
+ ctx.stroke()
+ ctx.set_dash(dash, offset)
diff --git a/deluge/plugins/Stats/deluge_stats/gtkui.py b/deluge/plugins/Stats/deluge_stats/gtkui.py
new file mode 100644
index 0000000..39c1d4c
--- /dev/null
+++ b/deluge/plugins/Stats/deluge_stats/gtkui.py
@@ -0,0 +1,296 @@
+#
+# Copyright (C) 2009 Ian Martin <ianmartin@cantab.net>
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-2008 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 logging
+
+import gi # isort:skip (Required before Gtk import).
+
+gi.require_version('Gtk', '3.0')
+
+from gi.repository import Gtk
+from gi.repository.Gdk import RGBA
+
+import deluge
+from deluge import component
+from deluge.common import fspeed
+from deluge.plugins.pluginbase import Gtk3PluginBase
+from deluge.ui.client import client
+from deluge.ui.gtk3.torrentdetails import Tab
+
+from .common import get_resource
+from .graph import Graph, size_formatter_scale
+
+log = logging.getLogger(__name__)
+
+# Gdk.RGBA textual spec
+RED = 'rgb(255,0,0)'
+GREEN = 'rgb(0,128,0)'
+BLUE = 'rgb(0,0,255)'
+DARKRED = 'rgb(139,0,0)'
+ORANGE = 'rgb(255,165,0)'
+
+DEFAULT_CONF = {
+ 'version': 2,
+ 'colors': {
+ 'bandwidth_graph': {'upload_rate': BLUE, 'download_rate': GREEN},
+ 'connections_graph': {
+ 'dht_nodes': ORANGE,
+ 'dht_cache_nodes': BLUE,
+ 'dht_torrents': GREEN,
+ 'num_connections': DARKRED,
+ },
+ 'seeds_graph': {'num_peers': BLUE},
+ },
+}
+
+
+def neat_time(column, cell, model, data):
+ """Render seconds as seconds or minutes with label"""
+ seconds = model.get_value(data, 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 fspeed_shortform(value):
+ return fspeed(value, shortform=True)
+
+
+def text_to_rgba(color):
+ """Turns a Color into a tuple with range 0-1 as used by the graph"""
+ color_rgba = RGBA()
+ color_rgba.parse(color)
+ return color_rgba
+
+
+class GraphsTab(Tab):
+ def __init__(self, colors):
+ super().__init__()
+
+ builder = Gtk.Builder()
+ builder.add_from_file(get_resource('tabs.ui'))
+ self.window = builder.get_object('graph_tab')
+ self.notebook = builder.get_object('graph_notebook')
+ self.label = builder.get_object('graph_label')
+
+ self._name = 'Stats'
+ self._child_widget = self.window
+ self._tab_label = self.label
+
+ self.colors = colors
+
+ self.bandwidth_graph = builder.get_object('bandwidth_graph')
+ self.bandwidth_graph.connect('draw', self.on_graph_draw)
+
+ self.connections_graph = builder.get_object('connections_graph')
+ self.connections_graph.connect('draw', self.on_graph_draw)
+
+ self.seeds_graph = builder.get_object('seeds_graph')
+ self.seeds_graph.connect('draw', self.on_graph_draw)
+
+ 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.intervals = None
+ self.intervals_combo = builder.get_object('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 on_graph_draw(self, widget, context):
+ self.graph.draw_to_context(
+ context,
+ self.graph_widget.get_allocated_width(),
+ self.graph_widget.get_allocated_height(),
+ )
+ # Do not propagate the event
+ return True
+
+ def update(self):
+ d1 = client.stats.get_stats(list(self.graph.stat_info), self.selected_interval)
+ d1.addCallback(self.graph.set_stats)
+
+ def _update_complete(result):
+ self.graph_widget.queue_draw()
+ return result
+
+ d1.addCallback(_update_complete)
+ return d1
+
+ 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()
+ colors = self.colors['bandwidth_graph']
+ self.graph.add_stat(
+ 'download_rate',
+ label='Download Rate',
+ color=text_to_rgba(colors['download_rate']),
+ )
+ self.graph.add_stat(
+ 'upload_rate',
+ label='Upload Rate',
+ color=text_to_rgba(colors['upload_rate']),
+ )
+ self.graph.set_left_axis(
+ formatter=fspeed_shortform, min=10240, formatter_scale=size_formatter_scale
+ )
+
+ def select_connections_graph(self):
+ log.debug('Selecting connections graph')
+ self.graph_widget = self.connections_graph
+ g = Graph()
+ self.graph = g
+ colors = self.colors['connections_graph']
+ g.add_stat('dht_nodes', color=text_to_rgba(colors['dht_nodes']))
+ g.add_stat('dht_cache_nodes', color=text_to_rgba(colors['dht_cache_nodes']))
+ g.add_stat('dht_torrents', color=text_to_rgba(colors['dht_torrents']))
+ g.add_stat('num_connections', color=text_to_rgba(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()
+ colors = self.colors['seeds_graph']
+ self.graph.add_stat('num_peers', color=text_to_rgba(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, self.notebook.get_current_page() # This is unused
+ )
+
+ 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 Exception:
+ 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()
+ tree_iter = combobox.get_active_iter()
+ self.selected_interval = model.get_value(tree_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(Gtk3PluginBase):
+ def enable(self):
+ log.debug('Stats plugin enable called')
+ self.config = deluge.configmanager.ConfigManager(
+ 'stats.gtk3ui.conf', DEFAULT_CONF
+ )
+
+ self.builder = Gtk.Builder()
+ self.builder.add_from_file(get_resource('config.ui'))
+
+ component.get('Preferences').add_page(
+ 'Stats', self.builder.get_object('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(self.config['colors'])
+ self.torrent_details = component.get('TorrentDetails')
+ self.torrent_details.add_tab(self.graphs_tab)
+
+ def disable(self):
+ component.get('Preferences').remove_page('Stats')
+ component.get('PluginManager').deregister_hook(
+ 'on_apply_prefs', self.on_apply_prefs
+ )
+ component.get('PluginManager').deregister_hook(
+ 'on_show_prefs', self.on_show_prefs
+ )
+ self.torrent_details.remove_tab(self.graphs_tab.get_name())
+
+ def on_apply_prefs(self):
+ log.debug('applying prefs for Stats')
+ gtkconf = {}
+ for graph, colors in self.config['colors'].items():
+ gtkconf[graph] = {}
+ for value, color in colors.items():
+ color_btn = self.builder.get_object(f'{graph}_{value}_color')
+ try:
+ gtkconf[graph][value] = color_btn.get_color().to_string()
+ except Exception:
+ 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.builder.get_object(f'{graph}_{value}_color')
+ color_btn.set_rgba(text_to_rgba(color))
+ except Exception as ex:
+ log.debug('Unable to set %s %s %s: %s', graph, value, color, ex)
+ client.stats.get_config().addCallback(self.cb_get_config)
+
+ def cb_get_config(self, config):
+ """Callback for on show_prefs."""
+ pass
diff --git a/deluge/plugins/Stats/deluge_stats/template/graph.html b/deluge/plugins/Stats/deluge_stats/template/graph.html
new file mode 100644
index 0000000..2ff803b
--- /dev/null
+++ b/deluge/plugins/Stats/deluge_stats/template/graph.html
@@ -0,0 +1,12 @@
+$:render.header(_("Network Graph"), 'graph')
+$:render.admin_toolbar('graph')
+
+<div style="padding-left:20px">
+
+<img src="$base/graph/network.png?height=300&width=1000"><br \>
+<img src="$base/graph/connections.png?height=300&width=1000"><br \>
+</div>
+
+
+
+$:render.footer()
diff --git a/deluge/plugins/Stats/deluge_stats/tests/__init__.py b/deluge/plugins/Stats/deluge_stats/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/deluge/plugins/Stats/deluge_stats/tests/__init__.py
diff --git a/deluge/plugins/Stats/deluge_stats/tests/test.html b/deluge/plugins/Stats/deluge_stats/tests/test.html
new file mode 100644
index 0000000..7af5f15
--- /dev/null
+++ b/deluge/plugins/Stats/deluge_stats/tests/test.html
@@ -0,0 +1,9 @@
+<html>
+ <head>
+ <meta http-equiv="refresh" content="2" />
+ </head>
+ <body>
+ <img src="output_async.png" /> <br />
+ <img src="output_dht.png" />
+ </body>
+</html>
diff --git a/deluge/plugins/Stats/deluge_stats/tests/test_stats.py b/deluge/plugins/Stats/deluge_stats/tests/test_stats.py
new file mode 100644
index 0000000..d61cd46
--- /dev/null
+++ b/deluge/plugins/Stats/deluge_stats/tests/test_stats.py
@@ -0,0 +1,106 @@
+#
+# 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 pytest
+import pytest_twisted
+from twisted.internet import defer
+
+from deluge.common import fsize, fspeed
+from deluge.ui.client import client
+
+
+def print_totals(totals):
+ for name, value in totals.items():
+ print(name, fsize(value))
+
+ print('overhead:')
+ print('up:', fsize(totals['total_upload'] - totals['total_payload_upload']))
+ print('down:', fsize(totals['total_download'] - totals['total_payload_download']))
+
+
+class TestStatsPlugin:
+ @pytest_twisted.async_yield_fixture(autouse=True)
+ async def set_up(self, component):
+ defer.setDebugging(True)
+ client.start_standalone()
+ client.core.enable_plugin('Stats')
+ await component.start()
+ yield
+ client.stop_standalone()
+
+ @defer.inlineCallbacks
+ def test_client_totals(self):
+ plugins = yield client.core.get_available_plugins()
+ if 'Stats' not in plugins:
+ pytest.skip('Stats plugin not available for testing')
+
+ totals = yield client.stats.get_totals()
+ assert totals['total_upload'] == 0
+ assert totals['total_payload_upload'] == 0
+ assert totals['total_payload_download'] == 0
+ assert totals['total_download'] == 0
+ # print_totals(totals)
+
+ @defer.inlineCallbacks
+ def test_session_totals(self):
+ plugins = yield client.core.get_available_plugins()
+ if 'Stats' not in plugins:
+ pytest.skip('Stats plugin not available for testing')
+
+ totals = yield client.stats.get_session_totals()
+ assert totals['total_upload'] == 0
+ assert totals['total_payload_upload'] == 0
+ assert totals['total_payload_download'] == 0
+ assert totals['total_download'] == 0
+ # print_totals(totals)
+
+ @pytest.mark.gtkui
+ @defer.inlineCallbacks
+ def test_write(self, tmp_path):
+ """
+ writing to a file-like object; need this for webui.
+
+ Not strictly a unit test, but tests if calls do not fail...
+ """
+ from deluge_stats import graph, gtkui
+
+ from deluge.configmanager import ConfigManager
+ from deluge.ui.gtk3.gtkui import DEFAULT_PREFS
+ from deluge.ui.gtk3.mainwindow import MainWindow
+ from deluge.ui.gtk3.pluginmanager import PluginManager
+ from deluge.ui.gtk3.preferences import Preferences
+ from deluge.ui.gtk3.torrentdetails import TorrentDetails
+ from deluge.ui.gtk3.torrentview import TorrentView
+
+ ConfigManager('gtk3ui.conf', defaults=DEFAULT_PREFS)
+
+ self.plugins = PluginManager()
+ MainWindow()
+ TorrentView()
+ TorrentDetails()
+ Preferences()
+
+ class FakeFile:
+ def __init__(self):
+ self.data = []
+
+ def write(self, data):
+ self.data.append(data)
+
+ stats_gtkui = gtkui.GtkUI('test_stats')
+ stats_gtkui.enable()
+ yield stats_gtkui.graphs_tab.update()
+
+ g = stats_gtkui.graphs_tab.graph
+ g.add_stat('download_rate', color=graph.green)
+ g.add_stat('upload_rate', color=graph.blue)
+ g.set_left_axis(formatter=fspeed, min=10240)
+
+ surface = g.draw(900, 150)
+ file_like = FakeFile()
+ surface.write_to_png(file_like)
+ data = b''.join(file_like.data)
+ with open(tmp_path / 'file_like.png', 'wb') as _file:
+ _file.write(data)
diff --git a/deluge/plugins/Stats/deluge_stats/webui.py b/deluge/plugins/Stats/deluge_stats/webui.py
new file mode 100644
index 0000000..2c2ed46
--- /dev/null
+++ b/deluge/plugins/Stats/deluge_stats/webui.py
@@ -0,0 +1,32 @@
+#
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-2008 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 logging
+
+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 scrictly required on the WebUI
+ # plugins. They are only here if you need to register images/stylesheets
+ # with the webserver.
+ def enable(self):
+ log.debug('Stats Web plugin enabled!')
+
+ def disable(self):
+ log.debug('Stats Web plugin disabled!')
diff --git a/deluge/plugins/Stats/setup.py b/deluge/plugins/Stats/setup.py
new file mode 100644
index 0000000..0f3e069
--- /dev/null
+++ b/deluge/plugins/Stats/setup.py
@@ -0,0 +1,49 @@
+#
+# Copyright (C) 2009 Ian Martin <ianmartin@cantab.net>
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-2008 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 setuptools import find_packages, setup
+
+__plugin_name__ = 'Stats'
+__author__ = 'Ian Martin'
+__author_email__ = 'ianmartin@cantab.net'
+__version__ = '0.4'
+__url__ = 'http://deluge-torrent.org'
+__license__ = 'GPLv3'
+__description__ = 'Display stats graphs'
+__long_description__ = """
+Records lots of extra stats
+and produces time series
+graphs"""
+__pkg_data__ = {'deluge_' + __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__,
+ 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),
+)
diff --git a/deluge/plugins/Toggle/deluge_toggle/__init__.py b/deluge/plugins/Toggle/deluge_toggle/__init__.py
new file mode 100644
index 0000000..b0332ee
--- /dev/null
+++ b/deluge/plugins/Toggle/deluge_toggle/__init__.py
@@ -0,0 +1,38 @@
+#
+# Copyright (C) 2010 John Garland <johnnybg+deluge@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
+# 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/Toggle/deluge_toggle/common.py b/deluge/plugins/Toggle/deluge_toggle/common.py
new file mode 100644
index 0000000..eb47f13
--- /dev/null
+++ b/deluge/plugins/Toggle/deluge_toggle/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/Toggle/deluge_toggle/core.py b/deluge/plugins/Toggle/deluge_toggle/core.py
new file mode 100644
index 0000000..ab4581b
--- /dev/null
+++ b/deluge/plugins/Toggle/deluge_toggle/core.py
@@ -0,0 +1,47 @@
+#
+# Copyright (C) 2010 John Garland <johnnybg+deluge@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
+# 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 deluge.component as component
+from deluge.core.rpcserver import export
+from deluge.plugins.pluginbase import CorePluginBase
+
+log = logging.getLogger(__name__)
+
+DEFAULT_PREFS = {}
+
+
+class Core(CorePluginBase):
+ def enable(self):
+ self.core = component.get('Core')
+
+ def disable(self):
+ pass
+
+ def update(self):
+ pass
+
+ @export
+ def get_status(self):
+ return self.core.session.is_paused()
+
+ @export
+ def toggle(self):
+ if self.core.session.is_paused():
+ self.core.resume_session()
+ paused = False
+ else:
+ self.core.pause_session()
+ paused = True
+ return paused
diff --git a/deluge/plugins/Toggle/deluge_toggle/data/toggle.js b/deluge/plugins/Toggle/deluge_toggle/data/toggle.js
new file mode 100644
index 0000000..20fa4f4
--- /dev/null
+++ b/deluge/plugins/Toggle/deluge_toggle/data/toggle.js
@@ -0,0 +1,27 @@
+/**
+ * Script: toggle.js
+ * The client-side javascript code for the Toggle plugin.
+ *
+ * Copyright (C) John Garland 2010 <johnnybg+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.
+ */
+
+TogglePlugin = Ext.extend(Deluge.Plugin, {
+ constructor: function (config) {
+ config = Ext.apply(
+ {
+ name: 'Toggle',
+ },
+ config
+ );
+ TogglePlugin.superclass.constructor.call(this, config);
+ },
+
+ onDisable: function () {},
+
+ onEnable: function () {},
+});
+new TogglePlugin();
diff --git a/deluge/plugins/Toggle/deluge_toggle/gtkui.py b/deluge/plugins/Toggle/deluge_toggle/gtkui.py
new file mode 100644
index 0000000..bfb90de
--- /dev/null
+++ b/deluge/plugins/Toggle/deluge_toggle/gtkui.py
@@ -0,0 +1,53 @@
+#
+# Copyright (C) 2010 John Garland <johnnybg+deluge@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
+# 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 deluge.component as component
+from deluge.plugins.pluginbase import Gtk3PluginBase
+from deluge.ui.client import client
+
+log = logging.getLogger(__name__)
+
+
+class GtkUI(Gtk3PluginBase):
+ def enable(self):
+ self.core = client.toggle
+ self.plugin = component.get('PluginManager')
+ self.separator = self.plugin.add_toolbar_separator()
+ self.button = self.plugin.add_toolbar_button(
+ self._on_button_clicked,
+ label='Pause Session',
+ stock='gtk-media-pause',
+ tooltip='Pause the session',
+ )
+
+ def disable(self):
+ component.get('PluginManager').remove_toolbar_button(self.button)
+ component.get('PluginManager').remove_toolbar_button(self.separator)
+
+ def update(self):
+ def _on_get_status(paused):
+ if paused:
+ self.button.set_label('Resume Session')
+ self.button.set_tooltip_text('Resume the session')
+ self.button.set_stock_id('gtk-media-play')
+ else:
+ self.button.set_label('Pause Session')
+ self.button.set_tooltip_text('Pause the session')
+ self.button.set_stock_id('gtk-media-pause')
+
+ self.core.get_status().addCallback(_on_get_status)
+
+ def _on_button_clicked(self, widget):
+ self.core.toggle()
diff --git a/deluge/plugins/Toggle/deluge_toggle/webui.py b/deluge/plugins/Toggle/deluge_toggle/webui.py
new file mode 100644
index 0000000..637365c
--- /dev/null
+++ b/deluge/plugins/Toggle/deluge_toggle/webui.py
@@ -0,0 +1,30 @@
+#
+# Copyright (C) 2010 John Garland <johnnybg+deluge@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
+# 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('toggle.js')]
+
+ def enable(self):
+ pass
+
+ def disable(self):
+ pass
diff --git a/deluge/plugins/Toggle/setup.py b/deluge/plugins/Toggle/setup.py
new file mode 100644
index 0000000..dadd32e
--- /dev/null
+++ b/deluge/plugins/Toggle/setup.py
@@ -0,0 +1,46 @@
+#
+# Copyright (C) 2010 John Garland <johnnybg+deluge@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
+# 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__ = 'Toggle'
+__author__ = 'John Garland'
+__author_email__ = 'johnnybg+deluge@gmail.com'
+__version__ = '0.4'
+__url__ = 'http://deluge-torrent.org'
+__license__ = 'GPLv3'
+__description__ = 'Toggles the session'
+__long_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__ if __long_description__ else __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),
+)
diff --git a/deluge/plugins/WebUi/create_dev_link.sh b/deluge/plugins/WebUi/create_dev_link.sh
new file mode 100755
index 0000000..f4d60d2
--- /dev/null
+++ b/deluge/plugins/WebUi/create_dev_link.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+BASEDIR=$(cd `dirname $0` && pwd)
+CONFIG_DIR=$( test -z $1 && echo "/home/damien/.config/deluge/" || echo "$1")
+[ -d "$CONFIG_DIR/plugins" ] || echo "Config dir "$CONFIG_DIR" is either not a directory or is not a proper deluge config directory. Exiting"
+[ -d "$CONFIG_DIR/plugins" ] || exit 1
+cd $BASEDIR
+test -d $BASEDIR/temp || mkdir $BASEDIR/temp
+export PYTHONPATH=$BASEDIR/temp
+python setup.py build develop --install-dir $BASEDIR/temp
+cp $BASEDIR/temp/*.egg-link $CONFIG_DIR/plugins
+rm -fr $BASEDIR/temp
diff --git a/deluge/plugins/WebUi/deluge_webui/__init__.py b/deluge/plugins/WebUi/deluge_webui/__init__.py
new file mode 100644
index 0000000..ba978b2
--- /dev/null
+++ b/deluge/plugins/WebUi/deluge_webui/__init__.py
@@ -0,0 +1,37 @@
+#
+# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-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 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/WebUi/deluge_webui/common.py b/deluge/plugins/WebUi/deluge_webui/common.py
new file mode 100644
index 0000000..eb47f13
--- /dev/null
+++ b/deluge/plugins/WebUi/deluge_webui/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/WebUi/deluge_webui/core.py b/deluge/plugins/WebUi/deluge_webui/core.py
new file mode 100644
index 0000000..f18203e
--- /dev/null
+++ b/deluge/plugins/WebUi/deluge_webui/core.py
@@ -0,0 +1,117 @@
+#
+# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-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 logging
+
+from twisted.internet import defer
+from twisted.internet.error import CannotListenError
+
+import deluge.component as component
+from deluge import configmanager
+from deluge.core.rpcserver import export
+from deluge.plugins.pluginbase import CorePluginBase
+
+try:
+ from deluge.ui.web import server
+except ImportError:
+ server = False
+
+log = logging.getLogger(__name__)
+
+DEFAULT_PREFS = {'enabled': False, 'ssl': False, 'port': 8112}
+
+
+class Core(CorePluginBase):
+ server = None
+
+ def enable(self):
+ self.config = configmanager.ConfigManager('web_plugin.conf', DEFAULT_PREFS)
+ if self.config['enabled']:
+ self.start_server()
+
+ def disable(self):
+ self.stop_server()
+
+ def update(self):
+ pass
+
+ def _on_stop(self, *args):
+ return self.start_server()
+
+ @export
+ def got_deluge_web(self):
+ """Status of deluge-web module installation.
+
+ Check if deluge.ui.web.server modulge is installed and has been successfully imported.
+
+ Returns:
+ bool: True is deluge-web is installed and available, otherwise False.
+
+ """
+
+ return bool(server)
+
+ def start_server(self):
+ if not self.server:
+ if not self.got_deluge_web():
+ return False
+
+ try:
+ self.server = component.get('DelugeWeb')
+ except KeyError:
+ self.server = server.DelugeWeb(daemon=False)
+
+ self.server.port = self.config['port']
+ self.server.https = self.config['ssl']
+ try:
+ self.server.start()
+ except CannotListenError as ex:
+ log.warning('Failed to start WebUI server: %s', ex)
+ raise
+ return True
+
+ def stop_server(self):
+ if self.server:
+ return self.server.stop()
+ return defer.succeed(True)
+
+ def restart_server(self):
+ return self.stop_server().addCallback(self._on_stop)
+
+ @export
+ def set_config(self, config):
+ """Sets the config dictionary."""
+
+ action = None
+ if 'enabled' in config:
+ if config['enabled'] != self.config['enabled']:
+ action = config['enabled'] and 'start' or 'stop'
+
+ if 'ssl' in config:
+ if not action:
+ action = 'restart'
+
+ for key in config:
+ self.config[key] = config[key]
+ self.config.save()
+
+ if action == 'start':
+ return self.start_server()
+ elif action == 'stop':
+ return self.stop_server()
+ elif action == 'restart':
+ return self.restart_server()
+
+ @export
+ def get_config(self):
+ """Returns the config dictionary."""
+ return self.config.config
diff --git a/deluge/plugins/WebUi/deluge_webui/data/config.ui b/deluge/plugins/WebUi/deluge_webui/data/config.ui
new file mode 100644
index 0000000..c58edd0
--- /dev/null
+++ b/deluge/plugins/WebUi/deluge_webui/data/config.ui
@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <object class="GtkAdjustment" id="adjustment1">
+ <property name="upper">99999</property>
+ <property name="value">8112</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">10</property>
+ </object>
+ <object class="GtkWindow" id="window1">
+ <property name="can_focus">False</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkBox" id="prefs_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkFrame" id="settings_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="settings_alignment">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="top_padding">10</property>
+ <property name="left_padding">12</property>
+ <child>
+ <object class="GtkBox" id="settings_vbox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">5</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkCheckButton" id="enabled_checkbutton">
+ <property name="label" translatable="yes">Enable web interface</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="ssl_checkbutton">
+ <property name="label" translatable="yes">Enable SSL</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="port_hbox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">5</property>
+ <child>
+ <object class="GtkLabel" id="port_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Listening port:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="port_spinbutton">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">●</property>
+ <property name="adjustment">adjustment1</property>
+ <property name="numeric">True</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="settings_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;b&gt;Settings&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/deluge/plugins/WebUi/deluge_webui/gtkui.py b/deluge/plugins/WebUi/deluge_webui/gtkui.py
new file mode 100644
index 0000000..3d19417
--- /dev/null
+++ b/deluge/plugins/WebUi/deluge_webui/gtkui.py
@@ -0,0 +1,97 @@
+#
+# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-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 logging
+
+from gi.repository import Gtk
+
+import deluge.component as component
+from deluge.plugins.pluginbase import Gtk3PluginBase
+from deluge.ui.client import client
+
+from .common import get_resource
+
+log = logging.getLogger(__name__)
+
+
+class GtkUI(Gtk3PluginBase):
+ def enable(self):
+ self.builder = Gtk.Builder()
+ self.builder.add_from_file(get_resource('config.ui'))
+
+ component.get('Preferences').add_page(
+ _('WebUi'), self.builder.get_object('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
+ )
+ client.webui.get_config().addCallback(self.cb_get_config)
+ client.webui.got_deluge_web().addCallback(self.cb_chk_deluge_web)
+
+ def disable(self):
+ component.get('Preferences').remove_page(_('WebUi'))
+ 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):
+ if not self.have_web:
+ return
+ log.debug('applying prefs for WebUi')
+ config = {
+ 'enabled': self.builder.get_object('enabled_checkbutton').get_active(),
+ 'ssl': self.builder.get_object('ssl_checkbutton').get_active(),
+ 'port': self.builder.get_object('port_spinbutton').get_value_as_int(),
+ }
+ client.webui.set_config(config)
+
+ def on_show_prefs(self):
+ client.webui.get_config().addCallback(self.cb_get_config)
+
+ def cb_get_config(self, config):
+ """Callback for on show_prefs."""
+ self.builder.get_object('enabled_checkbutton').set_active(config['enabled'])
+ self.builder.get_object('ssl_checkbutton').set_active(config['ssl'])
+ self.builder.get_object('port_spinbutton').set_value(config['port'])
+
+ def cb_chk_deluge_web(self, have_web):
+ self.have_web = have_web
+ if have_web:
+ return
+ self.builder.get_object('settings_vbox').set_sensitive(False)
+
+ vbox = self.builder.get_object('prefs_box')
+
+ hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, spacing=0)
+ icon = Gtk.Image.new_from_icon_name('dialog-error', Gtk.IconSize.BUTTON)
+ icon.set_padding(5, 5)
+ hbox.pack_start(icon, False, False, 0)
+
+ label = Gtk.Label(
+ _(
+ 'The Deluge web interface is not installed, '
+ 'please install the\ninterface and try again'
+ )
+ )
+ label.set_alignment(0, 0.5)
+ label.set_padding(5, 5)
+ hbox.pack_start(label, False, False, 0)
+
+ vbox.pack_start(hbox, False, False, 10)
+ vbox.reorder_child(hbox, 0)
+ vbox.show_all()
diff --git a/deluge/plugins/WebUi/deluge_webui/tests/__init__.py b/deluge/plugins/WebUi/deluge_webui/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/deluge/plugins/WebUi/deluge_webui/tests/__init__.py
diff --git a/deluge/plugins/WebUi/deluge_webui/tests/test_plugin_webui.py b/deluge/plugins/WebUi/deluge_webui/tests/test_plugin_webui.py
new file mode 100644
index 0000000..413d259
--- /dev/null
+++ b/deluge/plugins/WebUi/deluge_webui/tests/test_plugin_webui.py
@@ -0,0 +1,44 @@
+#
+# Copyright (C) 2016 bendikro <bro.devel+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 pytest
+import pytest_twisted
+
+from deluge.core.core import Core
+from deluge.core.rpcserver import RPCServer
+from deluge.tests import common
+
+common.disable_new_release_check()
+
+
+class TestWebUIPlugin:
+ @pytest_twisted.async_yield_fixture(autouse=True)
+ async def set_up(self, request, component):
+ self = request.instance
+ self.rpcserver = RPCServer(listen=False)
+ self.core = Core()
+ await component.start()
+
+ yield
+
+ await component.shutdown()
+ del self.rpcserver
+ del self.core
+
+ def test_enable_webui(self):
+ if 'WebUi' not in self.core.get_available_plugins():
+ pytest.skip('WebUi plugin not available for testing')
+
+ d = self.core.enable_plugin('WebUi')
+
+ def result_cb(result):
+ if 'WebUi' not in self.core.get_enabled_plugins():
+ self.fail('Failed to enable WebUi plugin')
+ assert result
+
+ d.addBoth(result_cb)
+ return d
diff --git a/deluge/plugins/WebUi/setup.py b/deluge/plugins/WebUi/setup.py
new file mode 100644
index 0000000..5f2184c
--- /dev/null
+++ b/deluge/plugins/WebUi/setup.py
@@ -0,0 +1,43 @@
+#
+# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-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 setuptools import find_packages, setup
+
+__plugin_name__ = 'WebUi'
+__author__ = 'Damien Churchill'
+__author_email__ = 'damoxc@gmail.com'
+__version__ = '0.2'
+__url__ = 'http://deluge-torrent.org'
+__license__ = 'GPLv3'
+__description__ = 'Allows starting the web interface within the daemon.'
+__long_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__ if __long_description__ else __description__,
+ packages=find_packages(),
+ package_data=__pkg_data__,
+ entry_points="""
+ [deluge.plugin.core]
+ %s = deluge_%s:CorePlugin
+ [deluge.plugin.gtk3ui]
+ %s = deluge_%s:GtkUIPlugin
+ """
+ % ((__plugin_name__, __plugin_name__.lower()) * 2),
+)
diff --git a/deluge/plugins/__init__.py b/deluge/plugins/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/deluge/plugins/__init__.py
diff --git a/deluge/plugins/init.py b/deluge/plugins/init.py
new file mode 100644
index 0000000..56b3197
--- /dev/null
+++ b/deluge/plugins/init.py
@@ -0,0 +1,27 @@
+#
+# Copyright (C) 2007 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.
+#
+
+"""
+This base class is used in plugin's __init__ for the plugin entry points.
+"""
+import logging
+
+log = logging.getLogger(__name__)
+
+
+class PluginInitBase:
+ _plugin_cls = None
+
+ def __init__(self, plugin_name):
+ self.plugin = self._plugin_cls(plugin_name) # pylint: disable=not-callable
+
+ def enable(self):
+ return self.plugin.enable()
+
+ def disable(self):
+ return self.plugin.disable()
diff --git a/deluge/plugins/pluginbase.py b/deluge/plugins/pluginbase.py
new file mode 100644
index 0000000..8d55156
--- /dev/null
+++ b/deluge/plugins/pluginbase.py
@@ -0,0 +1,82 @@
+#
+# Copyright (C) 2007-2010 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 logging
+
+import deluge.component as component
+
+log = logging.getLogger(__name__)
+
+
+class PluginBase(component.Component):
+ update_interval = 1
+
+ def __init__(self, name):
+ super().__init__(name, self.update_interval)
+
+ def enable(self):
+ raise NotImplementedError('Need to define an enable method!')
+
+ def disable(self):
+ raise NotImplementedError('Need to define a disable method!')
+
+
+class CorePluginBase(PluginBase):
+ def __init__(self, plugin_name):
+ super().__init__('CorePlugin.' + plugin_name)
+ # Register RPC methods
+ component.get('RPCServer').register_object(self, plugin_name.lower())
+ log.debug('CorePlugin initialized..')
+
+ def __del__(self):
+ try:
+ component.get('RPCServer').deregister_object(self)
+ except KeyError:
+ log.debug('RPCServer already deregistered')
+
+ def enable(self):
+ super().enable()
+
+ def disable(self):
+ super().disable()
+
+
+class Gtk3PluginBase(PluginBase):
+ def __init__(self, plugin_name):
+ super().__init__('Gtk3Plugin.' + plugin_name)
+ log.debug('Gtk3Plugin initialized..')
+
+ def enable(self):
+ super().enable()
+
+ def disable(self):
+ super().disable()
+
+
+class WebPluginBase(PluginBase):
+ scripts = []
+ debug_scripts = []
+
+ stylesheets = []
+ debug_stylesheets = []
+
+ def __init__(self, plugin_name):
+ super().__init__('WebPlugin.' + plugin_name)
+
+ # Register JSON rpc methods
+ component.get('JSON').register_object(self, plugin_name.lower())
+ log.debug('WebPlugin initialized..')
+
+ def __del__(self):
+ component.get('JSON').deregister_object(self)
+
+ def enable(self):
+ pass
+
+ def disable(self):
+ pass