├── .gitignore ├── create_dev_link.sh ├── autoadd ├── data │ ├── config.gladep │ ├── autoadd_options.gladep │ ├── autoadd.js │ ├── config.glade │ └── autoadd_options.glade ├── common.py ├── webui.py ├── __init__.py ├── core.py └── gtkui.py ├── README.md └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | build/* 2 | dist/* 3 | AutoAdd.egg-info/* 4 | *.pyc 5 | *.pyo 6 | *.bak 7 | *~ 8 | -------------------------------------------------------------------------------- /create_dev_link.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mkdir temp 3 | export PYTHONPATH=./temp 4 | python setup.py build develop --install-dir ./temp 5 | cp ./temp/AutoAdd.egg-link ~/.config/deluge/plugins 6 | rm -fr ./temp 7 | -------------------------------------------------------------------------------- /autoadd/data/config.gladep: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | FALSE 8 | 9 | -------------------------------------------------------------------------------- /autoadd/data/autoadd_options.gladep: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | FALSE 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # deluge-autoadd-plugin 2 | Adds multiple watch folder support to Deluge. 3 | 4 | This plugin has been merged into deluge, find the latest source at the [deluge repo](https://github.com/deluge-torrent/deluge/tree/develop/deluge/plugins/AutoAdd). 5 | 6 | All bugs should be reported on the [official deluge bug tracker](https://dev.deluge-torrent.org/report/1). 7 | -------------------------------------------------------------------------------- /autoadd/common.py: -------------------------------------------------------------------------------- 1 | # 2 | # common.py 3 | # 4 | # Copyright (C) 2009 GazpachoKing 5 | # 6 | # Basic plugin template created by: 7 | # Copyright (C) 2008 Martijn Voncken 8 | # Copyright (C) 2007-2009 Andrew Resch 9 | # Copyright (C) 2009 Damien Churchill 10 | # 11 | # Deluge is free software. 12 | # 13 | # You may redistribute it and/or modify it under the terms of the 14 | # GNU General Public License, as published by the Free Software 15 | # Foundation; either version 3 of the License, or (at your option) 16 | # any later version. 17 | # 18 | # deluge is distributed in the hope that it will be useful, 19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 21 | # See the GNU General Public License for more details. 22 | # 23 | # You should have received a copy of the GNU General Public License 24 | # along with deluge. If not, write to: 25 | # The Free Software Foundation, Inc., 26 | # 51 Franklin Street, Fifth Floor 27 | # Boston, MA 02110-1301, USA. 28 | # 29 | # In addition, as a special exception, the copyright holders give 30 | # permission to link the code of portions of this program with the OpenSSL 31 | # library. 32 | # You must obey the GNU General Public License in all respects for all of 33 | # the code used other than OpenSSL. If you modify file(s) with this 34 | # exception, you may extend this exception to your version of the file(s), 35 | # but you are not obligated to do so. If you do not wish to do so, delete 36 | # this exception statement from your version. If you delete this exception 37 | # statement from all source files in the program, then also delete it here. 38 | # 39 | 40 | def get_resource(filename): 41 | import pkg_resources, os 42 | return pkg_resources.resource_filename("autoadd", os.path.join("data", filename)) 43 | -------------------------------------------------------------------------------- /autoadd/data/autoadd.js: -------------------------------------------------------------------------------- 1 | /* 2 | Script: autoadd.js 3 | The client-side javascript code for the AutoAdd plugin. 4 | 5 | Copyright: 6 | (C) GazpachoKing 2009 7 | This program is free software; you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation; either version 3, or (at your option) 10 | any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, write to: 19 | The Free Software Foundation, Inc., 20 | 51 Franklin Street, Fifth Floor 21 | Boston, MA 02110-1301, USA. 22 | 23 | In addition, as a special exception, the copyright holders give 24 | permission to link the code of portions of this program with the OpenSSL 25 | library. 26 | You must obey the GNU General Public License in all respects for all of 27 | the code used other than OpenSSL. If you modify file(s) with this 28 | exception, you may extend this exception to your version of the file(s), 29 | but you are not obligated to do so. If you do not wish to do so, delete 30 | this exception statement from your version. If you delete this exception 31 | statement from all source files in the program, then also delete it here. 32 | */ 33 | 34 | AutoAddPlugin = Ext.extend(Deluge.Plugin, { 35 | constructor: function(config) { 36 | config = Ext.apply({ 37 | name: "AutoAdd" 38 | }, config); 39 | AutoAddPlugin.superclass.constructor.call(this, config); 40 | }, 41 | 42 | onDisable: function() { 43 | 44 | }, 45 | 46 | onEnable: function() { 47 | 48 | } 49 | }); 50 | new AutoAddPlugin(); 51 | -------------------------------------------------------------------------------- /autoadd/webui.py: -------------------------------------------------------------------------------- 1 | # 2 | # webui.py 3 | # 4 | # Copyright (C) 2009 GazpachoKing 5 | # 6 | # Basic plugin template created by: 7 | # Copyright (C) 2008 Martijn Voncken 8 | # Copyright (C) 2007-2009 Andrew Resch 9 | # Copyright (C) 2009 Damien Churchill 10 | # 11 | # Deluge is free software. 12 | # 13 | # You may redistribute it and/or modify it under the terms of the 14 | # GNU General Public License, as published by the Free Software 15 | # Foundation; either version 3 of the License, or (at your option) 16 | # any later version. 17 | # 18 | # deluge is distributed in the hope that it will be useful, 19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 21 | # See the GNU General Public License for more details. 22 | # 23 | # You should have received a copy of the GNU General Public License 24 | # along with deluge. If not, write to: 25 | # The Free Software Foundation, Inc., 26 | # 51 Franklin Street, Fifth Floor 27 | # Boston, MA 02110-1301, USA. 28 | # 29 | # In addition, as a special exception, the copyright holders give 30 | # permission to link the code of portions of this program with the OpenSSL 31 | # library. 32 | # You must obey the GNU General Public License in all respects for all of 33 | # the code used other than OpenSSL. If you modify file(s) with this 34 | # exception, you may extend this exception to your version of the file(s), 35 | # but you are not obligated to do so. If you do not wish to do so, delete 36 | # this exception statement from your version. If you delete this exception 37 | # statement from all source files in the program, then also delete it here. 38 | # 39 | 40 | from deluge.log import LOG as log 41 | from deluge.ui.client import client 42 | from deluge import component 43 | from deluge.plugins.pluginbase import WebPluginBase 44 | 45 | from common import get_resource 46 | 47 | class WebUI(WebPluginBase): 48 | 49 | scripts = [get_resource("autoadd.js")] 50 | 51 | def enable(self): 52 | pass 53 | 54 | def disable(self): 55 | pass 56 | -------------------------------------------------------------------------------- /autoadd/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # __init__.py 3 | # 4 | # Copyright (C) 2009 GazpachoKing 5 | # 6 | # Basic plugin template created by: 7 | # Copyright (C) 2008 Martijn Voncken 8 | # Copyright (C) 2007-2009 Andrew Resch 9 | # Copyright (C) 2009 Damien Churchill 10 | # 11 | # Deluge is free software. 12 | # 13 | # You may redistribute it and/or modify it under the terms of the 14 | # GNU General Public License, as published by the Free Software 15 | # Foundation; either version 3 of the License, or (at your option) 16 | # any later version. 17 | # 18 | # deluge is distributed in the hope that it will be useful, 19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 21 | # See the GNU General Public License for more details. 22 | # 23 | # You should have received a copy of the GNU General Public License 24 | # along with deluge. If not, write to: 25 | # The Free Software Foundation, Inc., 26 | # 51 Franklin Street, Fifth Floor 27 | # Boston, MA 02110-1301, USA. 28 | # 29 | # In addition, as a special exception, the copyright holders give 30 | # permission to link the code of portions of this program with the OpenSSL 31 | # library. 32 | # You must obey the GNU General Public License in all respects for all of 33 | # the code used other than OpenSSL. If you modify file(s) with this 34 | # exception, you may extend this exception to your version of the file(s), 35 | # but you are not obligated to do so. If you do not wish to do so, delete 36 | # this exception statement from your version. If you delete this exception 37 | # statement from all source files in the program, then also delete it here. 38 | # 39 | 40 | from deluge.plugins.init import PluginInitBase 41 | 42 | class CorePlugin(PluginInitBase): 43 | def __init__(self, plugin_name): 44 | from core import Core as _plugin_cls 45 | self._plugin_cls = _plugin_cls 46 | super(CorePlugin, self).__init__(plugin_name) 47 | 48 | class GtkUIPlugin(PluginInitBase): 49 | def __init__(self, plugin_name): 50 | from gtkui import GtkUI as _plugin_cls 51 | self._plugin_cls = _plugin_cls 52 | super(GtkUIPlugin, self).__init__(plugin_name) 53 | 54 | class WebUIPlugin(PluginInitBase): 55 | def __init__(self, plugin_name): 56 | from webui import WebUI as _plugin_cls 57 | self._plugin_cls = _plugin_cls 58 | super(WebUIPlugin, self).__init__(plugin_name) 59 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # 2 | # setup.py 3 | # 4 | # Copyright (C) 2009 GazpachoKing 5 | # 6 | # Basic plugin template created by: 7 | # Copyright (C) 2008 Martijn Voncken 8 | # Copyright (C) 2007-2009 Andrew Resch 9 | # Copyright (C) 2009 Damien Churchill 10 | # 11 | # Deluge is free software. 12 | # 13 | # You may redistribute it and/or modify it under the terms of the 14 | # GNU General Public License, as published by the Free Software 15 | # Foundation; either version 3 of the License, or (at your option) 16 | # any later version. 17 | # 18 | # deluge is distributed in the hope that it will be useful, 19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 21 | # See the GNU General Public License for more details. 22 | # 23 | # You should have received a copy of the GNU General Public License 24 | # along with deluge. If not, write to: 25 | # The Free Software Foundation, Inc., 26 | # 51 Franklin Street, Fifth Floor 27 | # Boston, MA 02110-1301, USA. 28 | # 29 | # In addition, as a special exception, the copyright holders give 30 | # permission to link the code of portions of this program with the OpenSSL 31 | # library. 32 | # You must obey the GNU General Public License in all respects for all of 33 | # the code used other than OpenSSL. If you modify file(s) with this 34 | # exception, you may extend this exception to your version of the file(s), 35 | # but you are not obligated to do so. If you do not wish to do so, delete 36 | # this exception statement from your version. If you delete this exception 37 | # statement from all source files in the program, then also delete it here. 38 | # 39 | 40 | from setuptools import setup 41 | 42 | __plugin_name__ = "AutoAdd" 43 | __author__ = "Chase Sterling" 44 | __author_email__ = "chase.sterling@gmail.com" 45 | __version__ = "1.02" 46 | __url__ = "http://forum.deluge-torrent.org/viewtopic.php?f=9&t=26775" 47 | __license__ = "GPLv3" 48 | __description__ = "Monitors folders for .torrent files." 49 | __long_description__ = """""" 50 | __pkg_data__ = {__plugin_name__.lower(): ["template/*", "data/*"]} 51 | 52 | setup( 53 | name=__plugin_name__, 54 | version=__version__, 55 | description=__description__, 56 | author=__author__, 57 | author_email=__author_email__, 58 | url=__url__, 59 | license=__license__, 60 | long_description=__long_description__ if __long_description__ else __description__, 61 | 62 | packages=[__plugin_name__.lower()], 63 | package_data = __pkg_data__, 64 | 65 | entry_points=""" 66 | [deluge.plugin.core] 67 | %s = %s:CorePlugin 68 | [deluge.plugin.gtkui] 69 | %s = %s:GtkUIPlugin 70 | [deluge.plugin.webui] 71 | %s = %s:WebUIPlugin 72 | """ % ((__plugin_name__, __plugin_name__.lower())*3) 73 | ) 74 | -------------------------------------------------------------------------------- /autoadd/data/config.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | True 9 | 10 | 11 | 12 | 13 | 14 | True 15 | 16 | 17 | 340 18 | 390 19 | True 20 | 3 21 | vertical 22 | 23 | 24 | 25 | True 26 | 0 27 | none 28 | 29 | 30 | True 31 | vertical 32 | True 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | True 41 | <b>Watch Folders:</b> 42 | True 43 | 44 | 45 | label_item 46 | 47 | 48 | 49 | 50 | 0 51 | 52 | 53 | 54 | 55 | True 56 | 57 | 58 | gtk-add 59 | True 60 | True 61 | True 62 | True 63 | True 64 | 65 | 66 | 67 | 0 68 | 69 | 70 | 71 | 72 | gtk-remove 73 | True 74 | False 75 | True 76 | True 77 | True 78 | True 79 | 80 | 81 | 82 | 1 83 | 84 | 85 | 86 | 87 | gtk-edit 88 | True 89 | False 90 | True 91 | True 92 | True 93 | True 94 | 95 | 96 | 97 | 2 98 | 99 | 100 | 101 | 102 | False 103 | 1 104 | 105 | 106 | 107 | 108 | 109 | 110 | 1 111 | 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /autoadd/core.py: -------------------------------------------------------------------------------- 1 | # 2 | # core.py 3 | # 4 | # Copyright (C) 2009 GazpachoKing 5 | # 6 | # Basic plugin template created by: 7 | # Copyright (C) 2008 Martijn Voncken 8 | # Copyright (C) 2007-2009 Andrew Resch 9 | # Copyright (C) 2009 Damien Churchill 10 | # 11 | # Deluge is free software. 12 | # 13 | # You may redistribute it and/or modify it under the terms of the 14 | # GNU General Public License, as published by the Free Software 15 | # Foundation; either version 3 of the License, or (at your option) 16 | # any later version. 17 | # 18 | # deluge is distributed in the hope that it will be useful, 19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 21 | # See the GNU General Public License for more details. 22 | # 23 | # You should have received a copy of the GNU General Public License 24 | # along with deluge. If not, write to: 25 | # The Free Software Foundation, Inc., 26 | # 51 Franklin Street, Fifth Floor 27 | # Boston, MA 02110-1301, USA. 28 | # 29 | # In addition, as a special exception, the copyright holders give 30 | # permission to link the code of portions of this program with the OpenSSL 31 | # library. 32 | # You must obey the GNU General Public License in all respects for all of 33 | # the code used other than OpenSSL. If you modify file(s) with this 34 | # exception, you may extend this exception to your version of the file(s), 35 | # but you are not obligated to do so. If you do not wish to do so, delete 36 | # this exception statement from your version. If you delete this exception 37 | # statement from all source files in the program, then also delete it here. 38 | # 39 | 40 | from deluge._libtorrent import lt 41 | import os 42 | from deluge.log import LOG as log 43 | from deluge.plugins.pluginbase import CorePluginBase 44 | import deluge.component as component 45 | import deluge.configmanager 46 | from deluge.core.rpcserver import export 47 | from twisted.internet.task import LoopingCall, deferLater 48 | from twisted.internet import reactor 49 | from deluge.event import DelugeEvent 50 | 51 | DEFAULT_PREFS = { 52 | "watchdirs":{}, 53 | "next_id":1 54 | } 55 | 56 | OPTIONS_AVAILABLE = { #option: builtin 57 | "enabled":False, 58 | "path":False, 59 | "append_extension":False, 60 | "abspath":False, 61 | "download_location":True, 62 | "max_download_speed":True, 63 | "max_upload_speed":True, 64 | "max_connections":True, 65 | "max_upload_slots":True, 66 | "prioritize_first_last":True, 67 | "auto_managed":True, 68 | "stop_at_ratio":True, 69 | "stop_ratio":True, 70 | "remove_at_ratio":True, 71 | "move_completed":True, 72 | "move_completed_path":True, 73 | "label":False, 74 | "add_paused":True, 75 | "queue_to_top":False 76 | } 77 | 78 | MAX_NUM_ATTEMPTS = 10 79 | 80 | class AutoaddOptionsChangedEvent(DelugeEvent): 81 | """Emitted when the options for the plugin are changed.""" 82 | def __init__(self): 83 | pass 84 | 85 | def CheckInput(cond, message): 86 | if not cond: 87 | raise Exception(message) 88 | 89 | class Core(CorePluginBase): 90 | def enable(self): 91 | 92 | #reduce typing, assigning some values to self... 93 | self.config = deluge.configmanager.ConfigManager("autoadd.conf", DEFAULT_PREFS) 94 | self.watchdirs = self.config["watchdirs"] 95 | self.core_cfg = deluge.configmanager.ConfigManager("core.conf") 96 | 97 | # Dict of Filename:Attempts 98 | self.invalid_torrents = {} 99 | # Loopingcall timers for each enabled watchdir 100 | self.update_timers = {} 101 | # If core autoadd folder is enabled, move it to the plugin 102 | if self.core_cfg.config.get('autoadd_enable'): 103 | # Disable core autoadd 104 | self.core_cfg['autoadd_enable'] = False 105 | self.core_cfg.save() 106 | # Check if core autoadd folder is already added in plugin 107 | for watchdir in self.watchdirs: 108 | if os.path.abspath(self.core_cfg['autoadd_location']) == watchdir['abspath']: 109 | watchdir['enabled'] = True 110 | break 111 | else: 112 | # didn't find core watchdir, add it 113 | self.add({'path':self.core_cfg['autoadd_location'], 'enabled':True}) 114 | deferLater(reactor, 5, self.enable_looping) 115 | 116 | def enable_looping(self): 117 | #Enable all looping calls for enabled watchdirs here 118 | for watchdir_id, watchdir in self.watchdirs.iteritems(): 119 | if watchdir['enabled']: 120 | self.enable_watchdir(watchdir_id) 121 | 122 | def disable(self): 123 | #disable all running looping calls 124 | for loopingcall in self.update_timers.itervalues(): 125 | loopingcall.stop() 126 | self.config.save() 127 | 128 | def update(self): 129 | pass 130 | 131 | @export() 132 | def set_options(self, watchdir_id, options): 133 | """Update the options for a watch folder.""" 134 | watchdir_id = str(watchdir_id) 135 | options = self._make_unicode(options) 136 | CheckInput(watchdir_id in self.watchdirs , _("Watch folder does not exist.")) 137 | if options.has_key('path'): 138 | options['abspath'] = os.path.abspath(options['path']) 139 | CheckInput(os.path.isdir(options['abspath']), _("Path does not exist.")) 140 | for w_id, w in self.watchdirs.iteritems(): 141 | if options['abspath'] == w['abspath'] and watchdir_id != w_id: 142 | raise Exception("Path is already being watched.") 143 | for key in options.keys(): 144 | if not key in OPTIONS_AVAILABLE: 145 | if not key in [key2+'_toggle' for key2 in OPTIONS_AVAILABLE.iterkeys()]: 146 | raise Exception("autoadd: Invalid options key:%s" % key) 147 | #disable the watch loop if it was active 148 | if watchdir_id in self.update_timers: 149 | self.disable_watchdir(watchdir_id) 150 | 151 | self.watchdirs[watchdir_id].update(options) 152 | #re-enable watch loop if appropriate 153 | if self.watchdirs[watchdir_id]['enabled']: 154 | self.enable_watchdir(watchdir_id) 155 | self.config.save() 156 | component.get("EventManager").emit(AutoaddOptionsChangedEvent()) 157 | 158 | def load_torrent(self, filename): 159 | try: 160 | log.debug("Attempting to open %s for add.", filename) 161 | _file = open(filename, "rb") 162 | filedump = _file.read() 163 | if not filedump: 164 | raise RuntimeError, "Torrent is 0 bytes!" 165 | _file.close() 166 | except IOError, e: 167 | log.warning("Unable to open %s: %s", filename, e) 168 | raise e 169 | 170 | # Get the info to see if any exceptions are raised 171 | info = lt.torrent_info(lt.bdecode(filedump)) 172 | 173 | return filedump 174 | 175 | def update_watchdir(self, watchdir_id): 176 | """Check the watch folder for new torrents to add.""" 177 | watchdir_id = str(watchdir_id) 178 | watchdir = self.watchdirs[watchdir_id] 179 | if not watchdir['enabled']: 180 | # We shouldn't be updating because this watchdir is not enabled 181 | self.disable_watchdir(watchdir_id) 182 | return 183 | 184 | if not os.path.isdir(watchdir["abspath"]): 185 | log.warning("Invalid AutoAdd folder: %s", watchdir["abspath"]) 186 | self.disable_watchdir(watchdir_id) 187 | return 188 | 189 | # Generate options dict for watchdir 190 | opts = {} 191 | if 'stop_at_ratio_toggle' in watchdir: 192 | watchdir['stop_ratio_toggle'] = watchdir['stop_at_ratio_toggle'] 193 | # We default to True wher reading _toggle values, so a config 194 | # without them is valid, and applies all its settings. 195 | for option, value in watchdir.iteritems(): 196 | if OPTIONS_AVAILABLE.get(option): 197 | if watchdir.get(option+'_toggle', True): 198 | opts[option] = value 199 | for filename in os.listdir(watchdir["abspath"]): 200 | if filename.split(".")[-1] == "torrent": 201 | try: 202 | filepath = os.path.join(watchdir["abspath"], filename) 203 | except UnicodeDecodeError, e: 204 | log.error("Unable to auto add torrent due to inproper filename encoding: %s", e) 205 | continue 206 | try: 207 | filedump = self.load_torrent(filepath) 208 | except (RuntimeError, Exception), e: 209 | # If the torrent is invalid, we keep track of it so that we 210 | # can try again on the next pass. This is because some 211 | # torrents may not be fully saved during the pass. 212 | log.debug("Torrent is invalid: %s", e) 213 | if filename in self.invalid_torrents: 214 | self.invalid_torrents[filename] += 1 215 | if self.invalid_torrents[filename] >= MAX_NUM_ATTEMPTS: 216 | os.rename(filepath, filepath + ".invalid") 217 | del self.invalid_torrents[filename] 218 | else: 219 | self.invalid_torrents[filename] = 1 220 | continue 221 | 222 | # The torrent looks good, so lets add it to the session. 223 | torrent_id = component.get("TorrentManager").add(filedump=filedump, filename=filename, options=opts) 224 | # If the torrent added successfully, set the extra options. 225 | if torrent_id: 226 | if 'Label' in component.get("CorePluginManager").get_enabled_plugins(): 227 | if watchdir.get('label_toggle', True) and watchdir.get('label'): 228 | label = component.get("CorePlugin.Label") 229 | if not watchdir['label'] in label.get_labels(): 230 | label.add(watchdir['label']) 231 | label.set_torrent(torrent_id, watchdir['label']) 232 | if watchdir.get('queue_to_top_toggle', True) and 'queue_to_top' in watchdir: 233 | if watchdir['queue_to_top']: 234 | component.get("TorrentManager").queue_top(torrent_id) 235 | else: 236 | component.get("TorrentManager").queue_bottom(torrent_id) 237 | # Rename or delete the torrent once added to deluge. 238 | if watchdir.get('append_extension_toggle'): 239 | if not watchdir.get('append_extension'): 240 | watchdir['append_extension'] = ".added" 241 | os.rename(filepath, filepath + watchdir['append_extension']) 242 | else: 243 | os.remove(filepath) 244 | 245 | def on_update_watchdir_error(self, failure, watchdir_id): 246 | """Disables any watch folders with unhandled exceptions.""" 247 | self.disable_watchdir(watchdir_id) 248 | log.error("Disabling '%s', error during update: %s" % (self.watchdirs[watchdir_id]["path"], failure)) 249 | 250 | @export 251 | def enable_watchdir(self, watchdir_id): 252 | watchdir_id = str(watchdir_id) 253 | # Enable the looping call 254 | if watchdir_id not in self.update_timers or not self.update_timers[watchdir_id].running: 255 | self.update_timers[watchdir_id] = LoopingCall(self.update_watchdir, watchdir_id) 256 | self.update_timers[watchdir_id].start(5).addErrback(self.on_update_watchdir_error, watchdir_id) 257 | # Update the config 258 | if not self.watchdirs[watchdir_id]['enabled']: 259 | self.watchdirs[watchdir_id]['enabled'] = True 260 | self.config.save() 261 | component.get("EventManager").emit(AutoaddOptionsChangedEvent()) 262 | 263 | @export 264 | def disable_watchdir(self, watchdir_id): 265 | watchdir_id = str(watchdir_id) 266 | # Disable the looping call 267 | if watchdir_id in self.update_timers: 268 | if self.update_timers[watchdir_id].running: 269 | self.update_timers[watchdir_id].stop() 270 | del self.update_timers[watchdir_id] 271 | # Update the config 272 | if self.watchdirs[watchdir_id]['enabled']: 273 | self.watchdirs[watchdir_id]['enabled'] = False 274 | self.config.save() 275 | component.get("EventManager").emit(AutoaddOptionsChangedEvent()) 276 | 277 | @export 278 | def set_config(self, config): 279 | """Sets the config dictionary.""" 280 | config = self._make_unicode(config) 281 | for key in config.keys(): 282 | self.config[key] = config[key] 283 | self.config.save() 284 | component.get("EventManager").emit(AutoaddOptionsChangedEvent()) 285 | 286 | @export 287 | def get_config(self): 288 | """Returns the config dictionary.""" 289 | return self.config.config 290 | 291 | @export() 292 | def get_watchdirs(self): 293 | return self.watchdirs.keys() 294 | 295 | def _make_unicode(self, options): 296 | opts = {} 297 | for key in options: 298 | if isinstance(options[key], str): 299 | options[key] = unicode(options[key], "utf8") 300 | opts[key] = options[key] 301 | return opts 302 | 303 | @export() 304 | def add(self, options={}): 305 | """Add a watch folder.""" 306 | options = self._make_unicode(options) 307 | abswatchdir = os.path.abspath(options['path']) 308 | CheckInput(os.path.isdir(abswatchdir) , _("Path does not exist.")) 309 | CheckInput(os.access(abswatchdir, os.R_OK|os.W_OK), "You must have read and write access to watch folder.") 310 | if abswatchdir in [wd['abspath'] for wd in self.watchdirs.itervalues()]: 311 | raise Exception("Path is already being watched.") 312 | options.setdefault('enabled', False) 313 | options['abspath'] = abswatchdir 314 | watchdir_id = self.config['next_id'] 315 | self.watchdirs[str(watchdir_id)] = options 316 | if options.get('enabled'): 317 | self.enable_watchdir(watchdir_id) 318 | self.config['next_id'] = watchdir_id + 1 319 | self.config.save() 320 | component.get("EventManager").emit(AutoaddOptionsChangedEvent()) 321 | return watchdir_id 322 | 323 | @export 324 | def remove(self, watchdir_id): 325 | """Remove a watch folder.""" 326 | watchdir_id = str(watchdir_id) 327 | CheckInput(watchdir_id in self.watchdirs, "Unknown Watchdir: %s" % self.watchdirs) 328 | if self.watchdirs[watchdir_id]['enabled']: 329 | self.disable_watchdir(watchdir_id) 330 | del self.watchdirs[watchdir_id] 331 | self.config.save() 332 | component.get("EventManager").emit(AutoaddOptionsChangedEvent()) 333 | -------------------------------------------------------------------------------- /autoadd/gtkui.py: -------------------------------------------------------------------------------- 1 | # 2 | # gtkui.py 3 | # 4 | # Copyright (C) 2009 GazpachoKing 5 | # 6 | # Basic plugin template created by: 7 | # Copyright (C) 2008 Martijn Voncken 8 | # Copyright (C) 2007-2009 Andrew Resch 9 | # Copyright (C) 2009 Damien Churchill 10 | # 11 | # Deluge is free software. 12 | # 13 | # You may redistribute it and/or modify it under the terms of the 14 | # GNU General Public License, as published by the Free Software 15 | # Foundation; either version 3 of the License, or (at your option) 16 | # any later version. 17 | # 18 | # deluge is distributed in the hope that it will be useful, 19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 21 | # See the GNU General Public License for more details. 22 | # 23 | # You should have received a copy of the GNU General Public License 24 | # along with deluge. If not, write to: 25 | # The Free Software Foundation, Inc., 26 | # 51 Franklin Street, Fifth Floor 27 | # Boston, MA 02110-1301, USA. 28 | # 29 | # In addition, as a special exception, the copyright holders give 30 | # permission to link the code of portions of this program with the OpenSSL 31 | # library. 32 | # You must obey the GNU General Public License in all respects for all of 33 | # the code used other than OpenSSL. If you modify file(s) with this 34 | # exception, you may extend this exception to your version of the file(s), 35 | # but you are not obligated to do so. If you do not wish to do so, delete 36 | # this exception statement from your version. If you delete this exception 37 | # statement from all source files in the program, then also delete it here. 38 | # 39 | 40 | import gtk 41 | 42 | from deluge.log import LOG as log 43 | from deluge.ui.client import client 44 | from deluge.plugins.pluginbase import GtkPluginBase 45 | import deluge.component as component 46 | import deluge.common 47 | import os 48 | 49 | from common import get_resource 50 | 51 | class OptionsDialog(): 52 | spin_ids = ["max_download_speed", "max_upload_speed", "stop_ratio"] 53 | spin_int_ids = ["max_upload_slots", "max_connections"] 54 | chk_ids = ["stop_at_ratio", "remove_at_ratio", "move_completed", "add_paused", "auto_managed", "queue_to_top"] 55 | def __init__(self): 56 | pass 57 | 58 | def show(self, options={}, watchdir_id=None): 59 | self.glade = gtk.glade.XML(get_resource("autoadd_options.glade")) 60 | self.glade.signal_autoconnect({ 61 | "on_opts_add":self.on_add, 62 | "on_opts_apply":self.on_apply, 63 | "on_opts_cancel":self.on_cancel, 64 | "on_options_dialog_close":self.on_cancel, 65 | "on_error_ok":self.on_error_ok, 66 | "on_error_dialog_close":self.on_error_ok, 67 | "on_toggle_toggled":self.on_toggle_toggled 68 | }) 69 | self.dialog = self.glade.get_widget("options_dialog") 70 | self.dialog.set_transient_for(component.get("Preferences").pref_dialog) 71 | self.err_dialog = self.glade.get_widget("error_dialog") 72 | self.err_dialog.set_transient_for(self.dialog) 73 | if watchdir_id: 74 | #We have an existing watchdir_id, we are editing 75 | self.glade.get_widget('opts_add_button').hide() 76 | self.glade.get_widget('opts_apply_button').show() 77 | self.watchdir_id = watchdir_id 78 | else: 79 | #We don't have an id, adding 80 | self.glade.get_widget('opts_add_button').show() 81 | self.glade.get_widget('opts_apply_button').hide() 82 | self.watchdir_id = None 83 | 84 | self.load_options(options) 85 | self.dialog.run() 86 | 87 | def load_options(self, options): 88 | self.glade.get_widget('enabled').set_active(options.get('enabled', False)) 89 | self.glade.get_widget('append_extension_toggle').set_active(options.get('append_extension_toggle', False)) 90 | self.glade.get_widget('append_extension').set_text(options.get('append_extension', '.added')) 91 | self.glade.get_widget('download_location_toggle').set_active(options.get('download_location_toggle', False)) 92 | self.glade.get_widget('label').set_text(options.get('label', '')) 93 | self.glade.get_widget('label_toggle').set_active(options.get('label_toggle', False)) 94 | for id in self.spin_ids + self.spin_int_ids: 95 | self.glade.get_widget(id).set_value(options.get(id, 0)) 96 | self.glade.get_widget(id+'_toggle').set_active(options.get(id+'_toggle', False)) 97 | for id in self.chk_ids: 98 | self.glade.get_widget(id).set_active(bool(options.get(id, True))) 99 | self.glade.get_widget(id+'_toggle').set_active(options.get(id+'_toggle', False)) 100 | if not options.get('add_paused', True): 101 | self.glade.get_widget('isnt_add_paused').set_active(True) 102 | if not options.get('queue_to_top', True): 103 | self.glade.get_widget('isnt_queue_to_top').set_active(True) 104 | if not options.get('auto_managed', True): 105 | self.glade.get_widget('isnt_auto_managed').set_active(True) 106 | for field in ['move_completed_path', 'path', 'download_location']: 107 | if client.is_localhost(): 108 | self.glade.get_widget(field+"_chooser").set_filename(options.get(field, os.path.expanduser("~"))) 109 | self.glade.get_widget(field+"_chooser").show() 110 | self.glade.get_widget(field+"_entry").hide() 111 | else: 112 | self.glade.get_widget(field+"_entry").set_text(options.get(field, "")) 113 | self.glade.get_widget(field+"_entry").show() 114 | self.glade.get_widget(field+"_chooser").hide() 115 | self.set_sensitive() 116 | 117 | def on_get_enabled_plugins(result): 118 | if 'Label' in result: 119 | self.glade.get_widget('label_frame').show() 120 | else: 121 | self.glade.get_widget('label_frame').hide() 122 | self.glade.get_widget('label_toggle').set_active(False) 123 | 124 | client.core.get_enabled_plugins().addCallback(on_get_enabled_plugins) 125 | 126 | def set_sensitive(self): 127 | maintoggles = ['download_location', 'append_extension', 'move_completed', 'label', \ 128 | 'max_download_speed', 'max_upload_speed', 'max_connections', \ 129 | 'max_upload_slots', 'add_paused', 'auto_managed', 'stop_at_ratio', 'queue_to_top'] 130 | [self.on_toggle_toggled(self.glade.get_widget(x+'_toggle')) for x in maintoggles] 131 | 132 | def on_toggle_toggled(self, tb): 133 | toggle = str(tb.name).replace("_toggle", "") 134 | isactive = tb.get_active() 135 | if toggle == 'download_location': 136 | self.glade.get_widget('download_location_chooser').set_sensitive(isactive) 137 | self.glade.get_widget('download_location_entry').set_sensitive(isactive) 138 | elif toggle == 'append_extension': 139 | self.glade.get_widget('append_extension').set_sensitive(isactive) 140 | elif toggle == 'move_completed': 141 | self.glade.get_widget('move_completed_path_chooser').set_sensitive(isactive) 142 | self.glade.get_widget('move_completed_path_entry').set_sensitive(isactive) 143 | self.glade.get_widget('move_completed').set_active(isactive) 144 | elif toggle == 'label': 145 | self.glade.get_widget('label').set_sensitive(isactive) 146 | elif toggle == 'max_download_speed': 147 | self.glade.get_widget('max_download_speed').set_sensitive(isactive) 148 | elif toggle == 'max_upload_speed': 149 | self.glade.get_widget('max_upload_speed').set_sensitive(isactive) 150 | elif toggle == 'max_connections': 151 | self.glade.get_widget('max_connections').set_sensitive(isactive) 152 | elif toggle == 'max_upload_slots': 153 | self.glade.get_widget('max_upload_slots').set_sensitive(isactive) 154 | elif toggle == 'add_paused': 155 | self.glade.get_widget('add_paused').set_sensitive(isactive) 156 | self.glade.get_widget('isnt_add_paused').set_sensitive(isactive) 157 | elif toggle == 'queue_to_top': 158 | self.glade.get_widget('queue_to_top').set_sensitive(isactive) 159 | self.glade.get_widget('isnt_queue_to_top').set_sensitive(isactive) 160 | elif toggle == 'auto_managed': 161 | self.glade.get_widget('auto_managed').set_sensitive(isactive) 162 | self.glade.get_widget('isnt_auto_managed').set_sensitive(isactive) 163 | elif toggle == 'stop_at_ratio': 164 | self.glade.get_widget('remove_at_ratio_toggle').set_active(isactive) 165 | self.glade.get_widget('stop_ratio_toggle').set_active(isactive) 166 | self.glade.get_widget('stop_at_ratio').set_active(isactive) 167 | self.glade.get_widget('stop_ratio').set_sensitive(isactive) 168 | self.glade.get_widget('remove_at_ratio').set_sensitive(isactive) 169 | 170 | def on_apply(self, Event=None): 171 | client.autoadd.set_options(str(self.watchdir_id), self.generate_opts()).addCallbacks(self.on_added, self.on_error_show) 172 | 173 | def on_error_show(self, result): 174 | self.glade.get_widget('error_label').set_text(result.value.exception_msg) 175 | self.err_dialog = self.glade.get_widget('error_dialog') 176 | self.err_dialog.set_transient_for(self.dialog) 177 | result.cleanFailure() 178 | self.err_dialog.show() 179 | 180 | def on_added(self, result): 181 | self.dialog.destroy() 182 | 183 | def on_error_ok(self, Event=None): 184 | self.err_dialog.hide() 185 | 186 | def on_add(self, Event=None): 187 | client.autoadd.add(self.generate_opts()).addCallbacks(self.on_added, self.on_error_show) 188 | 189 | def on_cancel(self, Event=None): 190 | self.dialog.destroy() 191 | 192 | def generate_opts(self): 193 | # generate options dict based on gtk objects 194 | options = {} 195 | options['enabled'] = self.glade.get_widget('enabled').get_active() 196 | if client.is_localhost(): 197 | options['path'] = self.glade.get_widget('path_chooser').get_filename() 198 | options['download_location'] = self.glade.get_widget('download_location_chooser').get_filename() 199 | options['move_completed_path'] = self.glade.get_widget('move_completed_path_chooser').get_filename() 200 | else: 201 | options['path'] = self.glade.get_widget('path_entry').get_text() 202 | options['download_location'] = self.glade.get_widget('download_location_entry').get_text() 203 | options['move_completed_path'] = self.glade.get_widget('move_completed_path_entry').get_text() 204 | options['append_extension_toggle'] = self.glade.get_widget('append_extension_toggle').get_active() 205 | options['append_extension'] = self.glade.get_widget('append_extension').get_text() 206 | options['download_location_toggle'] = self.glade.get_widget('download_location_toggle').get_active() 207 | options['label'] = self.glade.get_widget('label').get_text().lower() 208 | options['label_toggle'] = self.glade.get_widget('label_toggle').get_active() 209 | 210 | for id in self.spin_ids: 211 | options[id] = self.glade.get_widget(id).get_value() 212 | options[id+'_toggle'] = self.glade.get_widget(id+'_toggle').get_active() 213 | for id in self.spin_int_ids: 214 | options[id] = self.glade.get_widget(id).get_value_as_int() 215 | options[id+'_toggle'] = self.glade.get_widget(id+'_toggle').get_active() 216 | for id in self.chk_ids: 217 | options[id] = self.glade.get_widget(id).get_active() 218 | options[id+'_toggle'] = self.glade.get_widget(id+'_toggle').get_active() 219 | return options 220 | 221 | 222 | class GtkUI(GtkPluginBase): 223 | def enable(self): 224 | 225 | self.glade = gtk.glade.XML(get_resource("config.glade")) 226 | self.glade.signal_autoconnect({ 227 | "on_add_button_clicked": self.on_add_button_clicked, 228 | "on_edit_button_clicked": self.on_edit_button_clicked, 229 | "on_remove_button_clicked": self.on_remove_button_clicked 230 | }) 231 | self.opts_dialog = OptionsDialog() 232 | 233 | component.get("PluginManager").register_hook("on_apply_prefs", self.on_apply_prefs) 234 | component.get("PluginManager").register_hook("on_show_prefs", self.on_show_prefs) 235 | client.register_event_handler("AutoaddOptionsChangedEvent", self.on_options_changed_event) 236 | 237 | self.watchdirs = {} 238 | 239 | vbox = self.glade.get_widget("watchdirs_vbox") 240 | sw = gtk.ScrolledWindow() 241 | sw.set_shadow_type(gtk.SHADOW_ETCHED_IN) 242 | sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) 243 | 244 | vbox.pack_start(sw, True, True, 0) 245 | 246 | self.store = self.create_model() 247 | 248 | self.treeView = gtk.TreeView(self.store) 249 | self.treeView.connect("cursor-changed", self.on_listitem_activated) 250 | self.treeView.connect("row-activated", self.on_edit_button_clicked) 251 | self.treeView.set_rules_hint(True) 252 | 253 | self.create_columns(self.treeView) 254 | sw.add(self.treeView) 255 | sw.show_all() 256 | component.get("Preferences").add_page("AutoAdd", self.glade.get_widget("prefs_box")) 257 | self.on_show_prefs() 258 | 259 | 260 | def disable(self): 261 | component.get("Preferences").remove_page("AutoAdd") 262 | component.get("PluginManager").deregister_hook("on_apply_prefs", self.on_apply_prefs) 263 | component.get("PluginManager").deregister_hook("on_show_prefs", self.on_show_prefs) 264 | 265 | def create_model(self): 266 | 267 | store = gtk.ListStore(str, bool, str) 268 | for watchdir_id, watchdir in self.watchdirs.iteritems(): 269 | store.append([watchdir_id, watchdir['enabled'], watchdir['path']]) 270 | return store 271 | 272 | def create_columns(self, treeView): 273 | rendererToggle = gtk.CellRendererToggle() 274 | column = gtk.TreeViewColumn("On", rendererToggle, activatable=True, active=1) 275 | column.set_sort_column_id(1) 276 | treeView.append_column(column) 277 | tt = gtk.Tooltip() 278 | tt.set_text('Double-click to toggle') 279 | treeView.set_tooltip_cell(tt, None, None, rendererToggle) 280 | 281 | rendererText = gtk.CellRendererText() 282 | column = gtk.TreeViewColumn("Path", rendererText, text=2) 283 | column.set_sort_column_id(2) 284 | treeView.append_column(column) 285 | tt2 = gtk.Tooltip() 286 | tt2.set_text('Double-click to edit') 287 | #treeView.set_tooltip_cell(tt2, None, column, None) 288 | treeView.set_has_tooltip(True) 289 | 290 | def load_watchdir_list(self): 291 | pass 292 | 293 | def add_watchdir_entry(self): 294 | pass 295 | 296 | def on_add_button_clicked(self, Event=None): 297 | #display options_window 298 | self.opts_dialog.show() 299 | 300 | def on_remove_button_clicked(self, Event=None): 301 | tree, tree_id = self.treeView.get_selection().get_selected() 302 | watchdir_id = str(self.store.get_value(tree_id, 0)) 303 | if watchdir_id: 304 | client.autoadd.remove(watchdir_id) 305 | 306 | def on_edit_button_clicked(self, Event=None, a=None, col=None): 307 | tree, tree_id = self.treeView.get_selection().get_selected() 308 | watchdir_id = str(self.store.get_value(tree_id, 0)) 309 | if watchdir_id: 310 | if col and col.get_title() == 'On': 311 | if self.watchdirs[watchdir_id]['enabled']: 312 | client.autoadd.disable_watchdir(watchdir_id) 313 | else: 314 | client.autoadd.enable_watchdir(watchdir_id) 315 | else: 316 | self.opts_dialog.show(self.watchdirs[watchdir_id], watchdir_id) 317 | 318 | def on_listitem_activated(self, treeview): 319 | tree, tree_id = self.treeView.get_selection().get_selected() 320 | if tree_id: 321 | self.glade.get_widget('edit_button').set_sensitive(True) 322 | self.glade.get_widget('remove_button').set_sensitive(True) 323 | else: 324 | self.glade.get_widget('edit_button').set_sensitive(False) 325 | self.glade.get_widget('remove_button').set_sensitive(False) 326 | 327 | def on_apply_prefs(self): 328 | log.debug("applying prefs for AutoAdd") 329 | for watchdir_id, watchdir in self.watchdirs.iteritems(): 330 | client.autoadd.set_options(watchdir_id, watchdir) 331 | 332 | def on_show_prefs(self): 333 | client.autoadd.get_config().addCallback(self.cb_get_config) 334 | 335 | def on_options_changed_event(self): 336 | client.autoadd.get_config().addCallback(self.cb_get_config) 337 | 338 | def cb_get_config(self, config): 339 | """callback for on show_prefs""" 340 | self.watchdirs = config.get('watchdirs', {}) 341 | self.store.clear() 342 | for watchdir_id, watchdir in self.watchdirs.iteritems(): 343 | self.store.append([watchdir_id, watchdir['enabled'], watchdir['path']]) 344 | # Disable the remove and edit buttons, because nothing in the store is selected 345 | self.glade.get_widget('remove_button').set_sensitive(False) 346 | self.glade.get_widget('edit_button').set_sensitive(False) 347 | 348 | -------------------------------------------------------------------------------- /autoadd/data/autoadd_options.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 6 7 | AutoAdd Error 8 | False 9 | True 10 | dialog 11 | 12 | 13 | 14 | True 15 | 16 | 17 | True 18 | 19 | 20 | True 21 | gtk-dialog-error 22 | 23 | 24 | False 25 | 0 26 | 27 | 28 | 29 | 30 | True 31 | 0.46000000834465027 32 | Error 33 | True 34 | 35 | 36 | False 37 | False 38 | 1 39 | 40 | 41 | 42 | 43 | 2 44 | 45 | 46 | 47 | 48 | True 49 | end 50 | 51 | 52 | gtk-ok 53 | -5 54 | True 55 | True 56 | True 57 | False 58 | True 59 | 60 | 61 | 62 | False 63 | False 64 | 0 65 | 66 | 67 | 68 | 69 | False 70 | end 71 | 0 72 | 73 | 74 | 75 | 76 | 77 | 78 | Watch Folder Properties 79 | False 80 | True 81 | dialog 82 | 83 | 84 | 85 | True 86 | vertical 87 | 88 | 89 | True 90 | vertical 91 | 92 | 93 | True 94 | True 95 | 96 | 97 | True 98 | 6 99 | vertical 100 | 101 | 102 | True 103 | 0 104 | none 105 | 106 | 107 | True 108 | 12 109 | 110 | 111 | True 112 | vertical 113 | 114 | 115 | True 116 | 117 | 118 | True 119 | True 120 | 121 | 122 | 123 | 0 124 | 125 | 126 | 127 | 128 | True 129 | select-folder 130 | Select A Folder 131 | 132 | 133 | 1 134 | 135 | 136 | 137 | 138 | 0 139 | 140 | 141 | 142 | 143 | Enable this watch folder 144 | True 145 | True 146 | False 147 | True 148 | True 149 | 150 | 151 | 152 | False 153 | False 154 | 1 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | True 164 | <b>Watch Folder</b> 165 | True 166 | 167 | 168 | label_item 169 | 170 | 171 | 172 | 173 | False 174 | 0 175 | 176 | 177 | 178 | 179 | True 180 | 0 181 | none 182 | 183 | 184 | True 185 | 12 186 | 187 | 188 | True 189 | 190 | 191 | True 192 | vertical 193 | 194 | 195 | Delete .torrent after adding 196 | True 197 | True 198 | False 199 | True 200 | True 201 | 202 | 203 | 0 204 | 205 | 206 | 207 | 208 | True 209 | 210 | 211 | Append extension after adding: 212 | True 213 | True 214 | False 215 | True 216 | isnt_append_extension 217 | 218 | 219 | 220 | 0 221 | 222 | 223 | 224 | 225 | True 226 | True 227 | 228 | .added 229 | 230 | 231 | 1 232 | 233 | 234 | 235 | 236 | 1 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | True 248 | <b>Torrent File Action</b> 249 | True 250 | 251 | 252 | label_item 253 | 254 | 255 | 256 | 257 | 1 258 | 259 | 260 | 261 | 262 | True 263 | 0 264 | none 265 | 266 | 267 | True 268 | 12 269 | 270 | 271 | True 272 | vertical 273 | 274 | 275 | Set download location 276 | True 277 | True 278 | False 279 | True 280 | True 281 | 282 | 283 | 284 | False 285 | False 286 | 0 287 | 288 | 289 | 290 | 291 | True 292 | 293 | 294 | True 295 | True 296 | 297 | 298 | 299 | 0 300 | 301 | 302 | 303 | 304 | True 305 | select-folder 306 | Select A Folder 307 | 308 | 309 | 1 310 | 311 | 312 | 313 | 314 | False 315 | 1 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | True 325 | <b>Download Location</b> 326 | True 327 | 328 | 329 | label_item 330 | 331 | 332 | 333 | 334 | False 335 | 2 336 | 337 | 338 | 339 | 340 | True 341 | 0 342 | none 343 | 344 | 345 | True 346 | 12 347 | 348 | 349 | True 350 | vertical 351 | 352 | 353 | Set move completed location 354 | True 355 | True 356 | False 357 | True 358 | True 359 | 360 | 361 | 362 | False 363 | False 364 | 0 365 | 366 | 367 | 368 | 369 | True 370 | 371 | 372 | True 373 | True 374 | 375 | 376 | 377 | 0 378 | 379 | 380 | 381 | 382 | True 383 | select-folder 384 | Select A Folder 385 | 386 | 387 | 1 388 | 389 | 390 | 391 | 392 | False 393 | False 394 | False 395 | True 396 | True 397 | True 398 | 399 | 400 | False 401 | False 402 | 2 403 | 404 | 405 | 406 | 407 | False 408 | 1 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | True 418 | <b>Move Completed</b> 419 | True 420 | 421 | 422 | label_item 423 | 424 | 425 | 426 | 427 | False 428 | 3 429 | 430 | 431 | 432 | 433 | 0 434 | none 435 | 436 | 437 | True 438 | 12 439 | 440 | 441 | True 442 | 443 | 444 | Label: 445 | True 446 | True 447 | False 448 | True 449 | True 450 | 451 | 452 | 453 | False 454 | False 455 | 0 456 | 457 | 458 | 459 | 460 | True 461 | True 462 | 463 | 464 | 465 | 1 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | True 475 | <b>Label</b> 476 | True 477 | 478 | 479 | label_item 480 | 481 | 482 | 483 | 484 | 4 485 | 486 | 487 | 488 | 489 | 490 | 491 | True 492 | Main 493 | 494 | 495 | False 496 | tab 497 | 498 | 499 | 500 | 501 | True 502 | 6 503 | vertical 504 | 505 | 506 | 507 | 508 | 509 | True 510 | 0 511 | none 512 | 513 | 514 | True 515 | 12 516 | 517 | 518 | True 519 | 3 520 | 4 521 | 3 522 | 523 | 524 | Max Upload Speed: 525 | True 526 | True 527 | False 528 | True 529 | True 530 | 531 | 532 | 533 | 1 534 | 2 535 | GTK_FILL 536 | 537 | 538 | 539 | 540 | 541 | Max Connections: 542 | True 543 | True 544 | False 545 | True 546 | True 547 | 548 | 549 | 550 | 2 551 | 3 552 | GTK_FILL 553 | 554 | 555 | 556 | 557 | 558 | Max Upload Slots: 559 | True 560 | True 561 | False 562 | True 563 | True 564 | 565 | 566 | 567 | 3 568 | 4 569 | GTK_FILL 570 | 571 | 572 | 573 | 574 | 575 | True 576 | True 577 | -1 -1 10000 1 10 0 578 | 1 579 | 1 580 | 581 | 582 | 1 583 | 2 584 | 585 | 586 | 587 | 588 | 589 | True 590 | True 591 | -1 -1 10000 1 10 0 592 | 1 593 | 1 594 | 595 | 596 | 1 597 | 2 598 | 1 599 | 2 600 | 601 | 602 | 603 | 604 | 605 | True 606 | True 607 | -1 -1 10000 1 10 0 608 | 1 609 | 610 | 611 | 1 612 | 2 613 | 2 614 | 3 615 | 616 | 617 | 618 | 619 | 620 | True 621 | True 622 | -1 -1 10000 1 10 0 623 | 1 624 | 625 | 626 | 1 627 | 2 628 | 3 629 | 4 630 | 631 | 632 | 633 | 634 | 635 | True 636 | 0 637 | 5 638 | KiB/s 639 | 640 | 641 | 2 642 | 3 643 | GTK_FILL 644 | 645 | 646 | 647 | 648 | 649 | True 650 | 0 651 | 5 652 | KiB/s 653 | 654 | 655 | 2 656 | 3 657 | 1 658 | 2 659 | GTK_FILL 660 | 661 | 662 | 663 | 664 | 665 | Max Download Speed: 666 | True 667 | True 668 | False 669 | True 670 | True 671 | 672 | 673 | 674 | GTK_FILL 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | True 691 | <b>Bandwidth</b> 692 | True 693 | 694 | 695 | label_item 696 | 697 | 698 | 699 | 700 | 1 701 | 702 | 703 | 704 | 705 | True 706 | 0 707 | none 708 | 709 | 710 | True 711 | 12 712 | 713 | 714 | True 715 | 5 716 | 4 717 | 718 | 719 | True 720 | 721 | 722 | Stop seed at ratio: 723 | True 724 | True 725 | False 726 | True 727 | True 728 | 729 | 730 | 731 | 732 | 733 | 3 734 | 4 735 | GTK_FILL 736 | 737 | 738 | 739 | 740 | True 741 | 0 742 | 0 743 | 12 744 | 745 | 746 | Remove at ratio 747 | True 748 | True 749 | False 750 | True 751 | True 752 | 753 | 754 | 755 | 756 | 4 757 | 5 758 | 759 | 760 | 761 | 762 | Auto Managed: 763 | True 764 | True 765 | False 766 | True 767 | True 768 | 769 | 770 | 771 | 2 772 | 3 773 | GTK_FILL 774 | 775 | 776 | 777 | 778 | 779 | False 780 | True 781 | False 782 | True 783 | True 784 | True 785 | 786 | 787 | 2 788 | 3 789 | 4 790 | 5 791 | GTK_FILL 792 | 793 | 794 | 795 | 796 | 797 | False 798 | True 799 | False 800 | True 801 | True 802 | True 803 | 804 | 805 | 1 806 | 2 807 | 4 808 | 5 809 | GTK_FILL 810 | 811 | 812 | 813 | 814 | 815 | True 816 | True 817 | 818 | 2 0 100 0.10000000149 10 0 819 | 1 820 | 1 821 | 822 | 823 | 1 824 | 2 825 | 3 826 | 4 827 | 828 | 829 | 830 | 831 | 832 | True 833 | True 834 | 835 | 836 | Yes 837 | True 838 | True 839 | False 840 | True 841 | True 842 | 843 | 844 | 0 845 | 846 | 847 | 848 | 849 | No 850 | True 851 | True 852 | False 853 | True 854 | auto_managed 855 | 856 | 857 | 1 858 | 859 | 860 | 861 | 862 | 1 863 | 2 864 | 2 865 | 3 866 | GTK_FILL 867 | GTK_FILL 868 | 869 | 870 | 871 | 872 | False 873 | True 874 | False 875 | True 876 | True 877 | True 878 | 879 | 880 | 2 881 | 3 882 | 3 883 | 4 884 | GTK_FILL 885 | 886 | 887 | 888 | 889 | 890 | Add Paused: 891 | True 892 | True 893 | False 894 | True 895 | 896 | 897 | 898 | 899 | 900 | True 901 | True 902 | 903 | 904 | Yes 905 | True 906 | True 907 | False 908 | True 909 | True 910 | 911 | 912 | 0 913 | 914 | 915 | 916 | 917 | No 918 | True 919 | True 920 | False 921 | True 922 | add_paused 923 | 924 | 925 | 1 926 | 927 | 928 | 929 | 930 | 1 931 | 2 932 | 933 | 934 | 935 | 936 | Queue to: 937 | True 938 | True 939 | False 940 | True 941 | 942 | 943 | 944 | 1 945 | 2 946 | 947 | 948 | 949 | 950 | True 951 | True 952 | 953 | 954 | Top 955 | True 956 | True 957 | False 958 | True 959 | True 960 | 961 | 962 | 0 963 | 964 | 965 | 966 | 967 | Bottom 968 | True 969 | True 970 | False 971 | True 972 | queue_to_top 973 | 974 | 975 | 1 976 | 977 | 978 | 979 | 980 | 1 981 | 2 982 | 1 983 | 2 984 | 985 | 986 | 987 | 988 | 989 | 990 | 991 | 992 | 993 | 994 | 995 | 996 | 997 | 998 | 999 | 1000 | 1001 | 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | 1008 | 1009 | 1010 | 1011 | 1012 | 1013 | 1014 | 1015 | 1016 | True 1017 | <b>Queue</b> 1018 | True 1019 | 1020 | 1021 | label_item 1022 | 1023 | 1024 | 1025 | 1026 | 2 1027 | 1028 | 1029 | 1030 | 1031 | 1 1032 | 1033 | 1034 | 1035 | 1036 | True 1037 | Options 1038 | 1039 | 1040 | 1 1041 | False 1042 | tab 1043 | 1044 | 1045 | 1046 | 1047 | 0 1048 | 1049 | 1050 | 1051 | 1052 | True 1053 | vertical 1054 | 1055 | 1056 | 1 1057 | 1058 | 1059 | 1060 | 1061 | 2 1062 | 1063 | 1064 | 1065 | 1066 | True 1067 | end 1068 | 1069 | 1070 | gtk-cancel 1071 | True 1072 | True 1073 | True 1074 | False 1075 | True 1076 | 1077 | 1078 | 1079 | False 1080 | False 1081 | 0 1082 | 1083 | 1084 | 1085 | 1086 | gtk-add 1087 | True 1088 | True 1089 | True 1090 | False 1091 | True 1092 | 1093 | 1094 | 1095 | False 1096 | False 1097 | 1 1098 | 1099 | 1100 | 1101 | 1102 | gtk-apply 1103 | True 1104 | True 1105 | True 1106 | False 1107 | True 1108 | 1109 | 1110 | 1111 | False 1112 | False 1113 | 2 1114 | 1115 | 1116 | 1117 | 1118 | False 1119 | end 1120 | 0 1121 | 1122 | 1123 | 1124 | 1125 | 1126 | 1127 | --------------------------------------------------------------------------------