2
from netrc import netrc, NetrcParseError
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')
15
Transmissionrpc sets a new default opener for urllib2
16
We use this as a decorator to capture and restore it when needed
19
def new_f(self, *args, **kwargs):
21
prev_opener = urllib2._opener
22
urllib2.install_opener(self.opener)
24
f(self, *args, **kwargs)
25
self.opener = urllib2._opener
27
urllib2.install_opener(prev_opener)
31
class TransmissionBase(object):
37
def _validator(self, advanced):
38
"""Return config validator"""
39
advanced.accept('text', key='host')
40
advanced.accept('integer', key='port')
41
# note that password is optional in transmission
42
advanced.accept('file', key='netrc')
43
advanced.accept('text', key='username')
44
advanced.accept('text', key='password')
45
advanced.accept('boolean', key='enabled')
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)
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
65
user, account, password = netrc(config['netrc']).authenticators(config['host'])
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))
71
if 'username' in config:
72
user = config['username']
73
if 'password' in config:
74
password = config['password']
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.")
87
raise PluginError("Error connecting to transmission: %s" % e.original.message)
89
raise PluginError("Error connecting to transmission: %s" % e.message)
93
def on_process_start(self, feed, config):
95
import transmissionrpc
96
from transmissionrpc import TransmissionError
97
from transmissionrpc import HTTPHandlerError
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)
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)
111
log.info('Successfully connected to transmission.')
113
log.error('It looks like there was a problem connecting to transmission.')
116
class PluginTransmissionInput(TransmissionBase):
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')
127
def prepare_config(self, config):
128
config = TransmissionBase.prepare_config(self, config)
129
config.setdefault('onlycomplete', True)
132
def on_feed_input(self, feed, config):
133
config = self.prepare_config(config)
134
if not config['enabled']:
138
self.client = self.create_rpc_client(config)
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)
149
def _torrent_completed(self, torrent):
151
for tf in torrent.files().iteritems():
152
result &= (tf[1]['completed'] != tf[1]['size'])
156
class PluginTransmission(TransmissionBase):
158
Add url from entry url to transmission
165
netrc: /home/flexget/.tmnetrc
168
path: the download location
171
Default values for the config elements:
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')
197
def prepare_config(self, config):
198
config = TransmissionBase.prepare_config(self, config)
199
config.setdefault('removewhendone', False)
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',
213
super(PluginTransmission, self).on_process_start(feed, config)
216
def on_feed_download(self, feed, config):
218
Call download plugin to generate the temp files we will load
219
into deluge then verify they are valid torrents
221
config = self.prepare_config(config)
222
if not config['enabled']:
224
# If the download plugin is not enabled, we need to call it to get
225
# our temp .torrent files
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)
232
def on_feed_output(self, feed, config):
233
from transmissionrpc import TransmissionError
234
config = self.prepare_config(config)
235
# don't add when learning
236
if feed.manager.options.learn:
238
if not config['enabled']:
240
# Do not run if there is nothing to do
241
if not feed.accepted and not config['removewhendone']:
243
if self.client is None:
244
self.client = self.create_rpc_client(config)
246
log.debug('Successfully connected to transmission.')
248
raise PluginError("Couldn't connect to transmission.")
250
self.add_to_transmission(self.client, feed, config)
251
if config['removewhendone']:
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):
261
for opt_key in ('path', 'addpaused', 'honourlimits', 'bandwidthpriority',
262
'maxconnections', 'maxupspeed', 'maxdownspeed', 'ratio'):
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'):
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:
295
# 0 follow the global settings
296
# 1 override the global settings, seeding until a certain ratio
297
# 2 override the global settings, seeding regardless of ratio
298
options['change']['seedRatioMode'] = 2
300
options['change']['seedRatioMode'] = 1
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'])
311
options = self._make_torrent_options_dict(config, entry)
313
downloaded = not entry['url'].startswith('magnet:')
315
# Check that file is downloaded
316
if downloaded and not 'file' in entry:
317
feed.fail(entry, 'file missing?')
320
# Verify the temp file exists
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'])
330
f = open(entry['file'], 'rb')
332
filedump = base64.encodestring(f.read())
335
r = cli.add(filedump, 30, **options['add'])
337
r = cli.add_uri(entry['url'], timeout=30, **options['add'])
339
torrent = r.values()[0]
340
log.info('"%s" torrent added to transmission' % (entry['title']))
341
if options['change'].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'
348
feed.fail(entry, msg)
350
def remove_finished(self, cli):
351
# Get a list of active transfers
352
transfers = cli.info(arguments=['id', 'hashString', 'name', 'status', 'uploadRatio', 'seedRatioLimit'])
354
# Go through the list of active transfers and add finished transfers to 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)
361
# Remove finished transfers
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"""
367
# If download plugin is enabled, it will handle cleanup.
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)