flexget.plugins.plugin_transmission
Covered: 110 lines
Missed: 200 lines
Skipped 66 lines
Percent: 35 %
  1
import os
  2
from netrc import netrc, NetrcParseError
  3
import logging
  4
import base64
  5
from flexget.plugin import register_plugin, priority, get_plugin_by_name, PluginError
  6
from flexget import validator
  7
from flexget.entry import Entry
  8
from flexget.utils.template import RenderError
 10
log = logging.getLogger('transmission')
 13
def save_opener(f):
 14
    """
 15
        Transmissionrpc sets a new default opener for urllib2
 16
        We use this as a decorator to capture and restore it when needed
 17
    """
 19
    def new_f(self, *args, **kwargs):
 20
        import urllib2
 21
        prev_opener = urllib2._opener
 22
        urllib2.install_opener(self.opener)
 23
        try:
 24
            f(self, *args, **kwargs)
 25
            self.opener = urllib2._opener
 26
        finally:
 27
            urllib2.install_opener(prev_opener)
 28
    return new_f
 31
class TransmissionBase(object):
 33
    def __init__(self):
 34
        self.client = None
 35
        self.opener = None
 37
    def _validator(self, advanced):
 38
        """Return config validator"""
 39
        advanced.accept('text', key='host')
 40
        advanced.accept('integer', key='port')
 42
        advanced.accept('file', key='netrc')
 43
        advanced.accept('text', key='username')
 44
        advanced.accept('text', key='password')
 45
        advanced.accept('boolean', key='enabled')
 46
        return advanced
 48
    def prepare_config(self, config):
 49
        if isinstance(config, bool):
 50
            config = {'enabled': config}
 51
        config.setdefault('enabled', True)
 52
        config.setdefault('host', 'localhost')
 53
        config.setdefault('port', 9091)
 54
        return config
 56
    def create_rpc_client(self, config):
 57
        import transmissionrpc
 58
        from transmissionrpc import TransmissionError
 59
        from transmissionrpc import HTTPHandlerError
 61
        user, password = None, None
 63
        if 'netrc' in config:
 64
            try:
 65
                user, account, password = netrc(config['netrc']).authenticators(config['host'])
 66
            except IOError, e:
 67
                log.error('netrc: unable to open: %s' % e.filename)
 68
            except NetrcParseError, e:
 69
                log.error('netrc: %s, file: %s, line: %s' % (e.msg, e.filename, e.lineno))
 70
        else:
 71
            if 'username' in config:
 72
                user = config['username']
 73
            if 'password' in config:
 74
                password = config['password']
 76
        try:
 77
            cli = transmissionrpc.Client(config['host'], config['port'], user, password)
 78
        except TransmissionError, e:
 79
            if isinstance(e.original, HTTPHandlerError):
 80
                if e.original.code == 111:
 81
                    raise PluginError("Cannot connect to transmission. Is it running?")
 82
                elif e.original.code == 401:
 83
                    raise PluginError("Username/password for transmission is incorrect. Cannot connect.")
 84
                elif e.original.code == 110:
 85
                    raise PluginError("Cannot connect to transmission: Connection timed out.")
 86
                else:
 87
                    raise PluginError("Error connecting to transmission: %s" % e.original.message)
 88
            else:
 89
                raise PluginError("Error connecting to transmission: %s" % e.message)
 90
        return cli
 92
    @save_opener
 93
    def on_process_start(self, feed, config):
 94
        try:
 95
            import transmissionrpc
 96
            from transmissionrpc import TransmissionError
 97
            from transmissionrpc import HTTPHandlerError
 98
        except:
 99
            raise PluginError('Transmissionrpc module version 0.6 or higher required.', log)
100
        if [int(part) for part in transmissionrpc.__version__.split('.')] < [0, 6]:
101
            raise PluginError('Transmissionrpc module version 0.6 or higher required, please upgrade', log)
103
    @save_opener
104
    def on_feed_start(self, feed, config):
105
        config = self.prepare_config(config)
106
        if config['enabled']:
107
            if feed.manager.options.test:
108
                log.info('Trying to connect to transmission...')
109
                self.client = self.create_rpc_client(config)
110
                if self.client:
111
                    log.info('Successfully connected to transmission.')
112
                else:
113
                    log.error('It looks like there was a problem connecting to transmission.')
116
class PluginTransmissionInput(TransmissionBase):
118
    def validator(self):
119
        """Return config validator"""
120
        root = validator.factory()
121
        root.accept('boolean')
122
        advanced = root.accept('dict')
123
        self._validator(advanced)
124
        advanced.accept('boolean', key='onlycomplete')
125
        return root
127
    def prepare_config(self, config):
128
        config = TransmissionBase.prepare_config(self, config)
129
        config.setdefault('onlycomplete', True)
130
        return config
132
    def on_feed_input(self, feed, config):
133
        config = self.prepare_config(config)
134
        if not config['enabled']:
135
            return
137
        if not self.client:
138
            self.client = self.create_rpc_client(config)
139
        entries = []
140
        for torrent in self.client.info().values():
141
            torrentCompleted = self._torrent_completed(torrent)
142
            if not config['onlycomplete'] or torrentCompleted:
143
                entry = Entry(title=torrent.name,
144
                              url='file://%s' % torrent.torrentFile,
145
                              torrent_info_hash=torrent.hashString)
146
                entries.append(entry)
147
        return entries
149
    def _torrent_completed(self, torrent):
150
        result = True
151
        for tf in torrent.files().iteritems():
152
            result &= (tf[1]['completed'] != tf[1]['size'])
153
        return result
156
class PluginTransmission(TransmissionBase):
157
    """
158
      Add url from entry url to transmission
160
      Example:
162
      transmission:
163
        host: localhost
164
        port: 9091
165
        netrc: /home/flexget/.tmnetrc
166
        username: myusername
167
        password: mypassword
168
        path: the download location
169
        removewhendone: yes
171
    Default values for the config elements:
173
    transmission:
174
        host: localhost
175
        port: 9091
176
        enabled: yes
177
        removewhendone: no
178
    """
180
    def validator(self):
181
        """Return config validator"""
182
        root = validator.factory()
183
        root.accept('boolean')
184
        advanced = root.accept('dict')
185
        self._validator(advanced)
186
        advanced.accept('path', key='path', allow_replacement=True)
187
        advanced.accept('boolean', key='addpaused')
188
        advanced.accept('boolean', key='honourlimits')
189
        advanced.accept('integer', key='bandwidthpriority')
190
        advanced.accept('integer', key='maxconnections')
191
        advanced.accept('number', key='maxupspeed')
192
        advanced.accept('number', key='maxdownspeed')
193
        advanced.accept('number', key='ratio')
194
        advanced.accept('boolean', key='removewhendone')
195
        return root
197
    def prepare_config(self, config):
198
        config = TransmissionBase.prepare_config(self, config)
199
        config.setdefault('removewhendone', False)
200
        return config
202
    @save_opener
203
    def on_process_start(self, feed, config):
204
        set_plugin = get_plugin_by_name('set')
205
        set_plugin.instance.register_keys({'path': 'text',
206
                                           'addpaused': 'boolean',
207
                                           'honourlimits': 'boolean',
208
                                           'bandwidthpriority': 'integer',
209
                                           'maxconnections': 'integer',
210
                                           'maxupspeed': 'number',
211
                                           'maxdownspeed': 'number',
212
                                           'ratio': 'number'})
213
        super(PluginTransmission, self).on_process_start(feed, config)
215
    @priority(120)
216
    def on_feed_download(self, feed, config):
217
        """
218
            Call download plugin to generate the temp files we will load
219
            into deluge then verify they are valid torrents
220
        """
221
        config = self.prepare_config(config)
222
        if not config['enabled']:
223
            return
226
        if not 'download' in feed.config:
227
            download = get_plugin_by_name('download')
228
            download.instance.get_temp_files(feed, handle_magnets=True, fail_html=True)
230
    @priority(135)
231
    @save_opener
232
    def on_feed_output(self, feed, config):
233
        from transmissionrpc import TransmissionError
234
        config = self.prepare_config(config)
236
        if feed.manager.options.learn:
237
            return
238
        if not config['enabled']:
239
            return
241
        if not feed.accepted and not config['removewhendone']:
242
            return
243
        if self.client is None:
244
            self.client = self.create_rpc_client(config)
245
            if self.client:
246
                log.debug('Successfully connected to transmission.')
247
            else:
248
                raise PluginError("Couldn't connect to transmission.")
249
        if feed.accepted:
250
            self.add_to_transmission(self.client, feed, config)
251
        if config['removewhendone']:
252
            try:
253
                self.remove_finished(self.client)
254
            except TransmissionError, e:
255
                log.error('Error while attempting to remove completed torrents from transmission: %s' % e)
257
    def _make_torrent_options_dict(self, config, entry):
259
        opt_dic = {}
261
        for opt_key in ('path', 'addpaused', 'honourlimits', 'bandwidthpriority',
262
                        'maxconnections', 'maxupspeed', 'maxdownspeed', 'ratio'):
263
            if opt_key in entry:
264
                opt_dic[opt_key] = entry[opt_key]
265
            elif opt_key in config:
266
                opt_dic[opt_key] = config[opt_key]
268
        options = {'add': {}, 'change': {}}
270
        if opt_dic.get('path'):
271
            try:
272
                options['add']['download_dir'] = os.path.expanduser(entry.render(opt_dic['path'])).encode('utf-8')
273
            except RenderError, e:
274
                log.error('Error setting path for %s: %s' % (entry['title'], e))
275
        if opt_dic.get('addpaused'):
276
            options['add']['paused'] = True
277
        if 'bandwidthpriority' in opt_dic:
278
            options['add']['bandwidthPriority'] = opt_dic['bandwidthpriority']
279
        if 'maxconnections' in opt_dic:
280
            options['add']['peer_limit'] = opt_dic['maxconnections']
282
        if 'honourlimits' in opt_dic and not opt_dic['honourlimits']:
283
            options['change']['honorsSessionLimits'] = False
284
        if 'maxupspeed' in opt_dic:
285
            options['change']['uploadLimit'] = opt_dic['maxupspeed']
286
            options['change']['uploadLimited'] = True
287
        if 'maxdownspeed' in opt_dic:
288
            options['change']['downloadLimit'] = opt_dic['maxdownspeed']
289
            options['change']['downloadLimited'] = True
291
        if 'ratio' in opt_dic:
292
            options['change']['seedRatioLimit'] = opt_dic['ratio']
293
            if opt_dic['ratio'] == -1:
298
                options['change']['seedRatioMode'] = 2
299
            else:
300
                options['change']['seedRatioMode'] = 1
302
        return options
304
    def add_to_transmission(self, cli, feed, config):
305
        """Adds accepted entries to transmission """
306
        from transmissionrpc import TransmissionError
307
        for entry in feed.accepted:
308
            if feed.manager.options.test:
309
                log.info('Would add %s to transmission' % entry['url'])
310
                continue
311
            options = self._make_torrent_options_dict(config, entry)
313
            downloaded = not entry['url'].startswith('magnet:')
316
            if downloaded and not 'file' in entry:
317
                feed.fail(entry, 'file missing?')
318
                continue
321
            if downloaded and not os.path.exists(entry['file']):
322
                tmp_path = os.path.join(feed.manager.config_base, 'temp')
323
                log.debug('entry: %s' % entry)
324
                log.debug('temp: %s' % ', '.join(os.listdir(tmp_path)))
325
                feed.fail(entry, "Downloaded temp file '%s' doesn't exist!?" % entry['file'])
326
                continue
328
            try:
329
                if downloaded:
330
                    f = open(entry['file'], 'rb')
331
                    try:
332
                        filedump = base64.encodestring(f.read())
333
                    finally:
334
                        f.close()
335
                    r = cli.add(filedump, 30, **options['add'])
336
                else:
337
                    r = cli.add_uri(entry['url'], timeout=30, **options['add'])
338
                if r:
339
                    torrent = r.values()[0]
340
                log.info('"%s" torrent added to transmission' % (entry['title']))
341
                if options['change'].keys():
342
                    for id in r.keys():
343
                        cli.change(id, 30, **options['change'])
344
            except TransmissionError, e:
345
                log.debug('TransmissionError', exc_info=True)
346
                msg = 'TransmissionError: %s' % e.message or 'N/A'
347
                log.error(msg)
348
                feed.fail(entry, msg)
350
    def remove_finished(self, cli):
352
        transfers = cli.info(arguments=['id', 'hashString', 'name', 'status', 'uploadRatio', 'seedRatioLimit'])
353
        remove_ids = []
355
        for transfer in transfers.itervalues():
356
            log.debug('Transfer "%s": status: "%s" upload ratio: %.2f seed ratio: %.2f' % \
357
                (transfer.name, transfer.status, transfer.uploadRatio, transfer.seedRatioLimit))
358
            if transfer.status == 'stopped' and transfer.uploadRatio >= transfer.seedRatioLimit:
359
                log.info('Removing finished torrent `%s` from transmission' % transfer.name)
360
                remove_ids.append(transfer.id)
362
        if remove_ids:
363
            cli.remove(remove_ids)
365
    def on_feed_exit(self, feed, config):
366
        """Make sure all temp files are cleaned up when feed exits"""
368
        if not 'download' in feed.config:
369
            download = get_plugin_by_name('download')
370
            download.instance.cleanup_temp_files(feed)
372
    on_feed_abort = on_feed_exit
374
register_plugin(PluginTransmission, 'transmission', api_ver=2)
375
register_plugin(PluginTransmissionInput, 'from_transmission', api_ver=2)