flexget.plugins.plugin_deluge
Covered: 278 lines
Missed: 339 lines
Skipped 146 lines
Percent: 45 %
  1
import logging
  2
import time
  3
import os
  4
import base64
  5
import re
  6
import sys
  7
from flexget.event import event
  8
from flexget.entry import Entry
  9
from flexget.utils.tools import make_valid_path
 10
from flexget.plugin import register_plugin, PluginError, priority, get_plugin_by_name, DependencyError
 11
from flexget.utils.template import RenderError
 13
log = logging.getLogger('deluge')
 16
if sys.platform.startswith('win') and os.environ.get('ProgramFiles'):
 17
    deluge_dir = os.path.join(os.environ['ProgramFiles'], 'Deluge')
 18
    log.debug('Looking for deluge install in %s' % deluge_dir)
 19
    if os.path.isdir(deluge_dir):
 20
        log.debug('Found deluge install in %s adding to sys.path' % deluge_dir)
 21
        sys.path.append(deluge_dir)
 22
        for item in os.listdir(deluge_dir):
 23
            if item.endswith(('.egg', '.zip')):
 24
                sys.path.append(os.path.join(deluge_dir, item))
 26
try:
 27
    from twisted.python import log as twisted_log
 28
    from twisted.internet.main import installReactor
 29
    from twisted.internet.selectreactor import SelectReactor
 31
    class PausingReactor(SelectReactor):
 32
        """A SelectReactor that can be paused and resumed."""
 34
        def __init__(self):
 35
            SelectReactor.__init__(self)
 36
            self.paused = False
 37
            self._return_value = None
 38
            self._release_requested = False
 39
            self._mainLoopGen = None
 42
            if not hasattr(self, '_started'):
 43
                PausingReactor._started = property(lambda self: self.running)
 45
        def _mainLoopGenerator(self):
 46
            """Generator that acts as mainLoop, but yields when requested."""
 47
            while self._started:
 48
                try:
 49
                    while self._started:
 50
                        if self._release_requested:
 51
                            self._release_requested = False
 52
                            self.paused = True
 53
                            yield self._return_value
 54
                            self.paused = False
 55
                        self.iterate()
 56
                except KeyboardInterrupt:
 58
                    self.pause()
 59
                except GeneratorExit:
 61
                    log.debug('Got GeneratorExit, stopping reactor.', exc_info=True)
 62
                    self.paused = False
 63
                    self.stop()
 64
                except:
 65
                    twisted_log.msg("Unexpected error in main loop.")
 66
                    twisted_log.err()
 67
                else:
 68
                    twisted_log.msg('Main loop terminated.')
 70
        def run(self, installSignalHandlers=False):
 71
            """Starts or resumes the reactor."""
 72
            if not self._started:
 73
                self.startRunning(installSignalHandlers)
 74
                self._mainLoopGen = self._mainLoopGenerator()
 75
            try:
 76
                return self._mainLoopGen.next()
 77
            except StopIteration:
 78
                pass
 80
        def pause(self, return_value=None):
 81
            """Causes reactor to pause after this iteration.
 82
            If :return_value: is specified, it will be returned by the reactor.run call."""
 83
            self._return_value = return_value
 84
            self._release_requested = True
 86
        def stop(self):
 87
            """Stops the reactor."""
 88
            SelectReactor.stop(self)
 90
            if self.paused:
 91
                self.run()
 94
            self.addSystemEventTrigger('during', 'shutdown', self.crash)
 95
            self.addSystemEventTrigger('during', 'shutdown', self.disconnectAll)
 98
    installReactor(PausingReactor())
100
except ImportError:
102
    pass
106
class DelugePlugin(object):
107
    """Base class for deluge plugins, contains settings and methods for connecting to a deluge daemon."""
109
    def validate_connection_info(self, dict_validator):
110
        dict_validator.accept('text', key='host')
111
        dict_validator.accept('integer', key='port')
112
        dict_validator.accept('text', key='user')
113
        dict_validator.accept('text', key='pass')
115
    def prepare_connection_info(self, config):
116
        config.setdefault('host', 'localhost')
117
        config.setdefault('port', 58846)
118
        config.setdefault('user', '')
119
        config.setdefault('pass', '')
121
    def on_process_start(self, feed, config):
122
        """Raise a DependencyError if our dependencies aren't available"""
124
        try:
125
            from deluge.ui.client import client
126
        except ImportError, e:
127
            log.debug('Error importing deluge: %s' % e)
128
            raise DependencyError('output_deluge', 'deluge', 'Deluge module and it\'s dependencies required. ImportError: %s' % e, log)
129
        try:
130
            from twisted.internet import reactor
131
        except:
132
            raise DependencyError('output_deluge', 'twisted.internet', 'Twisted.internet package required', log)
133
        log.debug('Using deluge 1.2 api')
135
    def on_feed_abort(self, feed, config):
136
        pass
139
try:
140
    from twisted.internet import reactor
141
    from deluge.ui.client import client
142
    from deluge.ui.common import get_localhost_auth
144
    class DelugePlugin(DelugePlugin):
146
        def on_disconnect(self):
147
            """Pauses the reactor. Gets called when we disconnect from the daemon."""
149
            reactor.callLater(0, reactor.pause)
151
        def on_connect_fail(self, result):
152
            """Pauses the reactor, returns PluginError. Gets called when connection to deluge daemon fails."""
153
            log.debug('Connect to deluge daemon failed, result: %s' % result)
154
            reactor.callLater(0, reactor.pause, PluginError('Could not connect to deluge daemon', log))
156
        def on_connect_success(self, result, feed, config):
157
            """Gets called when successfully connected to the daemon. Should do the work then call client.disconnect"""
158
            raise NotImplementedError
160
        def connect(self, feed, config):
161
            """Connects to the deluge daemon and runs on_connect_success """
163
            if config['host'] in ['localhost', '127.0.0.1'] and not config.get('user'):
165
                auth = get_localhost_auth()
166
                if auth[0]:
167
                    config['user'], config['pass'] = auth
168
                else:
169
                    raise PluginError('Unable to get local authentication info for Deluge. '
170
                                      'You may need to specify an username and password from your Deluge auth file.')
172
            client.set_disconnect_callback(self.on_disconnect)
174
            d = client.connect(
175
                host=config['host'],
176
                port=config['port'],
177
                username=config['user'],
178
                password=config['pass'])
180
            d.addCallback(self.on_connect_success, feed, config).addErrback(self.on_connect_fail)
181
            result = reactor.run()
182
            if isinstance(result, Exception):
183
                raise result
184
            return result
186
    @event('manager.shutdown')
187
    def stop_reactor(manager):
188
        """Shut down the twisted reactor after all feeds have run."""
189
        if not reactor._stopped:
190
            log.debug('Stopping twisted reactor.')
191
            reactor.stop()
193
except ImportError:
194
    pass
197
class InputDeluge(DelugePlugin):
198
    """Create entries for torrents in the deluge session."""
200
    settings_map = {
201
        'name': 'title',
202
        'hash': 'torrent_info_hash',
203
        'num_peers': 'torrent_peers',
204
        'num_seeds': 'torrent_seeds',
205
        'progress': 'deluge_progress',
206
        'private': 'deluge_private',
207
        'state': 'deluge_state',
208
        'eta': 'deluge_eta',
209
        'ratio': 'deluge_ratio',
210
        'move_on_completed_path': 'deluge_movedone',
211
        'save_path': 'deluge_path',
212
        'label': 'deluge_label',
213
        'total_size': ('content_size', lambda size: size / 1024 / 1024),
214
        'files': ('content_files', lambda file_dicts: [os.path.basename(f['path']) for f in file_dicts])}
216
    def __init__(self):
217
        self.entries = []
219
    def validator(self):
220
        from flexget import validator
221
        root = validator.factory()
222
        root.accept('boolean')
223
        advanced = root.accept('dict')
224
        advanced.accept('path', key='config_path')
225
        self.validate_connection_info(advanced)
226
        filter = advanced.accept('dict', key='filter')
227
        filter.accept('text', key='label')
228
        filter.accept('choice', key='state').accept_choices(
229
            ['active', 'downloading', 'seeding', 'queued', 'paused'], ignore_case=True)
230
        return root
232
    def prepare_config(self, config):
233
        if isinstance(config, bool):
234
            config = {}
235
        if 'filter' in config:
236
            filter = config['filter']
237
            if 'label' in filter:
238
                filter['label'] = filter['label'].lower()
239
            if 'state' in filter:
240
                filter['state'] = filter['state'].capitalize()
241
        self.prepare_connection_info(config)
242
        return config
244
    def on_feed_input(self, feed, config):
245
        """Generates and returns a list of entries from the deluge daemon."""
247
        self.entries = []
249
        self.connect(feed, self.prepare_config(config))
250
        return self.entries
252
    def on_connect_success(self, result, feed, config):
253
        """Creates a list of FlexGet entries from items loaded in deluge and stores them to self.entries"""
254
        from deluge.ui.client import client
256
        def on_get_torrents_status(torrents):
257
            config_path = os.path.expanduser(config.get('config_path', ''))
258
            for hash, torrent_dict in torrents.iteritems():
260
                entry = Entry(deluge_id=hash, url='')
261
                if config_path:
262
                    torrent_path = os.path.join(config_path, 'state', hash + '.torrent')
263
                    if os.path.isfile(torrent_path):
264
                        entry['location'] = torrent_path
265
                        if not torrent_path.startswith('/'):
266
                            torrent_path = '/' + torrent_path
267
                        entry['url'] = 'file://' + torrent_path
268
                    else:
269
                        log.warning('Did not find torrent file at %s' % torrent_path)
270
                for key, value in torrent_dict.iteritems():
271
                    flexget_key = self.settings_map[key]
272
                    if isinstance(flexget_key, tuple):
273
                        flexget_key, format_func = flexget_key
274
                        value = format_func(value)
275
                    entry[flexget_key] = value
276
                self.entries.append(entry)
277
            client.disconnect()
278
        filter = config.get('filter', {})
279
        client.core.get_torrents_status(filter, self.settings_map.keys()).addCallback(on_get_torrents_status)
282
class OutputDeluge(DelugePlugin):
283
    """Add the torrents directly to deluge, supporting custom save paths."""
285
    def validator(self):
286
        from flexget import validator
287
        root = validator.factory()
288
        root.accept('boolean')
289
        deluge = root.accept('dict')
290
        self.validate_connection_info(deluge)
291
        deluge.accept('path', key='path', allow_replacement=True)
292
        deluge.accept('path', key='movedone', allow_replacement=True)
293
        deluge.accept('text', key='label')
294
        deluge.accept('boolean', key='queuetotop')
295
        deluge.accept('boolean', key='automanaged')
296
        deluge.accept('number', key='maxupspeed')
297
        deluge.accept('number', key='maxdownspeed')
298
        deluge.accept('integer', key='maxconnections')
299
        deluge.accept('integer', key='maxupslots')
300
        deluge.accept('number', key='ratio')
301
        deluge.accept('boolean', key='removeatratio')
302
        deluge.accept('boolean', key='addpaused')
303
        deluge.accept('boolean', key='compact')
304
        deluge.accept('text', key='content_filename')
305
        deluge.accept('boolean', key='main_file_only')
306
        deluge.accept('boolean', key='enabled')
307
        return root
309
    def prepare_config(self, config):
310
        if isinstance(config, bool):
311
            config = {'enabled': config}
312
        self.prepare_connection_info(config)
313
        config.setdefault('enabled', True)
314
        config.setdefault('path', '')
315
        config.setdefault('movedone', '')
316
        config.setdefault('label', '')
317
        return config
319
    def __init__(self):
320
        self.deluge12 = None
321
        self.deluge_version = None
322
        self.options = {'maxupspeed': 'max_upload_speed', 'maxdownspeed': 'max_download_speed', \
323
            'maxconnections': 'max_connections', 'maxupslots': 'max_upload_slots', \
324
            'automanaged': 'auto_managed', 'ratio': 'stop_ratio', 'removeatratio': 'remove_at_ratio', \
325
            'addpaused': 'add_paused', 'compact': 'compact_allocation'}
327
    @priority(120)
328
    def on_process_start(self, feed, config):
329
        """Register the usable set: keywords. Detect what version of deluge is loaded."""
330
        set_plugin = get_plugin_by_name('set')
331
        set_plugin.instance.register_keys({'path': 'text', 'movedone': 'text', \
332
            'queuetotop': 'boolean', 'label': 'text', 'automanaged': 'boolean', \
333
            'maxupspeed': 'number', 'maxdownspeed': 'number', 'maxupslots': 'integer', \
334
            'maxconnections': 'integer', 'ratio': 'number', 'removeatratio': 'boolean', \
335
            'addpaused': 'boolean', 'compact': 'boolean', 'content_filename': 'text', 'main_file_only': 'boolean'})
336
        if self.deluge12 is None:
337
            logger = log.info if feed.manager.options.test else log.debug
338
            try:
339
                log.debug('Looking for deluge 1.1 API')
340
                from deluge.ui.client import sclient
341
                log.debug('1.1 API found')
342
            except ImportError:
343
                log.debug('Looking for deluge 1.2 API')
344
                DelugePlugin.on_process_start(self, feed, config)
345
                logger('Using deluge 1.2 api')
346
                self.deluge12 = True
347
            else:
348
                logger('Using deluge 1.1 api')
349
                self.deluge12 = False
351
    @priority(120)
352
    def on_feed_download(self, feed, config):
353
        """
354
            call download plugin to generate the temp files we will load into deluge
355
            then verify they are valid torrents
356
        """
357
        import deluge.ui.common
358
        config = self.prepare_config(config)
359
        if not config['enabled']:
360
            return
362
        if not 'download' in feed.config:
363
            download = get_plugin_by_name('download')
364
            for entry in feed.accepted:
365
                if not entry.get('deluge_id'):
366
                    download.instance.get_temp_file(feed, entry, handle_magnets=True)
369
        for entry in feed.accepted:
370
            if os.path.exists(entry.get('file', '')):
372
                try:
373
                    deluge.ui.common.TorrentInfo(entry['file'])
374
                except Exception:
375
                    feed.fail(entry, 'Invalid torrent file')
376
                    log.error('Torrent file appears invalid for: %s', entry['title'])
378
    @priority(135)
379
    def on_feed_output(self, feed, config):
380
        """Add torrents to deluge at exit."""
381
        config = self.prepare_config(config)
383
        if feed.manager.options.learn:
384
            return
385
        if not config['enabled'] or not (feed.accepted or feed.manager.options.test):
386
            return
388
        add_to_deluge = self.connect if self.deluge12 else self.add_to_deluge11
389
        add_to_deluge(feed, config)
391
        if not 'download' in feed.config:
392
            for entry in feed.accepted + feed.failed:
393
                if os.path.exists(entry.get('file', '')):
394
                    os.remove(entry['file'])
395
                    del(entry['file'])
397
    def add_to_deluge11(self, feed, config):
398
        """Add torrents to deluge using deluge 1.1.x api."""
399
        try:
400
            from deluge.ui.client import sclient
401
        except:
402
            raise PluginError('Deluge module required', log)
404
        sclient.set_core_uri()
405
        for entry in feed.accepted:
406
            try:
407
                before = sclient.get_session_state()
408
            except Exception, (errno, msg):
409
                raise PluginError('Could not communicate with deluge core. %s' % msg, log)
410
            if feed.manager.options.test:
411
                return
412
            opts = {}
413
            path = entry.get('path', config['path'])
414
            if path:
415
                try:
416
                    opts['download_location'] = os.path.expanduser(entry.render(path))
417
                except RenderError, e:
418
                    log.error('Could not set path for %s: %s' % (entry['title'], e))
419
            for fopt, dopt in self.options.iteritems():
420
                value = entry.get(fopt, config.get(fopt))
421
                if value is not None:
422
                    opts[dopt] = value
423
                    if fopt == 'ratio':
424
                        opts['stop_at_ratio'] = True
427
            if not 'file' in entry:
428
                feed.fail(entry, 'file missing?')
429
                continue
432
            if not os.path.exists(entry['file']):
433
                tmp_path = os.path.join(feed.manager.config_base, 'temp')
434
                log.debug('entry: %s' % entry)
435
                log.debug('temp: %s' % ', '.join(os.listdir(tmp_path)))
436
                feed.fail(entry, 'Downloaded temp file \'%s\' doesn\'t exist!?' % entry['file'])
437
                continue
439
            sclient.add_torrent_file([entry['file']], [opts])
440
            log.info('%s torrent added to deluge with options %s' % (entry['title'], opts))
442
            movedone = entry.get('movedone', config['movedone'])
443
            label = entry.get('label', config['label']).lower()
444
            queuetotop = entry.get('queuetotop', config.get('queuetotop'))
447
            time.sleep(2)
448
            after = sclient.get_session_state()
449
            for item in after:
451
                if not item in before:
452
                    try:
453
                        movedone = entry.render(movedone)
454
                    except RenderError, e:
455
                        log.error('Could not set movedone for %s: %s' % (entry['title'], e))
456
                        movedone = ''
457
                    if movedone:
458
                        movedone = os.path.expanduser(movedone)
459
                        if not os.path.isdir(movedone):
460
                            log.debug('movedone path %s doesn\'t exist, creating' % movedone)
461
                            os.makedirs(movedone)
462
                        log.debug('%s move on complete set to %s' % (entry['title'], movedone))
463
                        sclient.set_torrent_move_on_completed(item, True)
464
                        sclient.set_torrent_move_on_completed_path(item, movedone)
465
                    if label:
466
                        if not 'label' in sclient.get_enabled_plugins():
467
                            sclient.enable_plugin('label')
468
                        if not label in sclient.label_get_labels():
469
                            sclient.label_add(label)
470
                        log.debug('%s label set to \'%s\'' % (entry['title'], label))
471
                        sclient.label_set_torrent(item, label)
472
                    if queuetotop:
473
                        log.debug('%s moved to top of queue' % entry['title'])
474
                        sclient.queue_top([item])
475
                    break
476
            else:
477
                log.info('%s is already loaded in deluge. Cannot change label, movedone, or queuetotop' % entry['title'])
479
    def on_connect_success(self, result, feed, config):
480
        """Gets called when successfully connected to a daemon."""
481
        from deluge.ui.client import client
482
        from twisted.internet import reactor, defer
485
        if not result:
486
            log.debug('on_connect_success returned a failed result. BUG?')
488
        if feed.manager.options.test:
489
            log.debug('Test connection to deluge daemon successful.')
490
            client.disconnect()
491
            return
493
        def format_label(label):
494
            """Makes a string compliant with deluge label naming rules"""
495
            return re.sub('[^\w-]+', '_', label.lower())
497
        def set_torrent_options(torrent_id, entry, opts):
498
            """Gets called when a torrent was added to the daemon."""
499
            dlist = []
500
            if not torrent_id:
501
                log.error('There was an error adding %s to deluge.' % entry['title'])
503
                return
504
            log.info('%s successfully added to deluge.' % entry['title'])
505
            entry['deluge_id'] = torrent_id
507
            def create_path(result, path):
508
                """Creates the specified path if deluge is older than 1.3"""
509
                from deluge.common import VersionSplit
511
                if VersionSplit('1.3.0') > VersionSplit(self.deluge_version):
512
                    if client.is_localhost():
513
                        if not os.path.isdir(path):
514
                            log.debug('path %s doesn\'t exist, creating' % path)
515
                            os.makedirs(path)
516
                    else:
517
                        log.warning('If path does not exist on the machine running the daemon, move will fail.')
519
            if opts.get('movedone'):
520
                dlist.append(version_deferred.addCallback(create_path, opts['movedone']))
521
                dlist.append(client.core.set_torrent_move_completed(torrent_id, True))
522
                dlist.append(client.core.set_torrent_move_completed_path(torrent_id, opts['movedone']))
523
                log.debug('%s move on complete set to %s' % (entry['title'], opts['movedone']))
524
            if opts.get('label'):
526
                def apply_label(result, torrent_id, label):
527
                    """Gets called after labels and torrent were added to deluge."""
528
                    return client.label.set_torrent(torrent_id, label)
530
                dlist.append(label_deferred.addCallback(apply_label, torrent_id, opts['label']))
531
            if opts.get('queuetotop') is not None:
532
                if opts['queuetotop']:
533
                    dlist.append(client.core.queue_top([torrent_id]))
534
                    log.debug('%s moved to top of queue' % entry['title'])
535
                else:
536
                    dlist.append(client.core.queue_bottom([torrent_id]))
537
                    log.debug('%s moved to bottom of queue' % entry['title'])
539
            def on_get_torrent_status(status):
540
                """Gets called with torrent status, including file info.
541
                Sets the torrent options which require knowledge of the current status of the torrent."""
543
                main_file_dlist = []
546
                move_now_path = None
547
                if opts.get('movedone'):
548
                    if status['progress'] == 100:
549
                        move_now_path = opts['movedone']
550
                    else:
553
                        log.debug('Not moving storage for %s, as this will prevent movedone.' % entry['title'])
554
                elif opts.get('path'):
555
                    move_now_path = opts['path']
557
                if move_now_path and os.path.normpath(move_now_path) != os.path.normpath(status['save_path']):
558
                    main_file_dlist.append(version_deferred.addCallback(create_path, move_now_path))
559
                    log.debug('Moving storage for %s to %s' % (entry['title'], move_now_path))
560
                    main_file_dlist.append(client.core.move_storage([torrent_id], move_now_path))
562
                if opts.get('content_filename') or opts.get('main_file_only'):
564
                    def file_exists():
566
                        if os.path.exists(os.path.join(status['save_path'], filename)):
567
                            return True
568
                        elif status.get('move_on_completed') and status.get('move_on_completed_path'):
569
                            if os.path.exists(os.path.join(status['move_on_completed_path'], filename)):
570
                                return True
571
                        else:
572
                            return False
574
                    for file in status['files']:
576
                        if file['size'] > (status['total_size'] * 0.9):
577
                            if opts.get('content_filename'):
578
                                filename = opts['content_filename'] + os.path.splitext(file['path'])[1]
579
                                counter = 1
580
                                if client.is_localhost():
581
                                    while file_exists():
583
                                        filename = ''.join([opts['content_filename'], '(', str(counter), ')', os.path.splitext(file['path'])[1]])
584
                                        counter += 1
585
                                else:
586
                                    log.debug('Cannot ensure content_filename is unique when adding to a remote deluge daemon.')
587
                                log.debug('File %s in %s renamed to %s' % (file['path'], entry['title'], filename))
588
                                main_file_dlist.append(client.core.rename_files(torrent_id, [(file['index'], filename)]))
589
                            if opts.get('main_file_only'):
590
                                file_priorities = [1 if f['index'] == file['index'] else 0 for f in status['files']]
591
                                main_file_dlist.append(client.core.set_torrent_file_priorities(torrent_id, file_priorities))
592
                            break
593
                    else:
594
                        log.warning('No files in %s are > 90%% of content size, no files renamed.' % entry['title'])
596
                return defer.DeferredList(main_file_dlist)
598
            status_keys = ['files', 'total_size', 'save_path', 'move_on_completed_path', 'move_on_completed', 'progress']
599
            dlist.append(client.core.get_torrent_status(torrent_id, status_keys).addCallback(on_get_torrent_status))
601
            return defer.DeferredList(dlist)
603
        def on_fail(result, feed, entry):
604
            """Gets called when daemon reports a failure adding the torrent."""
605
            log.info('%s was not added to deluge! %s' % (entry['title'], result))
606
            feed.fail(entry, 'Could not be added to deluge')
609
        dlist = []
611
        labels = set([format_label(entry['label']) for entry in feed.accepted if entry.get('label')])
612
        if config.get('label'):
613
            labels.add(format_label(config['label']))
614
        label_deferred = defer.succeed(True)
615
        if labels:
618
            def on_get_enabled_plugins(plugins):
619
                """Gets called with the list of enabled deluge plugins."""
621
                def on_label_enabled(result):
622
                    """ This runs when we verify the label plugin is enabled. """
624
                    def on_get_labels(d_labels):
625
                        """Gets available labels from deluge, and adds any new labels we need."""
626
                        dlist = []
627
                        for label in labels:
628
                            if not label in d_labels:
629
                                log.debug('Adding the label %s to deluge' % label)
630
                                dlist.append(client.label.add(label))
631
                        return defer.DeferredList(dlist)
633
                    return client.label.get_labels().addCallback(on_get_labels)
635
                if 'Label' in plugins:
636
                    return on_label_enabled(True)
637
                else:
640
                    def on_get_available_plugins(plugins):
641
                        """Gets plugins available to deluge, enables Label plugin if available."""
642
                        if 'Label' in plugins:
643
                            log.debug('Enabling label plugin in deluge')
644
                            return client.core.enable_plugin('Label').addCallback(on_label_enabled)
645
                        else:
646
                            log.error('Label plugin is not installed in deluge')
648
                    return client.core.get_available_plugins().addCallback(on_get_available_plugins)
650
            label_deferred = client.core.get_enabled_plugins().addCallback(on_get_enabled_plugins)
651
            dlist.append(label_deferred)
653
        def on_get_daemon_info(ver):
654
            """Gets called with the daemon version info, stores it in self."""
655
            log.debug('deluge version %s' % ver)
656
            self.deluge_version = ver
658
        version_deferred = client.daemon.info().addCallback(on_get_daemon_info)
659
        dlist.append(version_deferred)
661
        def on_get_session_state(torrent_ids):
662
            """Gets called with a list of torrent_ids loaded in the deluge session.
663
            Adds new torrents and modifies the settings for ones already in the session."""
664
            dlist = []
666
            for entry in feed.accepted:
668
                def add_entry(entry, opts):
669
                    """Adds an entry to the deluge session"""
670
                    magnet, filedump = None, None
671
                    if entry.get('url', '').startswith('magnet:'):
672
                        magnet = entry['url']
673
                    else:
674
                        if not os.path.exists(entry['file']):
675
                            feed.fail(entry, 'Downloaded temp file \'%s\' doesn\'t exist!' % entry['file'])
676
                            del(entry['file'])
677
                            return
678
                        try:
679
                            f = open(entry['file'], 'rb')
680
                            filedump = base64.encodestring(f.read())
681
                        finally:
682
                            f.close()
684
                    log.verbose('Adding %s to deluge.' % entry['title'])
685
                    if magnet:
686
                        return client.core.add_torrent_magnet(magnet, opts)
687
                    else:
688
                        return client.core.add_torrent_file(entry['title'], filedump, opts)
691
                add_opts = {}
692
                try:
693
                    path = entry.render(entry.get('path', config['path']))
694
                    if path:
695
                        add_opts['download_location'] = make_valid_path(os.path.expanduser(path))
696
                except RenderError, e:
697
                    log.error('Could not set path for %s: %s' % (entry['title'], e))
698
                for fopt, dopt in self.options.iteritems():
699
                    value = entry.get(fopt, config.get(fopt))
700
                    if value is not None:
701
                        add_opts[dopt] = value
702
                        if fopt == 'ratio':
703
                            add_opts['stop_at_ratio'] = True
705
                modify_opts = {'label': format_label(entry.get('label', config['label'])),
706
                               'queuetotop': entry.get('queuetotop', config.get('queuetotop')),
707
                               'main_file_only': entry.get('main_file_only', config.get('main_file_only', False))}
708
                try:
709
                    movedone = entry.render(entry.get('movedone', config['movedone']))
710
                    modify_opts['movedone'] = make_valid_path(os.path.expanduser(movedone))
711
                except RenderError, e:
712
                    log.error('Error setting movedone for %s: %s' % (entry['title'], e))
713
                try:
714
                    modify_opts['content_filename'] = entry.render(entry.get('content_filename', config.get('content_filename', '')))
715
                except RenderError, e:
716
                    log.error('Error setting content_filename for %s: %s' % (entry['title'], e))
718
                torrent_id = entry.get('deluge_id') or entry.get('torrent_info_hash')
719
                torrent_id = torrent_id and torrent_id.lower()
720
                if torrent_id in torrent_ids:
721
                    log.info('%s is already loaded in deluge, setting options' % entry['title'])
724
                    modify_opts['path'] = add_opts.pop('download_location', None)
725
                    dlist.extend([set_torrent_options(torrent_id, entry, modify_opts),
726
                                  client.core.set_torrent_options([torrent_id], add_opts)])
727
                else:
728
                    dlist.append(add_entry(entry, add_opts).addCallbacks(set_torrent_options, on_fail,
729
                            callbackArgs=(entry, modify_opts), errbackArgs=(feed, entry)))
730
            return defer.DeferredList(dlist)
731
        dlist.append(client.core.get_session_state().addCallback(on_get_session_state))
733
        def on_complete(result):
734
            """Gets called when all of our tasks for deluge daemon are complete."""
735
            client.disconnect()
736
        tasks = defer.DeferredList(dlist).addBoth(on_complete)
738
        def on_timeout(result):
739
            """Gets called if tasks have not completed in 30 seconds.
740
            Should only happen when something goes wrong."""
741
            log.error('Timed out while adding torrents to deluge.')
742
            log.debug('dlist: %s' % result.resultList)
743
            client.disconnect()
746
        reactor.callLater(30, lambda: tasks.called or on_timeout(tasks))
748
    def on_feed_exit(self, feed, config):
749
        """Make sure all temp files are cleaned up when feed exits"""
751
        if not 'download' in feed.config:
752
            download = get_plugin_by_name('download')
753
            download.instance.cleanup_temp_files(feed)
755
    def on_feed_abort(self, feed, config):
756
        """Make sure normal cleanup tasks still happen on abort."""
757
        DelugePlugin.on_feed_abort(self, feed, config)
758
        self.on_feed_exit(feed, config)
761
register_plugin(InputDeluge, 'from_deluge', api_ver=2)
762
register_plugin(OutputDeluge, 'deluge', api_ver=2)