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