1# -*- coding: utf-8 -*- 2# 3# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com> 4# 5# Basic plugin template created by: 6# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> 7# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com> 8# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com> 9# 10# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with 11# the additional special exception to link portions of this program with the OpenSSL library. 12# See LICENSE for more details. 13# 14 15from __future__ import unicode_literals 16 17import logging 18import os 19 20import gi # isort:skip (Required before Gtk import). 21 22gi.require_version('Gtk', '3.0') # NOQA: E402 23 24# isort:imports-thirdparty 25from gi.repository import Gtk 26 27# isort:imports-firstparty 28import deluge.common 29import deluge.component as component 30from deluge.plugins.pluginbase import Gtk3PluginBase 31from deluge.ui.client import client 32from deluge.ui.gtk3 import dialogs 33 34# isort:imports-localfolder 35from .common import get_resource 36 37log = logging.getLogger(__name__) 38 39 40class IncompatibleOption(Exception): 41 pass 42 43 44class OptionsDialog(object): 45 spin_ids = ['max_download_speed', 'max_upload_speed', 'stop_ratio'] 46 spin_int_ids = ['max_upload_slots', 'max_connections'] 47 chk_ids = [ 48 'stop_at_ratio', 49 'remove_at_ratio', 50 'move_completed', 51 'add_paused', 52 'auto_managed', 53 'queue_to_top', 54 ] 55 56 def __init__(self): 57 self.accounts = Gtk.ListStore(str) 58 self.labels = Gtk.ListStore(str) 59 self.core_config = {} 60 61 def show(self, options=None, watchdir_id=None): 62 if options is None: 63 options = {} 64 self.builder = Gtk.Builder() 65 self.builder.add_from_file(get_resource('autoadd_options.ui')) 66 self.builder.connect_signals( 67 { 68 'on_opts_add': self.on_add, 69 'on_opts_apply': self.on_apply, 70 'on_opts_cancel': self.on_cancel, 71 'on_options_dialog_close': self.on_cancel, 72 'on_toggle_toggled': self.on_toggle_toggled, 73 } 74 ) 75 self.dialog = self.builder.get_object('options_dialog') 76 self.dialog.set_transient_for(component.get('Preferences').pref_dialog) 77 78 if watchdir_id: 79 # We have an existing watchdir_id, we are editing 80 self.builder.get_object('opts_add_button').hide() 81 self.builder.get_object('opts_apply_button').show() 82 self.watchdir_id = watchdir_id 83 else: 84 # We don't have an id, adding 85 self.builder.get_object('opts_add_button').show() 86 self.builder.get_object('opts_apply_button').hide() 87 self.watchdir_id = None 88 89 self.load_options(options) 90 self.dialog.run() 91 92 def load_options(self, options): 93 self.builder.get_object('enabled').set_active(options.get('enabled', True)) 94 self.builder.get_object('append_extension_toggle').set_active( 95 options.get('append_extension_toggle', False) 96 ) 97 self.builder.get_object('append_extension').set_text( 98 options.get('append_extension', '.added') 99 ) 100 self.builder.get_object('download_location_toggle').set_active( 101 options.get('download_location_toggle', False) 102 ) 103 self.builder.get_object('copy_torrent_toggle').set_active( 104 options.get('copy_torrent_toggle', False) 105 ) 106 self.builder.get_object('delete_copy_torrent_toggle').set_active( 107 options.get('delete_copy_torrent_toggle', False) 108 ) 109 self.builder.get_object('seed_mode').set_active(options.get('seed_mode', False)) 110 self.accounts.clear() 111 self.labels.clear() 112 combobox = self.builder.get_object('OwnerCombobox') 113 combobox_render = Gtk.CellRendererText() 114 combobox.pack_start(combobox_render, True) 115 combobox.add_attribute(combobox_render, 'text', 0) 116 combobox.set_model(self.accounts) 117 118 label_widget = self.builder.get_object('label') 119 label_widget.get_child().set_text(options.get('label', '')) 120 label_widget.set_model(self.labels) 121 label_widget.set_entry_text_column(0) 122 self.builder.get_object('label_toggle').set_active( 123 options.get('label_toggle', False) 124 ) 125 126 for spin_id in self.spin_ids + self.spin_int_ids: 127 self.builder.get_object(spin_id).set_value(options.get(spin_id, 0)) 128 self.builder.get_object(spin_id + '_toggle').set_active( 129 options.get(spin_id + '_toggle', False) 130 ) 131 for chk_id in self.chk_ids: 132 self.builder.get_object(chk_id).set_active(bool(options.get(chk_id, True))) 133 self.builder.get_object(chk_id + '_toggle').set_active( 134 options.get(chk_id + '_toggle', False) 135 ) 136 if not options.get('add_paused', True): 137 self.builder.get_object('isnt_add_paused').set_active(True) 138 if not options.get('queue_to_top', True): 139 self.builder.get_object('isnt_queue_to_top').set_active(True) 140 if not options.get('auto_managed', True): 141 self.builder.get_object('isnt_auto_managed').set_active(True) 142 for field in [ 143 'move_completed_path', 144 'path', 145 'download_location', 146 'copy_torrent', 147 ]: 148 if client.is_localhost(): 149 self.builder.get_object(field + '_chooser').set_current_folder( 150 options.get(field, os.path.expanduser('~')) 151 ) 152 self.builder.get_object(field + '_chooser').show() 153 self.builder.get_object(field + '_entry').hide() 154 else: 155 self.builder.get_object(field + '_entry').set_text( 156 options.get(field, '') 157 ) 158 self.builder.get_object(field + '_entry').show() 159 self.builder.get_object(field + '_chooser').hide() 160 self.set_sensitive() 161 162 def on_core_config(config): 163 if client.is_localhost(): 164 self.builder.get_object('download_location_chooser').set_current_folder( 165 options.get('download_location', config['download_location']) 166 ) 167 if options.get('move_completed_toggle', config['move_completed']): 168 self.builder.get_object('move_completed_toggle').set_active(True) 169 self.builder.get_object( 170 'move_completed_path_chooser' 171 ).set_current_folder( 172 options.get( 173 'move_completed_path', config['move_completed_path'] 174 ) 175 ) 176 if options.get('copy_torrent_toggle', config['copy_torrent_file']): 177 self.builder.get_object('copy_torrent_toggle').set_active(True) 178 self.builder.get_object('copy_torrent_chooser').set_current_folder( 179 options.get('copy_torrent', config['torrentfiles_location']) 180 ) 181 else: 182 self.builder.get_object('download_location_entry').set_text( 183 options.get('download_location', config['download_location']) 184 ) 185 if options.get('move_completed_toggle', config['move_completed']): 186 self.builder.get_object('move_completed_toggle').set_active( 187 options.get('move_completed_toggle', False) 188 ) 189 self.builder.get_object('move_completed_path_entry').set_text( 190 options.get( 191 'move_completed_path', config['move_completed_path'] 192 ) 193 ) 194 if options.get('copy_torrent_toggle', config['copy_torrent_file']): 195 self.builder.get_object('copy_torrent_toggle').set_active(True) 196 self.builder.get_object('copy_torrent_entry').set_text( 197 options.get('copy_torrent', config['torrentfiles_location']) 198 ) 199 200 if options.get( 201 'delete_copy_torrent_toggle', config['del_copy_torrent_file'] 202 ): 203 self.builder.get_object('delete_copy_torrent_toggle').set_active(True) 204 205 if not options: 206 client.core.get_config().addCallback(on_core_config) 207 208 def on_accounts(accounts, owner): 209 log.debug('Got Accounts') 210 selected_iter = None 211 for account in accounts: 212 acc_iter = self.accounts.append() 213 self.accounts.set_value(acc_iter, 0, account['username']) 214 if account['username'] == owner: 215 selected_iter = acc_iter 216 self.builder.get_object('OwnerCombobox').set_active_iter(selected_iter) 217 218 def on_accounts_failure(failure): 219 log.debug('Failed to get accounts!!! %s', failure) 220 acc_iter = self.accounts.append() 221 self.accounts.set_value(acc_iter, 0, client.get_auth_user()) 222 self.builder.get_object('OwnerCombobox').set_active(0) 223 self.builder.get_object('OwnerCombobox').set_sensitive(False) 224 225 def on_labels(labels): 226 log.debug('Got Labels: %s', labels) 227 for label in labels: 228 self.labels.set_value(self.labels.append(), 0, label) 229 label_widget = self.builder.get_object('label') 230 label_widget.set_model(self.labels) 231 label_widget.set_entry_text_column(0) 232 233 def on_failure(failure): 234 log.exception(failure) 235 236 def on_get_enabled_plugins(result): 237 if 'Label' in result: 238 self.builder.get_object('label_frame').show() 239 client.label.get_labels().addCallback(on_labels).addErrback(on_failure) 240 else: 241 self.builder.get_object('label_frame').hide() 242 self.builder.get_object('label_toggle').set_active(False) 243 244 client.core.get_enabled_plugins().addCallback(on_get_enabled_plugins) 245 if client.get_auth_level() == deluge.common.AUTH_LEVEL_ADMIN: 246 client.core.get_known_accounts().addCallback( 247 on_accounts, options.get('owner', client.get_auth_user()) 248 ).addErrback(on_accounts_failure) 249 else: 250 acc_iter = self.accounts.append() 251 self.accounts.set_value(acc_iter, 0, client.get_auth_user()) 252 self.builder.get_object('OwnerCombobox').set_active(0) 253 self.builder.get_object('OwnerCombobox').set_sensitive(False) 254 255 def set_sensitive(self): 256 maintoggles = [ 257 'download_location', 258 'append_extension', 259 'move_completed', 260 'label', 261 'max_download_speed', 262 'max_upload_speed', 263 'max_connections', 264 'max_upload_slots', 265 'add_paused', 266 'auto_managed', 267 'stop_at_ratio', 268 'queue_to_top', 269 'copy_torrent', 270 ] 271 for maintoggle in maintoggles: 272 self.on_toggle_toggled(self.builder.get_object(maintoggle + '_toggle')) 273 274 def on_toggle_toggled(self, tb): 275 toggle = tb.get_name().replace('_toggle', '') 276 isactive = tb.get_active() 277 if toggle == 'download_location': 278 self.builder.get_object('download_location_chooser').set_sensitive(isactive) 279 self.builder.get_object('download_location_entry').set_sensitive(isactive) 280 elif toggle == 'append_extension': 281 self.builder.get_object('append_extension').set_sensitive(isactive) 282 elif toggle == 'copy_torrent': 283 self.builder.get_object('copy_torrent_entry').set_sensitive(isactive) 284 self.builder.get_object('copy_torrent_chooser').set_sensitive(isactive) 285 self.builder.get_object('delete_copy_torrent_toggle').set_sensitive( 286 isactive 287 ) 288 elif toggle == 'move_completed': 289 self.builder.get_object('move_completed_path_chooser').set_sensitive( 290 isactive 291 ) 292 self.builder.get_object('move_completed_path_entry').set_sensitive(isactive) 293 self.builder.get_object('move_completed').set_active(isactive) 294 elif toggle == 'label': 295 self.builder.get_object('label').set_sensitive(isactive) 296 elif toggle == 'max_download_speed': 297 self.builder.get_object('max_download_speed').set_sensitive(isactive) 298 elif toggle == 'max_upload_speed': 299 self.builder.get_object('max_upload_speed').set_sensitive(isactive) 300 elif toggle == 'max_connections': 301 self.builder.get_object('max_connections').set_sensitive(isactive) 302 elif toggle == 'max_upload_slots': 303 self.builder.get_object('max_upload_slots').set_sensitive(isactive) 304 elif toggle == 'add_paused': 305 self.builder.get_object('add_paused').set_sensitive(isactive) 306 self.builder.get_object('isnt_add_paused').set_sensitive(isactive) 307 elif toggle == 'queue_to_top': 308 self.builder.get_object('queue_to_top').set_sensitive(isactive) 309 self.builder.get_object('isnt_queue_to_top').set_sensitive(isactive) 310 elif toggle == 'auto_managed': 311 self.builder.get_object('auto_managed').set_sensitive(isactive) 312 self.builder.get_object('isnt_auto_managed').set_sensitive(isactive) 313 elif toggle == 'stop_at_ratio': 314 self.builder.get_object('remove_at_ratio_toggle').set_active(isactive) 315 self.builder.get_object('stop_ratio_toggle').set_active(isactive) 316 self.builder.get_object('stop_at_ratio').set_active(isactive) 317 self.builder.get_object('stop_ratio').set_sensitive(isactive) 318 self.builder.get_object('remove_at_ratio').set_sensitive(isactive) 319 320 def on_apply(self, event=None): 321 try: 322 options = self.generate_opts() 323 client.autoadd.set_options(str(self.watchdir_id), options).addCallbacks( 324 self.on_added, self.on_error_show 325 ) 326 except IncompatibleOption as ex: 327 dialogs.ErrorDialog(_('Incompatible Option'), str(ex), self.dialog).run() 328 329 def on_error_show(self, result): 330 d = dialogs.ErrorDialog(_('Error'), result.value.exception_msg, self.dialog) 331 result.cleanFailure() 332 d.run() 333 334 def on_added(self, result): 335 self.dialog.destroy() 336 337 def on_add(self, event=None): 338 try: 339 options = self.generate_opts() 340 client.autoadd.add(options).addCallbacks(self.on_added, self.on_error_show) 341 except IncompatibleOption as ex: 342 dialogs.ErrorDialog(_('Incompatible Option'), str(ex), self.dialog).run() 343 344 def on_cancel(self, event=None): 345 self.dialog.destroy() 346 347 def generate_opts(self): 348 # generate options dict based on gtk objects 349 options = {} 350 options['enabled'] = self.builder.get_object('enabled').get_active() 351 if client.is_localhost(): 352 options['path'] = self.builder.get_object('path_chooser').get_filename() 353 options['download_location'] = self.builder.get_object( 354 'download_location_chooser' 355 ).get_filename() 356 options['move_completed_path'] = self.builder.get_object( 357 'move_completed_path_chooser' 358 ).get_filename() 359 options['copy_torrent'] = self.builder.get_object( 360 'copy_torrent_chooser' 361 ).get_filename() 362 else: 363 options['path'] = self.builder.get_object('path_entry').get_text() 364 options['download_location'] = self.builder.get_object( 365 'download_location_entry' 366 ).get_text() 367 options['move_completed_path'] = self.builder.get_object( 368 'move_completed_path_entry' 369 ).get_text() 370 options['copy_torrent'] = self.builder.get_object( 371 'copy_torrent_entry' 372 ).get_text() 373 374 options['label'] = ( 375 self.builder.get_object('label').get_child().get_text().lower() 376 ) 377 options['append_extension'] = self.builder.get_object( 378 'append_extension' 379 ).get_text() 380 options['owner'] = self.accounts[ 381 self.builder.get_object('OwnerCombobox').get_active() 382 ][0] 383 384 for key in [ 385 'append_extension_toggle', 386 'download_location_toggle', 387 'label_toggle', 388 'copy_torrent_toggle', 389 'delete_copy_torrent_toggle', 390 'seed_mode', 391 ]: 392 options[key] = self.builder.get_object(key).get_active() 393 394 for spin_id in self.spin_ids: 395 options[spin_id] = self.builder.get_object(spin_id).get_value() 396 options[spin_id + '_toggle'] = self.builder.get_object( 397 spin_id + '_toggle' 398 ).get_active() 399 for spin_int_id in self.spin_int_ids: 400 options[spin_int_id] = self.builder.get_object( 401 spin_int_id 402 ).get_value_as_int() 403 options[spin_int_id + '_toggle'] = self.builder.get_object( 404 spin_int_id + '_toggle' 405 ).get_active() 406 for chk_id in self.chk_ids: 407 options[chk_id] = self.builder.get_object(chk_id).get_active() 408 options[chk_id + '_toggle'] = self.builder.get_object( 409 chk_id + '_toggle' 410 ).get_active() 411 412 if ( 413 options['copy_torrent_toggle'] 414 and options['path'] == options['copy_torrent'] 415 ): 416 raise IncompatibleOption( 417 _( 418 '"Watch Folder" directory and "Copy of .torrent' 419 ' files to" directory cannot be the same!' 420 ) 421 ) 422 return options 423 424 425class GtkUI(Gtk3PluginBase): 426 def enable(self): 427 self.builder = Gtk.Builder() 428 self.builder.add_from_file(get_resource('config.ui')) 429 self.builder.connect_signals(self) 430 self.opts_dialog = OptionsDialog() 431 432 component.get('PluginManager').register_hook( 433 'on_apply_prefs', self.on_apply_prefs 434 ) 435 component.get('PluginManager').register_hook( 436 'on_show_prefs', self.on_show_prefs 437 ) 438 client.register_event_handler( 439 'AutoaddOptionsChangedEvent', self.on_options_changed_event 440 ) 441 442 self.watchdirs = {} 443 444 vbox = self.builder.get_object('watchdirs_vbox') 445 sw = Gtk.ScrolledWindow() 446 sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN) 447 sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) 448 449 vbox.pack_start(sw, True, True, 0) 450 451 self.store = self.create_model() 452 453 self.treeView = Gtk.TreeView(self.store) 454 self.treeView.connect('cursor-changed', self.on_listitem_activated) 455 self.treeView.connect('row-activated', self.on_edit_button_clicked) 456 self.treeView.set_rules_hint(True) 457 458 self.create_columns(self.treeView) 459 sw.add(self.treeView) 460 sw.show_all() 461 component.get('Preferences').add_page( 462 _('AutoAdd'), self.builder.get_object('prefs_box') 463 ) 464 465 def disable(self): 466 component.get('Preferences').remove_page(_('AutoAdd')) 467 component.get('PluginManager').deregister_hook( 468 'on_apply_prefs', self.on_apply_prefs 469 ) 470 component.get('PluginManager').deregister_hook( 471 'on_show_prefs', self.on_show_prefs 472 ) 473 474 def create_model(self): 475 store = Gtk.ListStore(str, bool, str, str) 476 for watchdir_id, watchdir in self.watchdirs.items(): 477 store.append( 478 [ 479 watchdir_id, 480 watchdir['enabled'], 481 watchdir.get('owner', 'localclient'), 482 watchdir['path'], 483 ] 484 ) 485 return store 486 487 def create_columns(self, treeview): 488 renderer_toggle = Gtk.CellRendererToggle() 489 column = Gtk.TreeViewColumn( 490 _('Active'), renderer_toggle, activatable=1, active=1 491 ) 492 column.set_sort_column_id(1) 493 treeview.append_column(column) 494 tt = Gtk.Tooltip() 495 tt.set_text(_('Double-click to toggle')) 496 treeview.set_tooltip_cell(tt, None, None, renderer_toggle) 497 498 renderertext = Gtk.CellRendererText() 499 column = Gtk.TreeViewColumn(_('Owner'), renderertext, text=2) 500 column.set_sort_column_id(2) 501 treeview.append_column(column) 502 tt2 = Gtk.Tooltip() 503 tt2.set_text(_('Double-click to edit')) 504 treeview.set_has_tooltip(True) 505 506 renderertext = Gtk.CellRendererText() 507 column = Gtk.TreeViewColumn(_('Path'), renderertext, text=3) 508 column.set_sort_column_id(3) 509 treeview.append_column(column) 510 tt2 = Gtk.Tooltip() 511 tt2.set_text(_('Double-click to edit')) 512 treeview.set_has_tooltip(True) 513 514 def load_watchdir_list(self): 515 pass 516 517 def add_watchdir_entry(self): 518 pass 519 520 def on_add_button_clicked(self, event=None): 521 # display options_window 522 self.opts_dialog.show() 523 524 def on_remove_button_clicked(self, event=None): 525 tree, tree_id = self.treeView.get_selection().get_selected() 526 watchdir_id = str(self.store.get_value(tree_id, 0)) 527 if watchdir_id: 528 client.autoadd.remove(watchdir_id) 529 530 def on_edit_button_clicked(self, event=None, a=None, col=None): 531 tree, tree_id = self.treeView.get_selection().get_selected() 532 watchdir_id = str(self.store.get_value(tree_id, 0)) 533 if watchdir_id: 534 if col and col.get_title() == _('Active'): 535 if self.watchdirs[watchdir_id]['enabled']: 536 client.autoadd.disable_watchdir(watchdir_id) 537 else: 538 client.autoadd.enable_watchdir(watchdir_id) 539 else: 540 self.opts_dialog.show(self.watchdirs[watchdir_id], watchdir_id) 541 542 def on_listitem_activated(self, treeview): 543 tree, tree_id = self.treeView.get_selection().get_selected() 544 if tree_id: 545 self.builder.get_object('edit_button').set_sensitive(True) 546 self.builder.get_object('remove_button').set_sensitive(True) 547 else: 548 self.builder.get_object('edit_button').set_sensitive(False) 549 self.builder.get_object('remove_button').set_sensitive(False) 550 551 def on_apply_prefs(self): 552 log.debug('applying prefs for AutoAdd') 553 for watchdir_id, watchdir in self.watchdirs.items(): 554 client.autoadd.set_options(watchdir_id, watchdir) 555 556 def on_show_prefs(self): 557 client.autoadd.get_watchdirs().addCallback(self.cb_get_config) 558 559 def on_options_changed_event(self): 560 client.autoadd.get_watchdirs().addCallback(self.cb_get_config) 561 562 def cb_get_config(self, watchdirs): 563 """callback for on show_prefs""" 564 log.trace('Got whatchdirs from core: %s', watchdirs) 565 self.watchdirs = watchdirs or {} 566 self.store.clear() 567 for watchdir_id, watchdir in self.watchdirs.items(): 568 self.store.append( 569 [ 570 watchdir_id, 571 watchdir['enabled'], 572 watchdir.get('owner', 'localclient'), 573 watchdir['path'], 574 ] 575 ) 576 # Workaround for cached glade signal appearing when re-enabling plugin in same session 577 if self.builder.get_object('edit_button'): 578 # Disable the remove and edit buttons, because nothing in the store is selected 579 self.builder.get_object('remove_button').set_sensitive(False) 580 self.builder.get_object('edit_button').set_sensitive(False) 581