diff options
Diffstat (limited to 'deluge/plugins')
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"><b>Watch Folder</b></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"><b>Torrent File Action</b></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"><b>Download Folder</b></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"><b>Move Completed</b></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"><b>Label</b></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"><b>Owner</b></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"><b>Bandwidth</b></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"><b>Queue</b></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"><b>Watch Folders:</b></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 Binary files differnew file mode 100644 index 0000000..15b4299 --- /dev/null +++ b/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist16.png diff --git a/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_download24.png b/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_download24.png Binary files differnew file mode 100644 index 0000000..6de3a0d --- /dev/null +++ b/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_download24.png diff --git a/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_import24.png b/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_import24.png Binary files differnew file mode 100644 index 0000000..f1a02e7 --- /dev/null +++ b/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_import24.png 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"><b>General</b></property> + <property name="use_markup">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="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"><b>Settings</b></property> + <property name="use_markup">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <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"><b>Options</b></property> + <property name="use_markup">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">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"><b>Info</b></property> + <property name="use_markup">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">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"><b>Whitelist</b></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"><b>Add Command</b></property> + <property name="use_markup">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="commands_frame"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="commands_alignment"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="left_padding">12</property> + <child> + <object class="GtkBox" id="commands_vbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="halign">start</property> + <property name="orientation">vertical</property> + <child> + <placeholder/> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="commands_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes"><b>Commands</b></property> + <property name="use_markup">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> +</interface> diff --git a/deluge/plugins/Execute/deluge_execute/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"><b>General</b></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"><b>Add Label</b></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"><b>Label Options</b></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"><i>(1 line per tracker)</i></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"><i>Use the sidebar to add,edit and remove labels. </i> +</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"><b>Labels</b></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"><b>UI Notifications</b></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"><b>Recipients</b></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"><b>Email Notifications</b></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;"> </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"><b>Connections Graph</b></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"><b>Bandwidth Graph</b></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"><b>Seeds / Peers</b></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"><b>Graph Colors</b></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"><b>Settings</b></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 |