diff --git a/resources/lib/transmissionrpc/__init__.py b/resources/lib/transmissionrpc/__init__.py old mode 100644 new mode 100755 index f4afa02..82f7e21 --- a/resources/lib/transmissionrpc/__init__.py +++ b/resources/lib/transmissionrpc/__init__.py @@ -1,10 +1,16 @@ # -*- coding: utf-8 -*- -# 2008-08, Erik Svensson +# Copyright (c) 2008-2010 Erik Svensson +# Licensed under the MIT license. -from constants import * -from transmission import TransmissionError, Torrent, Session, Client +from transmissionrpc.constants import DEFAULT_PORT, DEFAULT_TIMEOUT, STATUS, PRIORITY, RATIO_LIMIT, LOGGER +from transmissionrpc.error import TransmissionError, HTTPHandlerError +from transmissionrpc.httphandler import HTTPHandler, DefaultHTTPHandler +from transmissionrpc.torrent import Torrent +from transmissionrpc.session import Session +from transmissionrpc.client import Client +from transmissionrpc.utils import add_stdout_logger __author__ = u'Erik Svensson ' -__version__ = u'0.3' -__copyright__ = u'Copyright (c) 2008 Erik Svensson' +__version__ = u'0.7' +__copyright__ = u'Copyright (c) 2008-2010 Erik Svensson' __license__ = u'MIT' diff --git a/resources/lib/transmissionrpc/client.py b/resources/lib/transmissionrpc/client.py new file mode 100755 index 0000000..c68a339 --- /dev/null +++ b/resources/lib/transmissionrpc/client.py @@ -0,0 +1,643 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2008-2010 Erik Svensson +# Licensed under the MIT license. + +import os, re, time +import warnings +import httplib, urllib2, urlparse, base64 + +try: + import json +except ImportError: + import simplejson as json + +from transmissionrpc.constants import DEFAULT_PORT, DEFAULT_TIMEOUT +from transmissionrpc.error import TransmissionError, HTTPHandlerError +from transmissionrpc.utils import LOGGER, get_arguments, make_rpc_name, argument_value_convert, rpc_bool +from transmissionrpc.httphandler import DefaultHTTPHandler +from transmissionrpc.torrent import Torrent +from transmissionrpc.session import Session + +def debug_httperror(error): + """ + Log the Transmission RPC HTTP error. + """ + try: + data = json.loads(error.data) + except ValueError: + data = error.data + LOGGER.debug( + json.dumps( + { + 'response': { + 'url': error.url, + 'code': error.code, + 'msg': error.message, + 'headers': error.headers, + 'data': data, + } + }, + indent=2 + ) + ) + +""" +Torrent ids + +Many functions in Client takes torrent id. A torrent id can either be id or +hashString. When suppling multiple id's it is possible to use a list mixed +with both id and hashString. + +Timeouts + +Since most methods results in HTTP requests against Transmission, it is +possible to provide a argument called ``timeout``. Timeout is only effective +when using Python 2.6 or later and the default timeout is 30 seconds. +""" + +class Client(object): + """ + Client is the class handling the Transmission JSON-RPC client protocol. + """ + + def __init__(self, address='localhost', port=DEFAULT_PORT, user=None, password=None, http_handler=None, timeout=None): + if isinstance(timeout, (int, long, float)): + self._query_timeout = float(timeout) + else: + self._query_timeout = DEFAULT_TIMEOUT + urlo = urlparse.urlparse(address) + if urlo.scheme == '': + base_url = 'http://' + address + ':' + str(port) + self.url = base_url + '/transmission/rpc' + else: + if urlo.port: + self.url = urlo.scheme + '://' + urlo.hostname + ':' + str(urlo.port) + urlo.path + else: + self.url = urlo.scheme + '://' + urlo.hostname + urlo.path + LOGGER.info('Using custom URL "' + self.url + '".') + if urlo.username and urlo.password: + user = urlo.username + password = urlo.password + elif urlo.username or urlo.password: + LOGGER.warning('Either user or password missing, not using authentication.') + if http_handler == None: + self.http_handler = DefaultHTTPHandler() + else: + if hasattr(http_handler, 'set_authentication') and hasattr(http_handler, 'request'): + self.http_handler = http_handler + else: + raise ValueError('Invalid HTTP handler.') + if user and password: + self.http_handler.set_authentication(self.url, user, password) + elif user or password: + LOGGER.warning('Either user or password missing, not using authentication.') + self._sequence = 0 + self.session = Session() + self.session_id = 0 + self.server_version = None + self.protocol_version = None + self.get_session() + self.torrent_get_arguments = get_arguments('torrent-get' + , self.rpc_version) + + def _get_timeout(self): + """ + Get current timeout for HTTP queries. + """ + return self._query_timeout + + def _set_timeout(self, value): + """ + Set timeout for HTTP queries. + """ + self._query_timeout = float(value) + + def _del_timeout(self): + """ + Reset the HTTP query timeout to the default. + """ + self._query_timeout = DEFAULT_TIMEOUT + + timeout = property(_get_timeout, _set_timeout, _del_timeout, doc="HTTP query timeout.") + + def _http_query(self, query, timeout=None): + """ + Query Transmission through HTTP. + """ + headers = {'x-transmission-session-id': str(self.session_id)} + request_count = 0 + if timeout == None: + timeout = self._query_timeout + while True: + LOGGER.debug(json.dumps({'url': self.url, 'headers': headers, 'query': query, 'timeout': timeout}, indent=2)) + try: + result = self.http_handler.request(self.url, query, headers, timeout) + break + except HTTPHandlerError, error: + if error.code == 409: + LOGGER.info('Server responded with 409, trying to set session-id.') + if request_count > 1: + raise TransmissionError('Session ID negotiation failed.', error) + if 'x-transmission-session-id' in error.headers: + self.session_id = error.headers['x-transmission-session-id'] + headers = {'x-transmission-session-id': str(self.session_id)} + else: + debug_httperror(error) + raise TransmissionError('Unknown conflict.', error) + else: + debug_httperror(error) + raise TransmissionError('Request failed.', error) + request_count = request_count + 1 + return result + + def _request(self, method, arguments=None, ids=None, require_ids=False, timeout=None): + """ + Send json-rpc request to Transmission using http POST + """ + if not isinstance(method, (str, unicode)): + raise ValueError('request takes method as string') + if arguments == None: + arguments = {} + if not isinstance(arguments, dict): + raise ValueError('request takes arguments as dict') + ids = self._format_ids(ids) + if len(ids) > 0: + arguments['ids'] = ids + elif require_ids: + raise ValueError('request require ids') + + query = json.dumps({'tag': self._sequence, 'method': method + , 'arguments': arguments}) + self._sequence += 1 + start = time.time() + http_data = self._http_query(query, timeout) + elapsed = time.time() - start + LOGGER.info('http request took %.3f s' % (elapsed)) + + try: + data = json.loads(http_data) + except ValueError, error: + LOGGER.error('Error: ' + str(error)) + LOGGER.error('Request: \"%s\"' % (query)) + LOGGER.error('HTTP data: \"%s\"' % (http_data)) + raise + + LOGGER.debug(json.dumps(data, indent=2)) + if 'result' in data: + if data['result'] != 'success': + raise TransmissionError('Query failed with result \"%s\".' % (data['result'])) + else: + raise TransmissionError('Query failed without result.') + + results = {} + if method == 'torrent-get': + for item in data['arguments']['torrents']: + results[item['id']] = Torrent(self, item) + if self.protocol_version == 2 and 'peers' not in item: + self.protocol_version = 1 + elif method == 'torrent-add': + item = data['arguments']['torrent-added'] + results[item['id']] = Torrent(self, item) + elif method == 'session-get': + self._update_session(data['arguments']) + elif method == 'session-stats': + # older versions of T has the return data in "session-stats" + if 'session-stats' in data['arguments']: + self._update_session(data['arguments']['session-stats']) + else: + self._update_session(data['arguments']) + elif method in ('port-test', 'blocklist-update'): + results = data['arguments'] + else: + return None + + return results + + def _format_ids(self, args): + """ + Take things and make them valid torrent identifiers + """ + ids = [] + + if args == None: + pass + elif isinstance(args, (int, long)): + ids.append(args) + elif isinstance(args, (str, unicode)): + for item in re.split(u'[ ,]+', args): + if len(item) == 0: + continue + addition = None + try: + # handle index + addition = [int(item)] + except ValueError: + pass + if not addition: + # handle hashes + try: + int(item, 16) + addition = [item] + except ValueError: + pass + if not addition: + # handle index ranges i.e. 5:10 + match = re.match(u'^(\d+):(\d+)$', item) + if match: + try: + idx_from = int(match.group(1)) + idx_to = int(match.group(2)) + addition = range(idx_from, idx_to + 1) + except ValueError: + pass + if not addition: + raise ValueError(u'Invalid torrent id, \"%s\"' % item) + ids.extend(addition) + elif isinstance(args, (list)): + for item in args: + ids.extend(self._format_ids(item)) + else: + raise ValueError(u'Invalid torrent id') + return ids + + def _update_session(self, data): + """ + Update session data. + """ + self.session.update(data) + + def _update_server_version(self): + if self.server_version == None: + version_major = 1 + version_minor = 30 + version_changeset = 0 + version_parser = re.compile('(\d).(\d+) \((\d+)\)') + if hasattr(self.session, 'version'): + match = version_parser.match(self.session.version) + if (match): + version_major = int(match.group(1)) + version_minor = int(match.group(2)) + version_changeset = match.group(3) + self.server_version = (version_major, version_minor, version_changeset) + + @property + def rpc_version(self): + """ + Get the Transmission RPC version. Trying to deduct if the server don't have a version value. + """ + if self.protocol_version == None: + # Ugly fix for 2.12 reporting rpc-version 10, but having new arguments + if (self.server_version and (self.server_version[0] == 2 and self.server_version[1] == 12)): + self.protocol_version = 11 + elif hasattr(self.session, 'rpc_version'): + self.protocol_version = self.session.rpc_version + elif hasattr(self.session, 'version'): + self.protocol_version = 3 + else: + self.protocol_version = 2 + return self.protocol_version + + def _rpc_version_warning(self, version): + """ + Add a warning to the log if the Transmission RPC version is lower then the provided version. + """ + if self.rpc_version < version: + LOGGER.warning('Using feature not supported by server. RPC version for server %d, feature introduced in %d.' % (self.rpc_version, version)) + + def add(self, data, timeout=None, **kwargs): + """ + Add torrent to transfers list. Takes a base64 encoded .torrent file in data. + Additional arguments are: + + ===================== ==== ============================================================= + Argument RPC Description + ===================== ==== ============================================================= + ``bandwidthPriority`` 8 - Priority for this transfer. + ``download_dir`` 1 - The directory where the downloaded contents will be saved in. + ``filename`` 1 - A filepath or URL to a torrent file or a magnet link. + ``files_unwanted`` 1 - A list of file id's that shouldn't be downloaded. + ``files_wanted`` 1 - A list of file id's that should be downloaded. + ``metainfo`` 1 - The content of a torrent file, base64 encoded. + ``paused`` 1 - If True, does not start the transfer when added. + ``peer_limit`` 1 - Maximum number of peers allowed. + ``priority_high`` 1 - A list of file id's that should have high priority. + ``priority_low`` 1 - A list of file id's that should have low priority. + ``priority_normal`` 1 - A list of file id's that should have normal priority. + ===================== ==== ============================================================= + """ + args = {} + if data: + args = {'metainfo': data} + elif 'metainfo' not in kwargs and 'filename' not in kwargs: + raise ValueError('No torrent data or torrent uri.') + for key, value in kwargs.iteritems(): + argument = make_rpc_name(key) + (arg, val) = argument_value_convert('torrent-add', + argument, value, self.rpc_version) + args[arg] = val + return self._request('torrent-add', args, timeout=timeout) + + def add_uri(self, uri, **kwargs): + """ + Add torrent to transfers list. Takes a uri to a torrent, supporting + all uri's supported by Transmissions torrent-add 'filename' + argument. Additional arguments are: + + ===================== ==== ============================================================= + Argument RPC Description + ===================== ==== ============================================================= + ``bandwidthPriority`` 8 - Priority for this transfer. + ``download_dir`` 1 - The directory where the downloaded contents will be saved in. + ``files_unwanted`` 1 - A list of file id's that shouldn't be downloaded. + ``files_wanted`` 1 - A list of file id's that should be downloaded. + ``paused`` 1 - If True, does not start the transfer when added. + ``peer_limit`` 1 - Maximum number of peers allowed. + ``priority_high`` 1 - A list of file id's that should have high priority. + ``priority_low`` 1 - A list of file id's that should have low priority. + ``priority_normal`` 1 - A list of file id's that should have normal priority. + ===================== ==== ============================================================= + """ + if uri == None: + raise ValueError('add_uri requires a URI.') + # there has been some problem with T's built in torrent fetcher, + # use a python one instead + parseduri = urlparse.urlparse(uri) + torrent_data = None + if parseduri.scheme in ['file', 'ftp', 'ftps', 'http', 'https']: + torrent_file = urllib2.urlopen(uri) + torrent_data = base64.b64encode(torrent_file.read()) + if torrent_data: + return self.add(torrent_data, **kwargs) + else: + return self.add(None, filename=uri, **kwargs) + + def remove(self, ids, delete_data=False, timeout=None): + """ + remove torrent(s) with provided id(s). Local data is removed if + delete_data is True, otherwise not. + """ + self._rpc_version_warning(3) + self._request('torrent-remove', + {'delete-local-data':rpc_bool(delete_data)}, ids, True, timeout=timeout) + + def start(self, ids, timeout=None): + """start torrent(s) with provided id(s)""" + self._request('torrent-start', {}, ids, True, timeout=timeout) + + def stop(self, ids, timeout=None): + """stop torrent(s) with provided id(s)""" + self._request('torrent-stop', {}, ids, True, timeout=timeout) + + def verify(self, ids, timeout=None): + """verify torrent(s) with provided id(s)""" + self._request('torrent-verify', {}, ids, True, timeout=timeout) + + def reannounce(self, ids, timeout=None): + """Reannounce torrent(s) with provided id(s)""" + self._rpc_version_warning(5) + self._request('torrent-reannounce', {}, ids, True, timeout=timeout) + + def info(self, ids=None, arguments=None, timeout=None): + """Get detailed information for torrent(s) with provided id(s).""" + if not arguments: + arguments = self.torrent_get_arguments + return self._request('torrent-get', {'fields': arguments}, ids, timeout=timeout) + + def get_files(self, ids=None, timeout=None): + """ + Get list of files for provided torrent id(s). If ids is empty, + information for all torrents are fetched. This function returns a dictonary + for each requested torrent id holding the information about the files. + + :: + + { + : { + : { + 'name': , + 'size': , + 'completed': , + 'priority': , + 'selected': + } + + ... + } + + ... + } + """ + fields = ['id', 'name', 'hashString', 'files', 'priorities', 'wanted'] + request_result = self._request('torrent-get', {'fields': fields}, ids, timeout=timeout) + result = {} + for tid, torrent in request_result.iteritems(): + result[tid] = torrent.files() + return result + + def set_files(self, items, timeout=None): + """ + Set file properties. Takes a dictonary with similar contents as the result + of `get_files`. + + :: + + { + : { + : { + 'priority': , + 'selected': + } + + ... + } + + ... + } + """ + if not isinstance(items, dict): + raise ValueError('Invalid file description') + for tid, files in items.iteritems(): + if not isinstance(files, dict): + continue + wanted = [] + unwanted = [] + priority_high = [] + priority_normal = [] + priority_low = [] + for fid, file_desc in files.iteritems(): + if not isinstance(file_desc, dict): + continue + if 'selected' in file_desc and file_desc['selected']: + wanted.append(fid) + else: + unwanted.append(fid) + if 'priority' in file_desc: + if file_desc['priority'] == 'high': + priority_high.append(fid) + elif file_desc['priority'] == 'normal': + priority_normal.append(fid) + elif file_desc['priority'] == 'low': + priority_low.append(fid) + self.change([tid], files_wanted = wanted + , files_unwanted = unwanted + , priority_high = priority_high + , priority_normal = priority_normal + , priority_low = priority_low, timeout=timeout) + + def list(self, timeout=None): + """list all torrents""" + fields = ['id', 'hashString', 'name', 'sizeWhenDone', 'leftUntilDone' + , 'eta', 'status', 'rateUpload', 'rateDownload', 'uploadedEver' + , 'downloadedEver'] + return self._request('torrent-get', {'fields': fields}, timeout=timeout) + + def change(self, ids, timeout=None, **kwargs): + """ + Change torrent parameters for the torrent(s) with the supplied id's. The + parameters are: + + ============================ ===== =============== ======================================================================================= + Argument RPC Replaced by Description + ============================ ===== =============== ======================================================================================= + ``bandwidthPriority`` 5 - Priority for this transfer. + ``downloadLimit`` 5 - Set the speed limit for download in Kib/s. + ``downloadLimited`` 5 - Enable download speed limiter. + ``files_unwanted`` 1 - A list of file id's that shouldn't be downloaded. + ``files_wanted`` 1 - A list of file id's that should be downloaded. + ``honorsSessionLimits`` 5 - Enables or disables the transfer to honour the upload limit set in the session. + ``ids`` 1 - Local download location. + ``peer_limit`` 1 - The peer limit for the torrents. + ``priority_high`` 1 - A list of file id's that should have high priority. + ``priority_low`` 1 - A list of file id's that should have normal priority. + ``priority_normal`` 1 - A list of file id's that should have low priority. + ``seedIdleLimit`` 10 - Seed inactivity limit in minutes. + ``seedIdleMode`` 10 - Seed inactivity mode. 0 = Use session limit, 1 = Use transfer limit, 2 = Disable limit. + ``seedRatioLimit`` 5 - Seeding ratio. + ``seedRatioMode`` 5 - Which ratio to use. 0 = Use session limit, 1 = Use transfer limit, 2 = Disable limit. + ``speed_limit_down`` 1 - 5 downloadLimit Set the speed limit for download in Kib/s. + ``speed_limit_down_enabled`` 1 - 5 downloadLimited Enable download speed limiter. + ``speed_limit_up`` 1 - 5 uploadLimit Set the speed limit for upload in Kib/s. + ``speed_limit_up_enabled`` 1 - 5 uploadLimited Enable upload speed limiter. + ``trackerAdd`` 10 - Array of string with announce URLs to add. + ``trackerRemove`` 10 - Array of ids of trackers to remove. + ``trackerReplace`` 10 - Array of (id, url) tuples where the announce URL should be replaced. + ``uploadLimit`` 5 - Set the speed limit for upload in Kib/s. + ``uploadLimited`` 5 - Enable upload speed limiter. + ============================ ===== =============== ======================================================================================= + + .. NOTE:: + transmissionrpc will try to automatically fix argument errors. + """ + args = {} + for key, value in kwargs.iteritems(): + argument = make_rpc_name(key) + (arg, val) = argument_value_convert('torrent-set' + , argument, value, self.rpc_version) + args[arg] = val + + if len(args) > 0: + self._request('torrent-set', args, ids, True, timeout=timeout) + else: + ValueError("No arguments to set") + + def move(self, ids, location, timeout=None): + """Move torrent data to the new location.""" + self._rpc_version_warning(6) + args = {'location': location, 'move': True} + self._request('torrent-set-location', args, ids, True, timeout=timeout) + + def locate(self, ids, location, timeout=None): + """Locate torrent data at the location.""" + self._rpc_version_warning(6) + args = {'location': location, 'move': False} + self._request('torrent-set-location', args, ids, True, timeout=timeout) + + def get_session(self, timeout=None): + """Get session parameters""" + self._request('session-get', timeout=timeout) + self._update_server_version() + return self.session + + def set_session(self, timeout=None, **kwargs): + """ + Set session parameters. The parameters are: + + ================================ ===== ================= ========================================================================================================================== + Argument RPC Replaced by Description + ================================ ===== ================= ========================================================================================================================== + ``alt_speed_down`` 5 - Alternate session download speed limit (in Kib/s). + ``alt_speed_enabled`` 5 - Enables alternate global download speed limiter. + ``alt_speed_time_begin`` 5 - Time when alternate speeds should be enabled. Minutes after midnight. + ``alt_speed_time_day`` 5 - Enables alternate speeds scheduling these days. + ``alt_speed_time_enabled`` 5 - Enables alternate speeds scheduling. + ``alt_speed_time_end`` 5 - Time when alternate speeds should be disabled. Minutes after midnight. + ``alt_speed_up`` 5 - Alternate session upload speed limit (in Kib/s). + ``blocklist_enabled`` 5 - Enables the block list + ``cache_size_mb`` 10 - The maximum size of the disk cache in MB + ``dht_enabled`` 6 - Enables DHT. + ``download_dir`` 1 - Set the session download directory. + ``encryption`` 1 - Set the session encryption mode, one of ``required``, ``preferred`` or ``tolerated``. + ``idle_seeding_limit`` 10 - The default seed inactivity limit in minutes. + ``idle_seeding_limit_enabled`` 10 - Enables the default seed inactivity limit + ``incomplete_dir`` 7 - The path to the directory of incomplete transfer data. + ``incomplete_dir_enabled`` 7 - Enables the incomplete transfer data directory. Otherwise data for incomplete transfers are stored in the download target. + ``lpd_enabled`` 9 - Enables local peer discovery for public torrents. + ``peer_limit`` 1 - 5 peer-limit-global Maximum number of peers + ``peer_limit_global`` 5 - Maximum number of peers + ``peer_limit_per_torrent`` 5 - Maximum number of peers per transfer + ``peer_port`` 5 - Peer port. + ``peer_port_random_on_start`` 5 - Enables randomized peer port on start of Transmission. + ``pex_allowed`` 1 - 5 pex-enabled Allowing PEX in public torrents. + ``pex_enabled`` 5 - Allowing PEX in public torrents. + ``port`` 1 - 5 peer-port Peer port. + ``port_forwarding_enabled`` 1 - Enables port forwarding. + ``rename_partial_files`` 8 - Appends ".part" to incomplete files + ``script_torrent_done_enabled`` 9 - Whether or not to call the "done" script. + ``script_torrent_done_filename`` 9 - Filename of the script to run when the transfer is done. + ``seedRatioLimit`` 5 - Seed ratio limit. 1.0 means 1:1 download and upload ratio. + ``seedRatioLimited`` 5 - Enables seed ration limit. + ``speed_limit_down`` 1 - Download speed limit (in Kib/s). + ``speed_limit_down_enabled`` 1 - Enables download speed limiting. + ``speed_limit_up`` 1 - Upload speed limit (in Kib/s). + ``speed_limit_up_enabled`` 1 - Enables upload speed limiting. + ``start_added_torrents`` 9 - Added torrents will be started right away. + ``trash_original_torrent_files`` 9 - The .torrent file of added torrents will be deleted. + ================================ ===== ================= ========================================================================================================================== + + .. NOTE:: + transmissionrpc will try to automatically fix argument errors. + """ + args = {} + for key, value in kwargs.iteritems(): + if key == 'encryption' and value not in ['required', 'preferred', 'tolerated']: + raise ValueError('Invalid encryption value') + argument = make_rpc_name(key) + (arg, val) = argument_value_convert('session-set' + , argument, value, self.rpc_version) + args[arg] = val + if len(args) > 0: + self._request('session-set', args, timeout=timeout) + + def blocklist_update(self, timeout=None): + """Update block list. Returns the size of the block list.""" + self._rpc_version_warning(5) + result = self._request('blocklist-update', timeout=timeout) + if 'blocklist-size' in result: + return result['blocklist-size'] + return None + + def port_test(self, timeout=None): + """ + Tests to see if your incoming peer port is accessible from the + outside world. + """ + self._rpc_version_warning(5) + result = self._request('port-test', timeout=timeout) + if 'port-is-open' in result: + return result['port-is-open'] + return None + + def session_stats(self, timeout=None): + """Get session statistics""" + self._request('session-stats', timeout=timeout) + return self.session diff --git a/resources/lib/transmissionrpc/constants.py b/resources/lib/transmissionrpc/constants.py index 96ccc01..56fae51 100755 --- a/resources/lib/transmissionrpc/constants.py +++ b/resources/lib/transmissionrpc/constants.py @@ -1,17 +1,23 @@ # -*- coding: utf-8 -*- -# 2008-07, Erik Svensson +# Copyright (c) 2008-2010 Erik Svensson +# Licensed under the MIT license. import logging -logger = logging.getLogger('transmissionrpc') -logger.setLevel(logging.ERROR) +LOGGER = logging.getLogger('transmissionrpc') +LOGGER.setLevel(logging.ERROR) -def mirror_dict(d): - d.update(dict((v, k) for k, v in d.iteritems())) - return d +def mirror_dict(source): + """ + Creates a dictionary with all values as keys and all keys as values. + """ + source.update(dict((value, key) for key, value in source.iteritems())) + return source DEFAULT_PORT = 9091 +DEFAULT_TIMEOUT = 30.0 + TR_STATUS_CHECK_WAIT = (1<<0) TR_STATUS_CHECK = (1<<1) TR_STATUS_DOWNLOAD = (1<<2) @@ -43,188 +49,238 @@ TR_RATIOLIMIT_UNLIMITED = 2 # override the global settings, seeding regardless o RATIO_LIMIT = mirror_dict({ 'global' : TR_RATIOLIMIT_GLOBAL, 'single' : TR_RATIOLIMIT_SINGLE, - 'unlimeted' : TR_RATIOLIMIT_UNLIMITED + 'unlimited' : TR_RATIOLIMIT_UNLIMITED +}) + +TR_IDLELIMIT_GLOBAL = 0 # follow the global settings +TR_IDLELIMIT_SINGLE = 1 # override the global settings, seeding until a certain idle time +TR_IDLELIMIT_UNLIMITED = 2 # override the global settings, seeding regardless of activity + +IDLE_LIMIT = mirror_dict({ + 'global' : TR_RATIOLIMIT_GLOBAL, + 'single' : TR_RATIOLIMIT_SINGLE, + 'unlimited' : TR_RATIOLIMIT_UNLIMITED }) # A note on argument maps # These maps are used to verify *-set methods. The information is structured in # a tree. -# set +- - [, , , , ] -# | +- - [, , , , ] +# set +- - [, , , , , ] +# | +- - [, , , , , ] # | -# get +- - [, , , , ] -# +- - [, , , , ] +# get +- - [, , , , , ] +# +- - [, , , , , ] # Arguments for torrent methods TORRENT_ARGS = { 'get' : { - 'activityDate': ('number', 1, None, None, None), - 'addedDate': ('number', 1, None, None, None), - 'announceResponse': ('string', 1, None, None, None), - 'announceURL': ('string', 1, None, None, None), - 'bandwidthPriority': ('number', 5, None, None, None), - 'comment': ('string', 1, None, None, None), - 'corruptEver': ('number', 1, None, None, None), - 'creator': ('string', 1, None, None, None), - 'dateCreated': ('number', 1, None, None, None), - 'desiredAvailable': ('number', 1, None, None, None), - 'doneDate': ('number', 1, None, None, None), - 'downloadDir': ('string', 4, None, None, None), - 'downloadedEver': ('number', 1, None, None, None), - 'downloaders': ('number', 4, None, None, None), - 'downloadLimit': ('number', 1, None, None, None), - 'downloadLimited': ('boolean', 5, None, None, None), - 'downloadLimitMode': ('number', 1, 5, None, None), - 'error': ('number', 1, None, None, None), - 'errorString': ('number', 1, None, None, None), - 'eta': ('number', 1, None, None, None), - 'files': ('array', 1, None, None, None), - 'fileStats': ('array', 5, None, None, None), - 'hashString': ('string', 1, None, None, None), - 'haveUnchecked': ('number', 1, None, None, None), - 'haveValid': ('number', 1, None, None, None), - 'honorsSessionLimits': ('boolean', 5, None, None, None), - 'id': ('number', 1, None, None, None), - 'isPrivate': ('boolean', 1, None, None, None), - 'lastAnnounceTime': ('number', 1, None, None, None), - 'lastScrapeTime': ('number', 1, None, None, None), - 'leechers': ('number', 1, None, None, None), - 'leftUntilDone': ('number', 1, None, None, None), - 'manualAnnounceTime': ('number', 1, None, None, None), - 'maxConnectedPeers': ('number', 1, None, None, None), - 'name': ('string', 1, None, None, None), - 'nextAnnounceTime': ('number', 1, None, None, None), - 'nextScrapeTime': ('number', 1, None, None, None), - 'peer-limit': ('number', 5, None, None, None), - 'peers': ('array', 2, None, None, None), - 'peersConnected': ('number', 1, None, None, None), - 'peersFrom': ('object', 1, None, None, None), - 'peersGettingFromUs': ('number', 1, None, None, None), - 'peersKnown': ('number', 1, None, None, None), - 'peersSendingToUs': ('number', 1, None, None, None), - 'percentDone': ('double', 5, None, None, None), - 'pieces': ('string', 5, None, None, None), - 'pieceCount': ('number', 1, None, None, None), - 'pieceSize': ('number', 1, None, None, None), - 'priorities': ('array', 1, None, None, None), - 'rateDownload': ('number', 1, None, None, None), - 'rateUpload': ('number', 1, None, None, None), - 'recheckProgress': ('double', 1, None, None, None), - 'scrapeResponse': ('string', 1, None, None, None), - 'scrapeURL': ('string', 1, None, None, None), - 'seeders': ('number', 1, None, None, None), - 'seedRatioLimit': ('double', 5, None, None, None), - 'seedRatioMode': ('number', 5, None, None, None), - 'sizeWhenDone': ('number', 1, None, None, None), - 'startDate': ('number', 1, None, None, None), - 'status': ('number', 1, None, None, None), - 'swarmSpeed': ('number', 1, None, None, None), - 'timesCompleted': ('number', 1, None, None, None), - 'trackers': ('array', 1, None, None, None), - 'totalSize': ('number', 1, None, None, None), - 'torrentFile': ('string', 5, None, None, None), - 'uploadedEver': ('number', 1, None, None, None), - 'uploadLimit': ('number', 1, None, None, None), - 'uploadLimitMode': ('number', 1, 5, None, None), - 'uploadLimited': ('boolean', 5, None, None, None), - 'uploadRatio': ('double', 1, None, None, None), - 'wanted': ('array', 1, None, None, None), - 'webseeds': ('array', 1, None, None, None), - 'webseedsSendingToUs': ('number', 1, None, None, None), + 'activityDate': ('number', 1, None, None, None, ''), + 'addedDate': ('number', 1, None, None, None, ''), + 'announceResponse': ('string', 1, 7, None, None, ''), + 'announceURL': ('string', 1, 7, None, None, ''), + 'bandwidthPriority': ('number', 5, None, None, None, ''), + 'comment': ('string', 1, None, None, None, ''), + 'corruptEver': ('number', 1, None, None, None, ''), + 'creator': ('string', 1, None, None, None, ''), + 'dateCreated': ('number', 1, None, None, None, ''), + 'desiredAvailable': ('number', 1, None, None, None, ''), + 'doneDate': ('number', 1, None, None, None, ''), + 'downloadDir': ('string', 4, None, None, None, ''), + 'downloadedEver': ('number', 1, None, None, None, ''), + 'downloaders': ('number', 4, 7, None, None, ''), + 'downloadLimit': ('number', 1, None, None, None, ''), + 'downloadLimited': ('boolean', 5, None, None, None, ''), + 'downloadLimitMode': ('number', 1, 5, None, None, ''), + 'error': ('number', 1, None, None, None, ''), + 'errorString': ('number', 1, None, None, None, ''), + 'eta': ('number', 1, None, None, None, ''), + 'files': ('array', 1, None, None, None, ''), + 'fileStats': ('array', 5, None, None, None, ''), + 'hashString': ('string', 1, None, None, None, ''), + 'haveUnchecked': ('number', 1, None, None, None, ''), + 'haveValid': ('number', 1, None, None, None, ''), + 'honorsSessionLimits': ('boolean', 5, None, None, None, ''), + 'id': ('number', 1, None, None, None, ''), + 'isFinished': ('boolean', 9, None, None, None, ''), + 'isPrivate': ('boolean', 1, None, None, None, ''), + 'lastAnnounceTime': ('number', 1, 7, None, None, ''), + 'lastScrapeTime': ('number', 1, 7, None, None, ''), + 'leechers': ('number', 1, 7, None, None, ''), + 'leftUntilDone': ('number', 1, None, None, None, ''), + 'magnetLink': ('string', 7, None, None, None, ''), + 'manualAnnounceTime': ('number', 1, None, None, None, ''), + 'maxConnectedPeers': ('number', 1, None, None, None, ''), + 'metadataPercentComplete': ('number', 7, None, None, None, ''), + 'name': ('string', 1, None, None, None, ''), + 'nextAnnounceTime': ('number', 1, 7, None, None, ''), + 'nextScrapeTime': ('number', 1, 7, None, None, ''), + 'peer-limit': ('number', 5, None, None, None, ''), + 'peers': ('array', 2, None, None, None, ''), + 'peersConnected': ('number', 1, None, None, None, ''), + 'peersFrom': ('object', 1, None, None, None, ''), + 'peersGettingFromUs': ('number', 1, None, None, None, ''), + 'peersKnown': ('number', 1, None, None, None, ''), + 'peersSendingToUs': ('number', 1, None, None, None, ''), + 'percentDone': ('double', 5, None, None, None, ''), + 'pieces': ('string', 5, None, None, None, ''), + 'pieceCount': ('number', 1, None, None, None, ''), + 'pieceSize': ('number', 1, None, None, None, ''), + 'priorities': ('array', 1, None, None, None, ''), + 'rateDownload': ('number', 1, None, None, None, ''), + 'rateUpload': ('number', 1, None, None, None, ''), + 'recheckProgress': ('double', 1, None, None, None, ''), + 'scrapeResponse': ('string', 1, 7, None, None, ''), + 'scrapeURL': ('string', 1, 7, None, None, ''), + 'seeders': ('number', 1, 7, None, None, ''), + 'seedIdleLimit': ('number', 10, None, None, None, ''), + 'seedIdleMode': ('number', 10, None, None, None, ''), + 'seedRatioLimit': ('double', 5, None, None, None, ''), + 'seedRatioMode': ('number', 5, None, None, None, ''), + 'sizeWhenDone': ('number', 1, None, None, None, ''), + 'startDate': ('number', 1, None, None, None, ''), + 'status': ('number', 1, None, None, None, ''), + 'swarmSpeed': ('number', 1, 7, None, None, ''), + 'timesCompleted': ('number', 1, 7, None, None, ''), + 'trackers': ('array', 1, None, None, None, ''), + 'trackerStats': ('object', 7, None, None, None, ''), + 'totalSize': ('number', 1, None, None, None, ''), + 'torrentFile': ('string', 5, None, None, None, ''), + 'uploadedEver': ('number', 1, None, None, None, ''), + 'uploadLimit': ('number', 1, None, None, None, ''), + 'uploadLimitMode': ('number', 1, 5, None, None, ''), + 'uploadLimited': ('boolean', 5, None, None, None, ''), + 'uploadRatio': ('double', 1, None, None, None, ''), + 'wanted': ('array', 1, None, None, None, ''), + 'webseeds': ('array', 1, None, None, None, ''), + 'webseedsSendingToUs': ('number', 1, None, None, None, ''), }, 'set': { - 'bandwidthPriority': ('number', 5, None, None, None), - 'downloadLimit': ('number', 5, None, 'speed-limit-down', None), - 'downloadLimited': ('boolean', 5, None, 'speed-limit-down-enabled', None), - 'files-wanted': ('array', 1, None, None, None), - 'files-unwanted': ('array', 1, None, None, None), - 'honorsSessionLimits': ('boolean', 5, None, None, None), - 'ids': ('array', 1, None, None, None), - 'peer-limit': ('number', 1, None, None, None), - 'priority-high': ('array', 1, None, None, None), - 'priority-low': ('array', 1, None, None, None), - 'priority-normal': ('array', 1, None, None, None), - 'seedRatioLimit': ('double', 5, None, None, None), - 'seedRatioMode': ('number', 5, None, None, None), - 'speed-limit-down': ('number', 1, 5, None, 'downloadLimit'), - 'speed-limit-down-enabled': ('boolean', 1, 5, None, 'downloadLimited'), - 'speed-limit-up': ('number', 1, 5, None, 'uploadLimit'), - 'speed-limit-up-enabled': ('boolean', 1, 5, None, 'uploadLimited'), - 'uploadLimit': ('number', 5, None, 'speed-limit-up', None), - 'uploadLimited': ('boolean', 5, None, 'speed-limit-up-enabled', None), + 'bandwidthPriority': ('number', 5, None, None, None, 'Priority for this transfer.'), + 'downloadLimit': ('number', 5, None, 'speed-limit-down', None, 'Set the speed limit for download in Kib/s.'), + 'downloadLimited': ('boolean', 5, None, 'speed-limit-down-enabled', None, 'Enable download speed limiter.'), + 'files-wanted': ('array', 1, None, None, None, "A list of file id's that should be downloaded."), + 'files-unwanted': ('array', 1, None, None, None, "A list of file id's that shouldn't be downloaded."), + 'honorsSessionLimits': ('boolean', 5, None, None, None, "Enables or disables the transfer to honour the upload limit set in the session."), + 'ids': ('array', 1, None, None, None, 'Local download location.'), + 'peer-limit': ('number', 1, None, None, None, 'The peer limit for the torrents.'), + 'priority-high': ('array', 1, None, None, None, "A list of file id's that should have high priority."), + 'priority-low': ('array', 1, None, None, None, "A list of file id's that should have normal priority."), + 'priority-normal': ('array', 1, None, None, None, "A list of file id's that should have low priority."), + 'seedIdleLimit': ('number', 10, None, None, None, 'Seed inactivity limit in minutes.'), + 'seedIdleMode': ('number', 10, None, None, None, 'Seed inactivity mode. 0 = Use session limit, 1 = Use transfer limit, 2 = Disable limit.'), + 'seedRatioLimit': ('double', 5, None, None, None, 'Seeding ratio.'), + 'seedRatioMode': ('number', 5, None, None, None, 'Which ratio to use. 0 = Use session limit, 1 = Use transfer limit, 2 = Disable limit.'), + 'speed-limit-down': ('number', 1, 5, None, 'downloadLimit', 'Set the speed limit for download in Kib/s.'), + 'speed-limit-down-enabled': ('boolean', 1, 5, None, 'downloadLimited', 'Enable download speed limiter.'), + 'speed-limit-up': ('number', 1, 5, None, 'uploadLimit', 'Set the speed limit for upload in Kib/s.'), + 'speed-limit-up-enabled': ('boolean', 1, 5, None, 'uploadLimited', 'Enable upload speed limiter.'), + 'trackerAdd': ('array', 10, None, None, None, 'Array of string with announce URLs to add.'), + 'trackerRemove': ('array', 10, None, None, None, 'Array of ids of trackers to remove.'), + 'trackerReplace': ('array', 10, None, None, None, 'Array of (id, url) tuples where the announce URL should be replaced.'), + 'uploadLimit': ('number', 5, None, 'speed-limit-up', None, 'Set the speed limit for upload in Kib/s.'), + 'uploadLimited': ('boolean', 5, None, 'speed-limit-up-enabled', None, 'Enable upload speed limiter.'), }, 'add': { - 'download-dir': ('string', 1, None, None, None), - 'filename': ('string', 1, None, None, None), - 'files-wanted': ('array', 1, None, None, None), - 'files-unwanted': ('array', 1, None, None, None), - 'metainfo': ('string', 1, None, None, None), - 'paused': ('boolean', 1, None, None, None), - 'peer-limit': ('number', 1, None, None, None), - 'priority-high': ('array', 1, None, None, None), - 'priority-low': ('array', 1, None, None, None), - 'priority-normal': ('array', 1, None, None, None), + 'bandwidthPriority': ('number', 8, None, None, None, 'Priority for this transfer.'), + 'download-dir': ('string', 1, None, None, None, 'The directory where the downloaded contents will be saved in.'), + 'filename': ('string', 1, None, None, None, "A filepath or URL to a torrent file or a magnet link."), + 'files-wanted': ('array', 1, None, None, None, "A list of file id's that should be downloaded."), + 'files-unwanted': ('array', 1, None, None, None, "A list of file id's that shouldn't be downloaded."), + 'metainfo': ('string', 1, None, None, None, 'The content of a torrent file, base64 encoded.'), + 'paused': ('boolean', 1, None, None, None, 'If True, does not start the transfer when added.'), + 'peer-limit': ('number', 1, None, None, None, 'Maximum number of peers allowed.'), + 'priority-high': ('array', 1, None, None, None, "A list of file id's that should have high priority."), + 'priority-low': ('array', 1, None, None, None, "A list of file id's that should have low priority."), + 'priority-normal': ('array', 1, None, None, None, "A list of file id's that should have normal priority."), } } # Arguments for session methods SESSION_ARGS = { 'get': { - "alt-speed-down": ('number', 5, None, None, None), - "alt-speed-enabled": ('boolean', 5, None, None, None), - "alt-speed-time-begin": ('number', 5, None, None, None), - "alt-speed-time-enabled": ('boolean', 5, None, None, None), - "alt-speed-time-end": ('number', 5, None, None, None), - "alt-speed-time-day": ('number', 5, None, None, None), - "alt-speed-up": ('number', 5, None, None, None), - "blocklist-enabled": ('boolean', 5, None, None, None), - "blocklist-size": ('number', 5, None, None, None), - "encryption": ('string', 1, None, None, None), - "download-dir": ('string', 1, None, None, None), - "peer-limit": ('number', 1, 5, None, None), - "peer-limit-global": ('number', 5, None, None, None), - "peer-limit-per-torrent": ('number', 5, None, None, None), - "pex-allowed": ('boolean', 1, 5, None, None), - "pex-enabled": ('boolean', 5, None, None, None), - "port": ('number', 1, 5, None, None), - "peer-port": ('number', 5, None, None, None), - "peer-port-random-on-start": ('boolean', 5, None, None, None), - "port-forwarding-enabled": ('boolean', 1, None, None, None), - "rpc-version": ('number', 4, None, None, None), - "rpc-version-minimum": ('number', 4, None, None, None), - "seedRatioLimit": ('double', 5, None, None, None), - "seedRatioLimited": ('boolean', 5, None, None, None), - "speed-limit-down": ('number', 1, None, None, None), - "speed-limit-down-enabled": ('boolean', 1, None, None, None), - "speed-limit-up": ('number', 1, None, None, None), - "speed-limit-up-enabled": ('boolean', 1, None, None, None), - "version": ('string', 3, None, None, None), + "alt-speed-down": ('number', 5, None, None, None, ''), + "alt-speed-enabled": ('boolean', 5, None, None, None, ''), + "alt-speed-time-begin": ('number', 5, None, None, None, ''), + "alt-speed-time-enabled": ('boolean', 5, None, None, None, ''), + "alt-speed-time-end": ('number', 5, None, None, None, ''), + "alt-speed-time-day": ('number', 5, None, None, None, ''), + "alt-speed-up": ('number', 5, None, None, None, ''), + "blocklist-enabled": ('boolean', 5, None, None, None, ''), + "blocklist-size": ('number', 5, None, None, None, ''), + "blocklist-url": ('string', 11, None, None, None, ''), + "cache-size-mb": ('number', 10, None, None, None, ''), + "config-dir": ('string', 8, None, None, None, ''), + "dht-enabled": ('boolean', 6, None, None, None, ''), + "download-dir": ('string', 1, None, None, None, ''), + "encryption": ('string', 1, None, None, None, ''), + "idle-seeding-limit": ('number', 10, None, None, None, ''), + "idle-seeding-limit-enabled": ('boolean', 10, None, None, None, ''), + "incomplete-dir": ('string', 7, None, None, None, ''), + "incomplete-dir-enabled": ('boolean', 7, None, None, None, ''), + "lpd-enabled": ('boolean', 9, None, None, None, ''), + "peer-limit": ('number', 1, 5, None, None, ''), + "peer-limit-global": ('number', 5, None, None, None, ''), + "peer-limit-per-torrent": ('number', 5, None, None, None, ''), + "pex-allowed": ('boolean', 1, 5, None, None, ''), + "pex-enabled": ('boolean', 5, None, None, None, ''), + "port": ('number', 1, 5, None, None, ''), + "peer-port": ('number', 5, None, None, None, ''), + "peer-port-random-on-start": ('boolean', 5, None, None, None, ''), + "port-forwarding-enabled": ('boolean', 1, None, None, None, ''), + "rename-partial-files": ('boolean', 8, None, None, None, ''), + "rpc-version": ('number', 4, None, None, None, ''), + "rpc-version-minimum": ('number', 4, None, None, None, ''), + "script-torrent-done-enabled": ('boolean', 9, None, None, None, ''), + "script-torrent-done-filename": ('string', 9, None, None, None, ''), + "seedRatioLimit": ('double', 5, None, None, None, ''), + "seedRatioLimited": ('boolean', 5, None, None, None, ''), + "speed-limit-down": ('number', 1, None, None, None, ''), + "speed-limit-down-enabled": ('boolean', 1, None, None, None, ''), + "speed-limit-up": ('number', 1, None, None, None, ''), + "speed-limit-up-enabled": ('boolean', 1, None, None, None, ''), + "start-added-torrents": ('boolean', 9, None, None, None, ''), + "trash-original-torrent-files": ('boolean', 9, None, None, None, ''), + 'units': ('object', 10, None, None, None, ''), + "version": ('string', 3, None, None, None, ''), }, 'set': { - "alt-speed-down": ('number', 5, None, None, None), - "alt-speed-enabled": ('boolean', 5, None, None, None), - "alt-speed-time-begin": ('number', 5, None, None, None), - "alt-speed-time-enabled": ('boolean', 5, None, None, None), - "alt-speed-time-end": ('number', 5, None, None, None), - "alt-speed-time-day": ('number', 5, None, None, None), - "alt-speed-up": ('number', 5, None, None, None), - "blocklist-enabled": ('boolean', 5, None, None, None), - "encryption": ('string', 1, None, None, None), - "download-dir": ('string', 1, None, None, None), - "peer-limit": ('number', 1, 5, None, 'peer-limit-global'), - "peer-limit-global": ('number', 5, None, 'peer-limit', None), - "peer-limit-per-torrent": ('number', 5, None, None, None), - "pex-allowed": ('boolean', 1, 5, None, 'pex-enabled'), - "pex-enabled": ('boolean', 5, None, 'pex-allowed', None), - "port": ('number', 1, 5, None, 'peer-port'), - "peer-port": ('number', 5, None, 'port', None), - "peer-port-random-on-start": ('boolean', 5, None, None, None), - "port-forwarding-enabled": ('boolean', 1, None, None, None), - "seedRatioLimit": ('double', 5, None, None, None), - "seedRatioLimited": ('boolean', 5, None, None, None), - "speed-limit-down": ('number', 1, None, None, None), - "speed-limit-down-enabled": ('boolean', 1, None, None, None), - "speed-limit-up": ('number', 1, None, None, None), - "speed-limit-up-enabled": ('boolean', 1, None, None, None), + "alt-speed-down": ('number', 5, None, None, None, 'Alternate session download speed limit (in Kib/s).'), + "alt-speed-enabled": ('boolean', 5, None, None, None, 'Enables alternate global download speed limiter.'), + "alt-speed-time-begin": ('number', 5, None, None, None, 'Time when alternate speeds should be enabled. Minutes after midnight.'), + "alt-speed-time-enabled": ('boolean', 5, None, None, None, 'Enables alternate speeds scheduling.'), + "alt-speed-time-end": ('number', 5, None, None, None, 'Time when alternate speeds should be disabled. Minutes after midnight.'), + "alt-speed-time-day": ('number', 5, None, None, None, 'Enables alternate speeds scheduling these days.'), + "alt-speed-up": ('number', 5, None, None, None, 'Alternate session upload speed limit (in Kib/s).'), + "blocklist-enabled": ('boolean', 5, None, None, None, 'Enables the block list'), + "blocklist-url": ('string', 11, None, None, None, 'Location of the blocklist. Updated with blocklist-update.'), + "cache-size-mb": ('number', 10, None, None, None, 'The maximum size of the disk cache in MB'), + "dht-enabled": ('boolean', 6, None, None, None, 'Enables DHT.'), + "download-dir": ('string', 1, None, None, None, 'Set the session download directory.'), + "encryption": ('string', 1, None, None, None, 'Set the session encryption mode, one of ``required``, ``preferred`` or ``tolerated``.'), + "idle-seeding-limit": ('number', 10, None, None, None, 'The default seed inactivity limit in minutes.'), + "idle-seeding-limit-enabled": ('boolean', 10, None, None, None, 'Enables the default seed inactivity limit'), + "incomplete-dir": ('string', 7, None, None, None, 'The path to the directory of incomplete transfer data.'), + "incomplete-dir-enabled": ('boolean', 7, None, None, None, 'Enables the incomplete transfer data directory. Otherwise data for incomplete transfers are stored in the download target.'), + "lpd-enabled": ('boolean', 9, None, None, None, 'Enables local peer discovery for public torrents.'), + "peer-limit": ('number', 1, 5, None, 'peer-limit-global', 'Maximum number of peers'), + "peer-limit-global": ('number', 5, None, 'peer-limit', None, 'Maximum number of peers'), + "peer-limit-per-torrent": ('number', 5, None, None, None, 'Maximum number of peers per transfer'), + "pex-allowed": ('boolean', 1, 5, None, 'pex-enabled', 'Allowing PEX in public torrents.'), + "pex-enabled": ('boolean', 5, None, 'pex-allowed', None, 'Allowing PEX in public torrents.'), + "port": ('number', 1, 5, None, 'peer-port', 'Peer port.'), + "peer-port": ('number', 5, None, 'port', None, 'Peer port.'), + "peer-port-random-on-start": ('boolean', 5, None, None, None, 'Enables randomized peer port on start of Transmission.'), + "port-forwarding-enabled": ('boolean', 1, None, None, None, 'Enables port forwarding.'), + "rename-partial-files": ('boolean', 8, None, None, None, 'Appends ".part" to incomplete files'), + "script-torrent-done-enabled": ('boolean', 9, None, None, None, 'Whether or not to call the "done" script.'), + "script-torrent-done-filename": ('string', 9, None, None, None, 'Filename of the script to run when the transfer is done.'), + "seedRatioLimit": ('double', 5, None, None, None, 'Seed ratio limit. 1.0 means 1:1 download and upload ratio.'), + "seedRatioLimited": ('boolean', 5, None, None, None, 'Enables seed ration limit.'), + "speed-limit-down": ('number', 1, None, None, None, 'Download speed limit (in Kib/s).'), + "speed-limit-down-enabled": ('boolean', 1, None, None, None, 'Enables download speed limiting.'), + "speed-limit-up": ('number', 1, None, None, None, 'Upload speed limit (in Kib/s).'), + "speed-limit-up-enabled": ('boolean', 1, None, None, None, 'Enables upload speed limiting.'), + "start-added-torrents": ('boolean', 9, None, None, None, 'Added torrents will be started right away.'), + "trash-original-torrent-files": ('boolean', 9, None, None, None, 'The .torrent file of added torrents will be deleted.'), }, } diff --git a/resources/lib/transmissionrpc/error.py b/resources/lib/transmissionrpc/error.py new file mode 100755 index 0000000..8fb2a57 --- /dev/null +++ b/resources/lib/transmissionrpc/error.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2008-2010 Erik Svensson +# Licensed under the MIT license. + +class TransmissionError(Exception): + """ + This exception is raised when there has occured an error related to + communication with Transmission. It is a subclass of Exception. + """ + def __init__(self, message='', original=None): + Exception.__init__(self) + self.message = message + self.original = original + + def __str__(self): + if self.original: + original_name = type(self.original).__name__ + return '%s Original exception: %s, "%s"' % (self.message, original_name, str(self.original)) + else: + return self.message + +class HTTPHandlerError(Exception): + """ + This exception is raised when there has occured an error related to + the HTTP handler. It is a subclass of Exception. + """ + def __init__(self, httpurl=None, httpcode=None, httpmsg=None, httpheaders=None, httpdata=None): + Exception.__init__(self) + self.url = '' + self.code = 600 + self.message = '' + self.headers = {} + self.data = '' + if isinstance(httpurl, (str, unicode)): + self.url = httpurl + if isinstance(httpcode, (int, long)): + self.code = httpcode + if isinstance(httpmsg, (str, unicode)): + self.message = httpmsg + if isinstance(httpheaders, (dict)): + self.headers = httpheaders + if isinstance(httpdata, (str, unicode)): + self.data = httpdata + + def __repr__(self): + return '' % (self.code, self.message) + + def __str__(self): + return '' % (self.code, self.message) + + def __unicode__(self): + return u'' % (self.code, self.message) diff --git a/resources/lib/transmissionrpc/httphandler.py b/resources/lib/transmissionrpc/httphandler.py new file mode 100755 index 0000000..e4933d4 --- /dev/null +++ b/resources/lib/transmissionrpc/httphandler.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2010 Erik Svensson +# Licensed under the MIT license. + +import sys, httplib, urllib2 + +from transmissionrpc.error import HTTPHandlerError + +class HTTPHandler(object): + """ + Prototype for HTTP handling. + """ + def set_authentication(self, uri, login, password): + """ + Transmission use basic authentication in earlier versions and digest + authentication in later versions. + + * uri, the authentication realm URI. + * login, the authentication login. + * password, the authentication password. + """ + raise NotImplementedError("Bad HTTPHandler, failed to implement set_authentication.") + + def request(self, url, query, headers, timeout): + """ + Implement a HTTP POST request here. + + * url, The URL to request. + * query, The query data to send. This is a JSON data string. + * headers, a dictionary of headers to send. + * timeout, requested request timeout in seconds. + """ + raise NotImplementedError("Bad HTTPHandler, failed to implement request.") + +class DefaultHTTPHandler(HTTPHandler): + """ + The default HTTP handler provided with transmissionrpc. + """ + def __init__(self): + HTTPHandler.__init__(self) + + def set_authentication(self, uri, login, password): + password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() + password_manager.add_password(realm=None, uri=uri, user=login, passwd=password) + opener = urllib2.build_opener( + urllib2.HTTPBasicAuthHandler(password_manager) + , urllib2.HTTPDigestAuthHandler(password_manager) + ) + urllib2.install_opener(opener) + + def request(self, url, query, headers, timeout): + request = urllib2.Request(url, query, headers) + try: + if (sys.version_info[0] == 2 and sys.version_info[1] > 5) or sys.version_info[0] > 2: + response = urllib2.urlopen(request, timeout=timeout) + else: + response = urllib2.urlopen(request) + except urllib2.HTTPError, error: + if error.fp == None: + raise HTTPHandlerError(error.filename, error.code, error.msg, dict(error.hdrs)) + else: + raise HTTPHandlerError(error.filename, error.code, error.msg, dict(error.hdrs), error.read()) + except urllib2.URLError, error: + # urllib2.URLError documentation is absymal! + # Try to get the tuple arguments of URLError + if hasattr(error.reason, 'args') and isinstance(error.reason.args, tuple) and len(error.reason.args) == 2: + raise HTTPHandlerError(httpcode=error.reason.args[0], httpmsg=error.reason.args[1]) + else: + raise HTTPHandlerError(httpmsg='urllib2.URLError: %s' % (error.reason)) + except httplib.BadStatusLine, error: + raise HTTPHandlerError(httpmsg='httplib.BadStatusLine: %s' % (error.line)) + return response.read() diff --git a/resources/lib/transmissionrpc/session.py b/resources/lib/transmissionrpc/session.py new file mode 100755 index 0000000..56e842c --- /dev/null +++ b/resources/lib/transmissionrpc/session.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2008-2010 Erik Svensson +# Licensed under the MIT license. + +class Session(object): + """ + Session is a class holding the session data for a Transmission daemon. + + Access the session field can be done through attributes. + The attributes available are the same as the session arguments in the + Transmission RPC specification, but with underscore instead of hypen. + ``download-dir`` -> ``download_dir``. + """ + + def __init__(self, fields=None): + self.fields = {} + if fields != None: + self.update(fields) + + def update(self, other): + """Update the session data from a session arguments dictinary""" + + fields = None + if isinstance(other, dict): + fields = other + elif isinstance(other, Session): + fields = other.fields + else: + raise ValueError('Cannot update with supplied data') + + for key, value in fields.iteritems(): + self.fields[key.replace('-', '_')] = value + + def __getattr__(self, name): + try: + return self.fields[name] + except KeyError: + raise AttributeError('No attribute %s' % name) + + def __str__(self): + text = '' + for key in sorted(self.fields.keys()): + text += "% 32s: %s\n" % (key[-32:], self.fields[key]) + return text diff --git a/resources/lib/transmissionrpc/torrent.py b/resources/lib/transmissionrpc/torrent.py new file mode 100755 index 0000000..7b86bbf --- /dev/null +++ b/resources/lib/transmissionrpc/torrent.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2008-2010 Erik Svensson +# Licensed under the MIT license. + +import sys, datetime + +from transmissionrpc.constants import STATUS, PRIORITY +from transmissionrpc.utils import format_timedelta + +class Torrent(object): + """ + Torrent is a class holding the data raceived from Transmission regarding a bittorrent transfer. + All fetched torrent fields are accessable through this class using attributes. + This class has a few convenience properties using the torrent data. + """ + + def __init__(self, client, fields): + if 'id' not in fields: + raise ValueError('Torrent requires an id') + self.fields = {} + self.update(fields) + self.client = client + + def _getNameString(self, codec=None): + if codec == None: + codec = sys.getdefaultencoding() + name = None + # try to find name + if 'name' in self.fields: + name = self.fields['name'] + # if name is unicode, try to decode + if isinstance(name, unicode): + try: + name = name.encode(codec) + except UnicodeError: + name = None + return name + + def __repr__(self): + tid = self.fields['id'] + name = self._getNameString() + if isinstance(name, str): + return '' % (tid, name) + else: + return '' % (tid) + + def __str__(self): + name = self._getNameString() + if isinstance(name, str): + return 'Torrent \"%s\"' % (name) + else: + return 'Torrent' + + def __copy__(self): + return Torrent(self.client, self.fields) + + def update(self, other): + """ + Update the torrent data from a Transmission JSON-RPC arguments dictinary + """ + fields = None + if isinstance(other, dict): + fields = other + elif isinstance(other, Torrent): + fields = other.fields + else: + raise ValueError('Cannot update with supplied data') + for key, value in fields.iteritems(): + self.fields[key.replace('-', '_')] = value + + def files(self): + """ + Get list of files for this torrent. + + This function returns a dictionary with file information for each file. + The file information is has following fields: + :: + + { + : { + 'name': , + 'size': , + 'completed': , + 'priority': , + 'selected': + } + + ... + } + """ + result = {} + if 'files' in self.fields: + indicies = xrange(len(self.fields['files'])) + files = self.fields['files'] + priorities = self.fields['priorities'] + wanted = self.fields['wanted'] + for item in zip(indicies, files, priorities, wanted): + selected = True if item[3] else False + priority = PRIORITY[item[2]] + result[item[0]] = { + 'selected': selected, + 'priority': priority, + 'size': item[1]['length'], + 'name': item[1]['name'], + 'completed': item[1]['bytesCompleted']} + return result + + def __getattr__(self, name): + try: + return self.fields[name] + except KeyError: + raise AttributeError('No attribute %s' % name) + + @property + def status(self): + """ + Returns the torrent status. Is either one of 'check pending', 'checking', + 'downloading', 'seeding' or 'stopped'. The first two is related to + verification. + """ + return STATUS[self.fields['status']] + + @property + def progress(self): + """Get the download progress in percent.""" + try: + return 100.0 * (self.fields['sizeWhenDone'] - self.fields['leftUntilDone']) / float(self.fields['sizeWhenDone']) + except ZeroDivisionError: + return 0.0 + + @property + def ratio(self): + """Get the upload/download ratio.""" + try: + return self.fields['uploadedEver'] / float(self.fields['downloadedEver']) + except ZeroDivisionError: + return 0.0 + + @property + def eta(self): + """Get the "eta" as datetime.timedelta.""" + eta = self.fields['eta'] + if eta >= 0: + return datetime.timedelta(seconds=eta) + else: + ValueError('eta not valid') + + @property + def date_active(self): + """Get the attribute "activityDate" as datetime.datetime.""" + return datetime.datetime.fromtimestamp(self.fields['activityDate']) + + @property + def date_added(self): + """Get the attribute "addedDate" as datetime.datetime.""" + return datetime.datetime.fromtimestamp(self.fields['addedDate']) + + @property + def date_started(self): + """Get the attribute "startDate" as datetime.datetime.""" + return datetime.datetime.fromtimestamp(self.fields['startDate']) + + @property + def date_done(self): + """Get the attribute "doneDate" as datetime.datetime.""" + return datetime.datetime.fromtimestamp(self.fields['doneDate']) + + def format_eta(self): + """ + Returns the attribute *eta* formatted as a string. + + * If eta is -1 the result is 'not available' + * If eta is -2 the result is 'unknown' + * Otherwise eta is formatted as ::. + """ + eta = self.fields['eta'] + if eta == -1: + return 'not available' + elif eta == -2: + return 'unknown' + else: + return format_timedelta(self.eta) + + @property + def priority(self): + """ + Get the priority as string. + Can be one of 'low', 'normal', 'high'. + """ + return PRIORITY[self.fields['bandwidthPriority']] diff --git a/resources/lib/transmissionrpc/transmission.py b/resources/lib/transmissionrpc/transmission.py deleted file mode 100755 index fb3867d..0000000 --- a/resources/lib/transmissionrpc/transmission.py +++ /dev/null @@ -1,606 +0,0 @@ -# -*- coding: utf-8 -*- -# 2008-07, Erik Svensson - -import sys, os, time, datetime -import re -import httplib, urllib2, base64, socket - -try: - import json -except ImportError: - import simplejson as json - -from constants import * -from utils import * - -class TransmissionError(Exception): - def __init__(self, message='', original=None): - Exception.__init__(self, message) - self.message = message - self.original = original - - def __str__(self): - if self.original: - original_name = type(self.original).__name__ - return '%s Original exception: %s, "%s"' % (self.message, original_name, self.original.args) - else: - return self.args - -class Torrent(object): - """ - Torrent is a class holding the data raceived from Transmission regarding a bittorrent transfer. - All fetched torrent fields are accessable through this class using attributes. - This class has a few convenience properties using the torrent data. - """ - - def __init__(self, fields): - if 'id' not in fields: - raise ValueError('Torrent requires an id') - self.fields = {} - self.update(fields) - - def __repr__(self): - return '' % (self.fields['id'], self.fields['name']) - - def __str__(self): - return 'torrent %s' % self.fields['name'] - - def update(self, other): - """Update the torrent data from a Transmission arguments dictinary""" - fields = None - if isinstance(other, dict): - fields = other - elif isinstance(other, Torrent): - fields = other.fields - else: - raise ValueError('Cannot update with supplied data') - for k, v in fields.iteritems(): - self.fields[k.replace('-', '_')] = v - - def files(self): - """ - Get list of files for this torrent. This function returns a dictionary with file information for each file. - """ - result = {} - if 'files' in self.fields: - indicies = xrange(len(self.fields['files'])) - files = self.fields['files'] - priorities = self.fields['priorities'] - wanted = self.fields['wanted'] - index = 1 - for item in zip(indicies, files, priorities, wanted): - selected = bool(item[3]) - priority = PRIORITY[item[2]] - result[item[0]] = { - 'selected': selected, - 'priority': priority, - 'size': item[1]['length'], - 'name': item[1]['name'], - 'completed': item[1]['bytesCompleted']} - return result - - def __getattr__(self, name): - try: - return self.fields[name] - except KeyError, e: - raise AttributeError('No attribute %s' % name) - - @property - def status(self): - """Get the status as string.""" - return STATUS[self.fields['status']] - - @property - def progress(self): - """Get the download progress in percent as float.""" - try: - return 100.0 * (self.fields['sizeWhenDone'] - self.fields['leftUntilDone']) / float(self.fields['sizeWhenDone']) - except ZeroDivisionError: - return 0.0 - - @property - def ratio(self): - """Get the upload/download ratio.""" - try: - return self.fields['uploadedEver'] / float(self.fields['downloadedEver']) - except ZeroDivisionError: - return 0.0 - - @property - def eta(self): - """Get the "eta" as datetime.timedelta.""" - eta = self.fields['eta'] - if eta >= 0: - return datetime.timedelta(seconds=eta) - else: - ValueError('eta not valid') - - @property - def date_active(self): - """Get the attribute "activityDate" as datetime.datetime.""" - return datetime.datetime.fromtimestamp(self.fields['activityDate']) - - @property - def date_added(self): - """Get the attribute "addedDate" as datetime.datetime.""" - return datetime.datetime.fromtimestamp(self.fields['addedDate']) - - @property - def date_started(self): - """Get the attribute "startDate" as datetime.datetime.""" - return datetime.datetime.fromtimestamp(self.fields['startDate']) - - @property - def date_done(self): - """Get the attribute "doneDate" as datetime.datetime.""" - return datetime.datetime.fromtimestamp(self.fields['doneDate']) - - def format_eta(self): - """Returns the attribute "eta" formatted as a string.""" - eta = self.fields['eta'] - if eta == -1: - return 'not available' - elif eta == -2: - return 'unknown' - else: - return format_timedelta(self.eta) - -class Session(object): - """ - Session is a class holding the session data for a Transmission daemon. - - Access the session field can be done through attributes. - The attributes available are the same as the session arguments in the - Transmission RPC specification, but with underscore instead of hypen. - ``download-dir`` -> ``download_dir``. - """ - - def __init__(self, fields={}): - self.fields = {} - self.update(fields) - - def update(self, other): - """Update the session data from a session arguments dictinary""" - - fields = None - if isinstance(other, dict): - fields = other - elif isinstance(other, Session): - fields = other.fields - else: - raise ValueError('Cannot update with supplied data') - - for k, v in fields.iteritems(): - self.fields[k.replace('-', '_')] = v - - def __getattr__(self, name): - try: - return self.fields[name] - except KeyError, e: - raise AttributeError('No attribute %s' % name) - - def __str__(self): - text = '' - for k, v in self.fields.iteritems(): - text += "% 32s: %s\n" % (k[-32:], v) - return text - -class Client(object): - """ - This is it. This class implements the json-RPC protocol to communicate with Transmission. - """ - - def __init__(self, address='localhost', port=DEFAULT_PORT, user=None, password=None): - base_url = 'http://' + address + ':' + str(port) - self.url = base_url + '/transmission/rpc' - if user and password: - password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() - password_manager.add_password(realm=None, uri=self.url, user=user, passwd=password) - opener = urllib2.build_opener( - urllib2.HTTPBasicAuthHandler(password_manager) - , urllib2.HTTPDigestAuthHandler(password_manager) - ) - urllib2.install_opener(opener) - elif user or password: - logger.warning('Either user or password missing, not using authentication.') - self._sequence = 0 - self.session = Session() - self.sessionid = 0 - self.protocol_version = None - self.get_session() - self.torrent_get_arguments = get_arguments('torrent-get' - , self.rpc_version) - - def _debug_request(self, request): - logger.debug( - json.dumps( - { - 'request': { - 'url': request.get_full_url(), - 'request-headers': dict(request.header_items()), - 'request-data': json.loads(request.data), - } - }, - indent=2 - ) - ) - - def _debug_response(self, response, response_data): - try: - response_data = json.loads(response_data) - except: - pass - logger.debug( - json.dumps( - { - 'response': { - 'url': response.url, - 'code': response.code, - 'msg': response.msg, - 'headers': dict(response.headers), - 'data': response_data, - } - }, - indent=2 - ) - ) - - def _http_query(self, query): - headers = {'X-Transmission-Session-Id': self.sessionid} - request = urllib2.Request(self.url, query, headers) - request_count = 0 - while True: - error_data = "" - try: - try: - self._debug_request(request) - socket.setdefaulttimeout(10) - if (sys.version_info[0] == 2 and sys.version_info[1] > 5) or sys.version_info[0] > 2: - response = urllib2.urlopen(request, timeout=60) - else: - response = urllib2.urlopen(request) - break - except urllib2.HTTPError, error: - error_data = error.read() - if error.code == 409: - logger.info('Server responded with 409, trying to set session-id.') - if request_count > 1: - raise TransmissionError('Session ID negotiation failed.', error) - if 'X-Transmission-Session-Id' in error.headers: - self.sessionid = error.headers['X-Transmission-Session-Id'] - request.add_header('X-Transmission-Session-Id', self.sessionid) - else: - raise TransmissionError('Unknown conflict.', error) - except urllib2.URLError, error: - raise TransmissionError('Failed to connect to daemon.', error) - except httplib.BadStatusLine, error: - if (request_count > 1): - raise TransmissionError('Failed to request %s "%s".' % (self.url, query), error) - finally: - if error_data: - self._debug_response(error, error_data) - request_count = request_count + 1 - result = response.read() - self._debug_response(response, result) - return result - - def _request(self, method, arguments={}, ids=[], require_ids = False): - """Send json-rpc request to Transmission using http POST""" - - if not isinstance(method, (str, unicode)): - raise ValueError('request takes method as string') - if not isinstance(arguments, dict): - raise ValueError('request takes arguments as dict') - ids = self._format_ids(ids) - if len(ids) > 0: - arguments['ids'] = ids - elif require_ids: - raise ValueError('request require ids') - - query = json.dumps({'tag': self._sequence, 'method': method - , 'arguments': arguments}) - logger.info(query) - self._sequence += 1 - start = time.time() - http_data = self._http_query(query) - elapsed = time.time() - start - logger.info('http request took %.3f s' % (elapsed)) - - try: - data = json.loads(http_data) - except ValueError, e: - logger.error('Error: ' + str(e)) - logger.error('Request: \"%s\"' % (query)) - logger.error('HTTP data: \"%s\"' % (http_data)) - raise - - logger.info(json.dumps(data, indent=2)) - - if data['result'] != 'success': - raise TransmissionError('Query failed with result \"%s\"' - % data['result']) - - results = {} - if method == 'torrent-get': - for item in data['arguments']['torrents']: - results[item['id']] = Torrent(item) - if self.protocol_version == 2 and 'peers' not in item: - self.protocol_version = 1 - elif method == 'torrent-add': - item = data['arguments']['torrent-added'] - results[item['id']] = Torrent(item) - elif method == 'session-get': - self._update_session(data['arguments']) - elif method == 'session-stats': - # older versions of T has the return data in "session-stats" - if 'session-stats' in data['arguments']: - self._update_session(data['arguments']['session-stats']) - else: - self._update_session(data['arguments']) - elif method in ('port-test', 'blocklist-update'): - results = data['arguments'] - else: - return None - - return results - - def _format_ids(self, args): - """Take things and make them valid torrent identifiers""" - ids = [] - - if isinstance(args, (int, long)): - ids.append(args) - elif isinstance(args, (str, unicode)): - for item in re.split(u'[ ,]+', args): - if len(item) == 0: - continue - addition = None - try: - # handle index - addition = [int(item)] - except ValueError: - pass - if not addition: - # handle hashes - try: - int(item, 16) - addition = [item] - except: - pass - if not addition: - # handle index ranges i.e. 5:10 - match = re.match(u'^(\d+):(\d+)$', item) - if match: - try: - idx_from = int(match.group(1)) - idx_to = int(match.group(2)) - addition = range(idx_from, idx_to + 1) - except: - pass - if not addition: - raise ValueError(u'Invalid torrent id, \"%s\"' % item) - ids.extend(addition) - elif isinstance(args, (list)): - for item in args: - ids.extend(self._format_ids(item)) - else: - raise ValueError(u'Invalid torrent id') - return ids - - def _update_session(self, data): - self.session.update(data) - - @property - def rpc_version(self): - if self.protocol_version == None: - if hasattr(self.session, 'rpc_version'): - self.protocol_version = self.session.rpc_version - elif hasattr(self.session, 'version'): - self.protocol_version = 3 - else: - self.protocol_version = 2 - return self.protocol_version - - def _rpc_version_warning(self, version): - if self.rpc_version < version: - logger.warning('Using feature not supported by server. RPC version for server %d, feature introduced in %d.' % (self.rpc_version, version)) - - def add(self, data, **kwargs): - """ - Add torrent to transfers list. Takes a base64 encoded .torrent file in data. - Additional arguments are: - - * `paused`, boolean, Whether to pause the transfer on add. - * `download_dir`, path, The directory where the downloaded - contents will be saved in. - * `peer_limit`, number, Limits the number of peers for this - transfer. - * `files_unwanted`, - * `files_wanted`, - * `priority_high`, - * `priority_low`, - * `priority_normal`, - """ - args = {'metainfo': data} - for key, value in kwargs.iteritems(): - argument = make_rpc_name(key) - (arg, val) = argument_value_convert('torrent-add', - argument, value, self.rpc_version) - args[arg] = val - return self._request('torrent-add', args) - - def add_url(self, torrent_url, **kwargs): - """ - Add torrent to transfers list. Takes a url to a .torrent file. - Additional arguments are: - - * `paused`, boolean, Whether to pause the transfer on add. - * `download_dir`, path, The directory where the downloaded - contents will be saved in. - * `peer_limit`, number, Limits the number of peers for this - transfer. - * `files_unwanted`, - * `files_wanted`, - * `priority_high`, - * `priority_low`, - * `priority_normal`, - """ - torrent_file = None - if os.path.exists(torrent_url): - torrent_file = open(torrent_url, 'r') - else: - try: - torrent_file = urllib2.urlopen(torrent_url) - except: - torrent_file = None - - if not torrent_file: - raise TransmissionError('File does not exist.') - - torrent_data = base64.b64encode(torrent_file.read()) - return self.add(torrent_data, **kwargs) - - def remove(self, ids, delete_data=False): - """ - remove torrent(s) with provided id(s). Local data is removed if - delete_data is True, otherwise not. - """ - self._rpc_version_warning(3) - self._request('torrent-remove', - {'delete-local-data':rpc_bool(delete_data)}, ids, True) - - def start(self, ids): - """start torrent(s) with provided id(s)""" - self._request('torrent-start', {}, ids, True) - - def stop(self, ids): - """stop torrent(s) with provided id(s)""" - self._request('torrent-stop', {}, ids, True) - - def verify(self, ids): - """verify torrent(s) with provided id(s)""" - self._request('torrent-verify', {}, ids, True) - - def reannounce(self, ids): - """reannounce torrent(s) with provided id(s)""" - self._rpc_version_warning(5) - self._request('torrent-reannounce', {}, ids, True) - - def info(self, ids=[], arguments={}): - """Get detailed information for torrent(s) with provided id(s).""" - if not arguments: - arguments = self.torrent_get_arguments - return self._request('torrent-get', {'fields': arguments}, ids) - - def get_files(self, ids=[]): - """ - Get list of files for provided torrent id(s). - This function returns a dictonary for each requested torrent id holding - the information about the files. - """ - fields = ['id', 'name', 'hashString', 'files', 'priorities', 'wanted'] - request_result = self._request('torrent-get', {'fields': fields}, ids) - result = {} - for id, torrent in request_result.iteritems(): - result[id] = torrent.files() - return result - - def set_files(self, items): - """ - Set file properties. Takes a dictonary with similar contents as the - result of get_files. - """ - if not isinstance(items, dict): - raise ValueError('Invalid file description') - for tid, files in items.iteritems(): - if not isinstance(files, dict): - continue - wanted = [] - unwanted = [] - priority_high = [] - priority_normal = [] - priority_low = [] - for fid, file in files.iteritems(): - if not isinstance(file, dict): - continue - if 'selected' in file and file['selected']: - wanted.append(fid) - else: - unwanted.append(fid) - if 'priority' in file: - if file['priority'] == 'high': - priority_high.append(fid) - elif file['priority'] == 'normal': - priority_normal.append(fid) - elif file['priority'] == 'low': - priority_low.append(fid) - self.change([tid], files_wanted = wanted - , files_unwanted = unwanted - , priority_high = priority_high - , priority_normal = priority_normal - , priority_low = priority_low) - - def list(self): - """list all torrents""" - fields = ['id', 'hashString', 'name', 'sizeWhenDone', 'leftUntilDone' - , 'eta', 'status', 'rateUpload', 'rateDownload', 'uploadedEver' - , 'downloadedEver'] - return self._request('torrent-get', {'fields': fields}) - - def change(self, ids, **kwargs): - """ - Change torrent parameters. This is the list of parameters that. - """ - args = {} - for key, value in kwargs.iteritems(): - argument = make_rpc_name(key) - (arg, val) = argument_value_convert('torrent-set' - , argument, value, self.rpc_version) - args[arg] = val - - if len(args) > 0: - self._request('torrent-set', args, ids, True) - else: - ValueError("No arguments to set") - - def get_session(self): - """Get session parameters""" - self._request('session-get') - return self.session - - def set_session(self, **kwargs): - """Set session parameters""" - args = {} - for key, value in kwargs.iteritems(): - if key == 'encryption' and value not in ['required', 'preferred', 'tolerated']: - raise ValueError('Invalid encryption value') - argument = make_rpc_name(key) - (arg, val) = argument_value_convert('session-set' - , argument, value, self.rpc_version) - args[arg] = val - if len(args) > 0: - self._request('session-set', args) - - def blocklist_update(self): - """Update block list. Returns the size of the block list.""" - self._rpc_version_warning(5) - result = self._request('blocklist-update') - if 'blocklist-size' in result: - return result['blocklist-size'] - return None - - def port_test(self): - """ - Tests to see if your incoming peer port is accessible from the - outside world. - """ - self._rpc_version_warning(5) - result = self._request('port-test') - if 'port-is-open' in result: - return result['port-is-open'] - return None - - def session_stats(self): - """Get session statistics""" - self._request('session-stats') - return self.session diff --git a/resources/lib/transmissionrpc/utils.py b/resources/lib/transmissionrpc/utils.py old mode 100644 new mode 100755 index 3e4099e..8bfd692 --- a/resources/lib/transmissionrpc/utils.py +++ b/resources/lib/transmissionrpc/utils.py @@ -1,14 +1,18 @@ # -*- coding: utf-8 -*- -# 2008-07, Erik Svensson +# Copyright (c) 2008-2010 Erik Svensson +# Licensed under the MIT license. -import socket, datetime -import constants -from constants import logger +import socket, datetime, logging +import transmissionrpc.constants as constants +from transmissionrpc.constants import LOGGER UNITS = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB'] def format_size(size): - s = float(size) + """ + Format byte size into IEC prefixes, B, KiB, MiB ... + """ + size = float(size) i = 0 while size >= 1024.0 and i < len(UNITS): i += 1 @@ -16,58 +20,75 @@ def format_size(size): return (size, UNITS[i]) def format_speed(size): + """ + Format bytes per second speed into IEC prefixes, B/s, KiB/s, MiB/s ... + """ (size, unit) = format_size(size) return (size, unit + '/s') def format_timedelta(delta): + """ + Format datetime.timedelta into ::. + """ minutes, seconds = divmod(delta.seconds, 60) hours, minutes = divmod(minutes, 60) return '%d %02d:%02d:%02d' % (delta.days, hours, minutes, seconds) def format_timestamp(timestamp): + """ + Format unix timestamp into ISO date format. + """ if timestamp > 0: - dt = datetime.datetime.fromtimestamp(timestamp) - return dt.isoformat(' ') + dt_timestamp = datetime.datetime.fromtimestamp(timestamp) + return dt_timestamp.isoformat(' ') else: return '-' class INetAddressError(Exception): + """ + Error parsing / generating a internet address. + """ pass def inet_address(address, default_port, default_address='localhost'): + """ + Parse internet address. + """ addr = address.split(':') if len(addr) == 1: try: port = int(addr[0]) addr = default_address - except: + except ValueError: addr = addr[0] port = default_port elif len(addr) == 2: - port = int(addr[1]) + try: + port = int(addr[1]) + except ValueError: + raise INetAddressError('Invalid address "%s".' % address) if len(addr[0]) == 0: addr = default_address else: addr = addr[0] else: - addr = default_address - port = default_port + raise INetAddressError('Invalid address "%s".' % address) try: socket.getaddrinfo(addr, port, socket.AF_INET, socket.SOCK_STREAM) - except socket.gaierror, e: + except socket.gaierror: raise INetAddressError('Cannot look up address "%s".' % address) return (addr, port) def rpc_bool(arg): + """ + Convert between Python boolean and Transmission RPC boolean. + """ if isinstance(arg, (str, unicode)): try: arg = bool(int(arg)) - except: + except ValueError: arg = arg.lower() in [u'true', u'yes'] - if bool(arg): - return 1 - else: - return 0 + return 1 if bool(arg) else 0 TR_TYPE_MAP = { 'number' : int, @@ -79,12 +100,21 @@ TR_TYPE_MAP = { } def make_python_name(name): + """ + Convert Transmission RPC name to python compatible name. + """ return name.replace('-', '_') def make_rpc_name(name): + """ + Convert python compatible name to Transmission RPC name. + """ return name.replace('_', '-') def argument_value_convert(method, argument, value, rpc_version): + """ + Check and fix Transmission RPC issues with regards to methods, arguments and values. + """ if method in ('torrent-add', 'torrent-get', 'torrent-set'): args = constants.TORRENT_ARGS[method[-3:]] elif method in ('session-get', 'session-set'): @@ -105,7 +135,7 @@ def argument_value_convert(method, argument, value, rpc_version): replacement = info[4] if invalid_version: if replacement: - logger.warning( + LOGGER.warning( 'Replacing requested argument "%s" with "%s".' % (argument, replacement)) argument = replacement @@ -120,6 +150,9 @@ def argument_value_convert(method, argument, value, rpc_version): (argument, method)) def get_arguments(method, rpc_version): + """ + Get arguments for method in specified Transmission RPC version. + """ if method in ('torrent-add', 'torrent-get', 'torrent-set'): args = constants.TORRENT_ARGS[method[-3:]] elif method in ('session-get', 'session-set'): @@ -136,3 +169,17 @@ def get_arguments(method, rpc_version): if valid_version: accessible.append(argument) return accessible + +def add_stdout_logger(level='debug'): + """ + Add a stdout target for the transmissionrpc logging. + """ + levels = {'debug': logging.DEBUG, 'info': logging.INFO, 'warning': logging.WARNING, 'error': logging.ERROR} + + trpc_logger = logging.getLogger('transmissionrpc') + loghandler = logging.StreamHandler() + if level in levels.keys(): + loglevel = levels[level] + trpc_logger.setLevel(loglevel) + loghandler.setLevel(loglevel) + trpc_logger.addHandler(loghandler)