flexget.plugins.output_transmissionrpc
Covered: 70 lines
Missed: 185 lines
Skipped 57 lines
Percent: 27 %
  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.utils.tools import replace_from_entry
  9
log = logging.getLogger('transmission')
 12
def save_opener(f):
 13
    """
 14
        Transmissionrpc sets a new default opener for urllib2
 15
        We use this as a decorator to capture and restore it when needed
 16
    """
 18
    def new_f(self, *args, **kwargs):
 19
        import urllib2
 20
        prev_opener = urllib2._opener
 21
        urllib2.install_opener(self.opener)
 22
        try:
 23
            f(self, *args, **kwargs)
 24
            self.opener = urllib2._opener
 25
        finally:
 26
            urllib2.install_opener(prev_opener)
 27
    return new_f
 30
class PluginTransmissionrpc:
 31
    """
 32
      Add url from entry url to transmission
 34
      Example:
 36
      transmissionrpc:
 37
        host: localhost
 38
        port: 9091
 39
        netrc: /home/flexget/.tmnetrc
 40
        username: myusername
 41
        password: mypassword
 42
        path: the download location
 43
        removewhendone: yes
 45
    Default values for the config elements:
 47
    transmissionrpc:
 48
        host: localhost
 49
        port: 9091
 50
        enabled: yes
 51
        removewhendone: no
 52
    """
 54
    def __init__(self):
 55
        self.client = None
 56
        self.opener = None
 58
    def validator(self):
 59
        """Return config validator"""
 60
        root = validator.factory()
 61
        root.accept('boolean')
 62
        advanced = root.accept('dict')
 63
        advanced.accept('text', key='host')
 64
        advanced.accept('number', key='port')
 66
        advanced.accept('file', key='netrc', required=False)
 67
        advanced.accept('text', key='username', required=False)
 68
        advanced.accept('text', key='password', required=False)
 69
        advanced.accept('path', key='path', required=False, allow_replacement=True)
 70
        advanced.accept('boolean', key='addpaused', required=False)
 71
        advanced.accept('number', key='maxconnections', required=False)
 72
        advanced.accept('number', key='maxupspeed', required=False)
 73
        advanced.accept('number', key='maxdownspeed', required=False)
 74
        advanced.accept('decimal', key='ratio', required=False)
 75
        advanced.accept('boolean', key='enabled')
 76
        advanced.accept('boolean', key='removewhendone')
 77
        return root
 79
    def get_config(self, feed):
 80
        config = feed.config['transmissionrpc']
 81
        if isinstance(config, bool):
 82
            config = {'enabled': config}
 83
        config.setdefault('enabled', True)
 84
        config.setdefault('host', 'localhost')
 85
        config.setdefault('port', 9091)
 86
        config.setdefault('removewhendone', False)
 87
        return config
 89
    @save_opener
 90
    def on_process_start(self, feed):
 91
        """Event handler"""
 92
        try:
 93
            import transmissionrpc
 94
            from transmissionrpc import TransmissionError
 95
            from transmissionrpc import HTTPHandlerError
 96
        except:
 97
            raise PluginError('Transmissionrpc module version 0.5 or higher required.', log)
 98
        set_plugin = get_plugin_by_name('set')
 99
        set_plugin.instance.register_keys({'path': 'text', \
100
                                           'addpaused': 'boolean', \
101
                                           'maxconnections': 'number', \
102
                                           'maxupspeed': 'number',  \
103
                                           'maxdownspeed': 'number', \
104
                                           'ratio': 'decimal'})
105
        config = self.get_config(feed)
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(feed)
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.')
114
            elif config['removewhendone']:
115
                self.client = self.create_rpc_client(feed)
116
                self.remove_finished(self.client)
118
    @priority(120)
119
    def on_feed_download(self, feed):
120
        """
121
            Call download plugin to generate the temp files we will load
122
            into deluge then verify they are valid torrents
123
        """
124
        config = self.get_config(feed)
125
        if not config['enabled']:
126
            return
129
        if not 'download' in feed.config:
130
            download = get_plugin_by_name('download')
131
            download.instance.get_temp_files(feed, handle_magnets=True)
133
    @priority(135)
134
    @save_opener
135
    def on_feed_output(self, feed):
136
        """Event handler"""
137
        config = self.get_config(feed)
139
        if feed.manager.options.learn:
140
            return
141
        if not config['enabled']:
142
            return
144
        if len(feed.accepted) == 0:
145
            return
146
        if self.client is None:
147
            self.client = self.create_rpc_client(feed)
148
            if self.client:
149
                log.debug('Successfully connected to transmission.')
150
            else:
151
                raise PluginError("Couldn't connect to transmission.")
152
        self.add_to_transmission(self.client, feed)
154
    def _make_torrent_options_dict(self, feed, entry):
156
        opt_dic = {}
157
        config = self.get_config(feed)
159
        for opt_key in ['path', 'addpaused', 'maxconnections', 'maxupspeed', 'maxdownspeed', 'ratio']:
160
            if opt_key in entry:
161
                opt_dic[opt_key] = entry[opt_key]
162
            elif opt_key in config:
163
                opt_dic[opt_key] = config[opt_key]
165
        options = {'add': {}, 'change': {}}
167
        if 'path' in opt_dic:
168
            opt_dic['path'] = replace_from_entry(opt_dic['path'], entry, 'path', log.error)
169
            if opt_dic['path']:
170
                options['add']['download_dir'] = os.path.expanduser(opt_dic['path'])
171
        if 'addpaused' in opt_dic and opt_dic['addpaused']:
172
            options['add']['paused'] = True
173
        if 'maxconnections' in opt_dic:
174
            options['add']['peer_limit'] = opt_dic['maxconnections']
176
        if 'maxupspeed' in opt_dic:
177
            options['change']['uploadLimit'] = opt_dic['maxupspeed']
178
            options['change']['uploadLimited'] = True
179
        if 'maxdownspeed' in opt_dic:
180
            options['change']['downloadLimit'] = opt_dic['maxdownspeed']
181
            options['change']['downloadLimited'] = True
183
        if 'ratio' in opt_dic:
184
            options['change']['seedRatioLimit'] = opt_dic['ratio']
185
            if opt_dic['ratio'] == -1:
190
                options['change']['seedRatioMode'] = 2
191
            else:
192
                options['change']['seedRatioMode'] = 1
194
        return options
196
    def create_rpc_client(self, feed):
197
        import transmissionrpc
198
        from transmissionrpc import TransmissionError
199
        from transmissionrpc import HTTPHandlerError
201
        config = self.get_config(feed)
202
        user, password = None, None
204
        if 'netrc' in config:
205
            try:
206
                user, account, password = netrc(config['netrc']).authenticators(config['host'])
207
            except IOError, e:
208
                log.error('netrc: unable to open: %s' % e.filename)
209
            except NetrcParseError, e:
210
                log.error('netrc: %s, file: %s, line: %s' % (e.msg, e.filename, e.lineno))
211
        else:
212
            if 'username' in config:
213
                user = config['username']
214
            if 'password' in config:
215
                password = config['password']
217
        try:
218
            cli = transmissionrpc.Client(config['host'], config['port'], user, password)
219
        except TransmissionError, e:
220
            if isinstance(e.original, HTTPHandlerError):
221
                if e.original.code == 111:
222
                    raise PluginError("Cannot connect to transmission. Is it running?")
223
                elif e.original.code == 401:
224
                    raise PluginError("Username/password for transmission is incorrect. Cannot connect.")
225
                elif e.original.code == 110:
226
                    raise PluginError("Cannot connect to transmission: Connection timed out.")
227
                else:
228
                    raise PluginError("Error connecting to transmission: %s" % e.original.message)
229
            else:
230
                raise PluginError("Error connecting to transmission: %s" % e.message)
231
        return cli
233
    def add_to_transmission(self, cli, feed):
234
        """Adds accepted entries to transmission """
235
        from transmissionrpc import TransmissionError
236
        for entry in feed.accepted:
237
            if feed.manager.options.test:
238
                log.info('Would add %s to transmission' % entry['url'])
239
                continue
240
            options = self._make_torrent_options_dict(feed, entry)
242
            download = not(entry['url'].startswith('magnet:'))
245
            if download and not 'file' in entry:
246
                feed.fail(entry, 'file missing?')
247
                continue
250
            if download and not os.path.exists(entry['file']):
251
                tmp_path = os.path.join(feed.manager.config_base, 'temp')
252
                log.debug('entry: %s' % entry)
253
                log.debug('temp: %s' % ', '.join(os.listdir(tmp_path)))
254
                feed.fail(entry, "Downloaded temp file '%s' doesn't exist!?" % entry['file'])
255
                continue
257
            try:
258
                if download:
259
                    f = open(entry['file'], 'rb')
260
                    try:
261
                        filedump = base64.encodestring(f.read())
262
                    finally:
263
                        f.close()
264
                    r = cli.add(filedump, 30, **options['add'])
265
                else:
266
                    r = cli.add(None, filename=entry['url'],
267
                                timeout=30, **options['add'])
268
                log.info('"%s" torrent added to transmission' % (entry['title']))
269
                if options['change'].keys():
270
                    for id in r.keys():
271
                        cli.change(id, 30, **options['change'])
272
            except TransmissionError, e:
273
                log.error(e.message)
274
                feed.fail(entry)
278
            if download and not 'download' in feed.config:
279
                os.remove(entry['file'])
280
                del(entry['file'])
282
    def remove_finished(self, cli):
284
        transfers = cli.info(arguments=['id', 'hashString', 'name', 'status', 'uploadRatio', 'seedRatioLimit'])
285
        remove_ids = []
287
        for transfer in transfers.itervalues():
288
            log.debug('Transfer "%s": status: "%s" upload ratio: %.2f seed ratio: %.2f' % \
289
                (transfer.name, transfer.status, transfer.uploadRatio, transfer.seedRatioLimit))
290
            if transfer.status == 'stopped' and transfer.uploadRatio >= transfer.seedRatioLimit:
291
                log.info('Remove torrent "%s" from transmission' % (transfer.name))
292
                remove_ids.append(transfer.id)
294
        if len(remove_ids) > 0:
295
            cli.remove(remove_ids)
297
    def on_feed_exit(self, feed):
298
        """Make sure all temp files are cleaned up when feed exits"""
300
        if not 'download' in feed.config:
301
            download = get_plugin_by_name('download')
302
            download.instance.cleanup_temp_files(feed)
304
    def on_feed_abort(self, feed):
305
        """Make sure all temp files are cleaned up when feed is aborted."""
307
        if not 'download' in feed.config:
308
            download = get_plugin_by_name('download')
309
            download.instance.cleanup_temp_files(feed)
311
register_plugin(PluginTransmissionrpc, 'transmissionrpc')