diff options
Diffstat (limited to 'deluge/plugins/AutoAdd')
-rw-r--r-- | deluge/plugins/AutoAdd/deluge_autoadd/__init__.py | 38 | ||||
-rw-r--r-- | deluge/plugins/AutoAdd/deluge_autoadd/common.py | 21 | ||||
-rw-r--r-- | deluge/plugins/AutoAdd/deluge_autoadd/core.py | 528 | ||||
-rw-r--r-- | deluge/plugins/AutoAdd/deluge_autoadd/data/autoadd.js | 226 | ||||
-rw-r--r-- | deluge/plugins/AutoAdd/deluge_autoadd/data/autoadd_options.js | 470 | ||||
-rw-r--r-- | deluge/plugins/AutoAdd/deluge_autoadd/data/autoadd_options.ui | 1322 | ||||
-rw-r--r-- | deluge/plugins/AutoAdd/deluge_autoadd/data/autoadd_options/main_tab.js | 304 | ||||
-rw-r--r-- | deluge/plugins/AutoAdd/deluge_autoadd/data/autoadd_options/options_tab.js | 302 | ||||
-rw-r--r-- | deluge/plugins/AutoAdd/deluge_autoadd/data/config.ui | 134 | ||||
-rw-r--r-- | deluge/plugins/AutoAdd/deluge_autoadd/gtkui.py | 576 | ||||
-rw-r--r-- | deluge/plugins/AutoAdd/deluge_autoadd/webui.py | 35 | ||||
-rw-r--r-- | deluge/plugins/AutoAdd/setup.py | 47 |
12 files changed, 4003 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), +) |