Update transmissionrpc to version 0.11

This commit is contained in:
Correl Roush 2014-07-26 20:39:57 -04:00
parent f341a8a125
commit 13dc11f94d
10 changed files with 2179 additions and 1625 deletions

View file

@ -7,6 +7,7 @@
<import addon="xbmc.python" version="2.14.0"/> <import addon="xbmc.python" version="2.14.0"/>
<import addon="script.module.simplejson" version="3.3.0"/> <import addon="script.module.simplejson" version="3.3.0"/>
<import addon="script.module.beautifulsoup" version="3.0.8" /> <import addon="script.module.beautifulsoup" version="3.0.8" />
<import addon="script.module.six" version="1.5.2" />
</requires> </requires>
<extension point="xbmc.python.script" <extension point="xbmc.python.script"
library="default.py" /> library="default.py" />

View file

@ -149,7 +149,7 @@ class TransmissionGUI(xbmcgui.WindowXMLDialog):
if selected < 0: if selected < 0:
return return
try: try:
self.transmission.add_uri(results[selected]['url']) self.transmission.add_torrent(results[selected]['url'])
except: except:
xbmcgui.Dialog().ok(_(0), _(293)) xbmcgui.Dialog().ok(_(0), _(293))
return return

14
resources/lib/transmissionrpc/__init__.py Normal file → Executable file
View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2008-2011 Erik Svensson <erik.public@gmail.com> # Copyright (c) 2008-2013 Erik Svensson <erik.public@gmail.com>
# Licensed under the MIT license. # Licensed under the MIT license.
from transmissionrpc.constants import DEFAULT_PORT, DEFAULT_TIMEOUT, PRIORITY, RATIO_LIMIT, LOGGER from transmissionrpc.constants import DEFAULT_PORT, DEFAULT_TIMEOUT, PRIORITY, RATIO_LIMIT, LOGGER
@ -8,9 +8,11 @@ from transmissionrpc.httphandler import HTTPHandler, DefaultHTTPHandler
from transmissionrpc.torrent import Torrent from transmissionrpc.torrent import Torrent
from transmissionrpc.session import Session from transmissionrpc.session import Session
from transmissionrpc.client import Client from transmissionrpc.client import Client
from transmissionrpc.utils import add_stdout_logger from transmissionrpc.utils import add_stdout_logger, add_file_logger
__author__ = u'Erik Svensson <erik.public@gmail.com>' __author__ = 'Erik Svensson <erik.public@gmail.com>'
__version__ = u'0.8' __version_major__ = 0
__copyright__ = u'Copyright (c) 2008-2011 Erik Svensson' __version_minor__ = 11
__license__ = u'MIT' __version__ = '{0}.{1}'.format(__version_major__, __version_minor__)
__copyright__ = 'Copyright (c) 2008-2013 Erik Svensson'
__license__ = 'MIT'

561
resources/lib/transmissionrpc/client.py Normal file → Executable file
View file

@ -1,15 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2008-2011 Erik Svensson <erik.public@gmail.com> # Copyright (c) 2008-2013 Erik Svensson <erik.public@gmail.com>
# Licensed under the MIT license. # Licensed under the MIT license.
import sys import re, time, operator, warnings, os
import re, time import base64
import urllib2, urlparse, base64 import json
try:
import json
except ImportError:
import simplejson as json
from transmissionrpc.constants import DEFAULT_PORT, DEFAULT_TIMEOUT from transmissionrpc.constants import DEFAULT_PORT, DEFAULT_TIMEOUT
from transmissionrpc.error import TransmissionError, HTTPHandlerError from transmissionrpc.error import TransmissionError, HTTPHandlerError
@ -18,6 +13,15 @@ from transmissionrpc.httphandler import DefaultHTTPHandler
from transmissionrpc.torrent import Torrent from transmissionrpc.torrent import Torrent
from transmissionrpc.session import Session from transmissionrpc.session import Session
from six import PY3, integer_types, string_types, iteritems
if PY3:
from urllib.parse import urlparse
from urllib.request import urlopen
else:
from urlparse import urlparse
from urllib2 import urlopen
def debug_httperror(error): def debug_httperror(error):
""" """
Log the Transmission RPC HTTP error. Log the Transmission RPC HTTP error.
@ -41,54 +45,71 @@ def debug_httperror(error):
) )
) )
if sys.version_info[:2] < (2, 5): def parse_torrent_id(arg):
""" """Parse an torrent id or torrent hashString."""
Mimic python 2.5+ urlparse.urlparse torrent_id = None
""" if isinstance(arg, integer_types):
_old_urlparse = urlparse.urlparse # handle index
def _urlparse(address): torrent_id = int(arg)
class ParsedResult(tuple): elif isinstance(arg, float):
def __init__(self, address = None): torrent_id = int(arg)
self.scheme = '' if torrent_id != arg:
self.netloc = '' torrent_id = None
self.path = '' elif isinstance(arg, string_types):
self.params = '' try:
self.query = '' torrent_id = int(arg)
self.fragment = '' if torrent_id >= 2**31:
self.username = None torrent_id = None
self.password = None except (ValueError, TypeError):
self.hostname = None pass
self.port = None if torrent_id is None:
# handle hashes
try:
int(arg, 16)
torrent_id = arg
except (ValueError, TypeError):
pass
return torrent_id
if address: def parse_torrent_ids(args):
self.parse(address) """
def parse(self, address): Take things and make them valid torrent identifiers
( """
self.scheme, ids = []
self.netloc,
self.path, if args is None:
self.params, pass
self.query, elif isinstance(args, string_types):
self.fragment for item in re.split('[ ,]+', args):
) = _old_urlparse(address) if len(item) == 0:
self.hostname = self.netloc continue
if '@' in self.netloc: addition = None
(login, self.hostname) = self.netloc.split('@') torrent_id = parse_torrent_id(item)
if ':' in login: if torrent_id is not None:
(self.username, self.password) = login.split(':') addition = [torrent_id]
else: if not addition:
self.username = login # handle index ranges i.e. 5:10
if ':' in self.hostname: match = re.match('^(\d+):(\d+)$', item)
(self.hostname, self.port) = self.hostname.split(':') if match:
try: try:
self.port = int(self.port) idx_from = int(match.group(1))
except: idx_to = int(match.group(2))
self.port = None addition = list(range(idx_from, idx_to + 1))
except ValueError:
result = ParsedResult(address) pass
return result if not addition:
raise ValueError('Invalid torrent id, \"%s\"' % item)
urlparse.urlparse = _urlparse ids.extend(addition)
elif isinstance(args, (list, tuple)):
for item in args:
ids.extend(parse_torrent_ids(item))
else:
torrent_id = parse_torrent_id(args)
if torrent_id == None:
raise ValueError('Invalid torrent id')
else:
ids = [torrent_id]
return ids
""" """
Torrent ids Torrent ids
@ -110,11 +131,11 @@ class Client(object):
""" """
def __init__(self, address='localhost', port=DEFAULT_PORT, user=None, password=None, http_handler=None, timeout=None): def __init__(self, address='localhost', port=DEFAULT_PORT, user=None, password=None, http_handler=None, timeout=None):
if isinstance(timeout, (int, long, float)): if isinstance(timeout, (integer_types, float)):
self._query_timeout = float(timeout) self._query_timeout = float(timeout)
else: else:
self._query_timeout = DEFAULT_TIMEOUT self._query_timeout = DEFAULT_TIMEOUT
urlo = urlparse.urlparse(address) urlo = urlparse(address)
if urlo.scheme == '': if urlo.scheme == '':
base_url = 'http://' + address + ':' + str(port) base_url = 'http://' + address + ':' + str(port)
self.url = base_url + '/transmission/rpc' self.url = base_url + '/transmission/rpc'
@ -141,7 +162,7 @@ class Client(object):
elif user or password: elif user or password:
LOGGER.warning('Either user or password missing, not using authentication.') LOGGER.warning('Either user or password missing, not using authentication.')
self._sequence = 0 self._sequence = 0
self.session = Session() self.session = None
self.session_id = 0 self.session_id = 0
self.server_version = None self.server_version = None
self.protocol_version = None self.protocol_version = None
@ -183,15 +204,18 @@ class Client(object):
try: try:
result = self.http_handler.request(self.url, query, headers, timeout) result = self.http_handler.request(self.url, query, headers, timeout)
break break
except HTTPHandlerError, error: except HTTPHandlerError as error:
if error.code == 409: if error.code == 409:
LOGGER.info('Server responded with 409, trying to set session-id.') LOGGER.info('Server responded with 409, trying to set session-id.')
if request_count > 1: if request_count > 1:
raise TransmissionError('Session ID negotiation failed.', error) raise TransmissionError('Session ID negotiation failed.', error)
if 'x-transmission-session-id' in error.headers: session_id = None
self.session_id = error.headers['x-transmission-session-id'] for key in list(error.headers.keys()):
headers = {'x-transmission-session-id': str(self.session_id)} if key.lower() == 'x-transmission-session-id':
else: session_id = error.headers[key]
self.session_id = session_id
headers = {'x-transmission-session-id': str(self.session_id)}
if session_id is None:
debug_httperror(error) debug_httperror(error)
raise TransmissionError('Unknown conflict.', error) raise TransmissionError('Unknown conflict.', error)
else: else:
@ -204,13 +228,13 @@ class Client(object):
""" """
Send json-rpc request to Transmission using http POST Send json-rpc request to Transmission using http POST
""" """
if not isinstance(method, (str, unicode)): if not isinstance(method, string_types):
raise ValueError('request takes method as string') raise ValueError('request takes method as string')
if arguments is None: if arguments is None:
arguments = {} arguments = {}
if not isinstance(arguments, dict): if not isinstance(arguments, dict):
raise ValueError('request takes arguments as dict') raise ValueError('request takes arguments as dict')
ids = self._format_ids(ids) ids = parse_torrent_ids(ids)
if len(ids) > 0: if len(ids) > 0:
arguments['ids'] = ids arguments['ids'] = ids
elif require_ids: elif require_ids:
@ -226,7 +250,7 @@ class Client(object):
try: try:
data = json.loads(http_data) data = json.loads(http_data)
except ValueError, error: except ValueError as error:
LOGGER.error('Error: ' + str(error)) LOGGER.error('Error: ' + str(error))
LOGGER.error('Request: \"%s\"' % (query)) LOGGER.error('Request: \"%s\"' % (query))
LOGGER.error('HTTP data: \"%s\"' % (http_data)) LOGGER.error('HTTP data: \"%s\"' % (http_data))
@ -246,8 +270,15 @@ class Client(object):
if self.protocol_version == 2 and 'peers' not in item: if self.protocol_version == 2 and 'peers' not in item:
self.protocol_version = 1 self.protocol_version = 1
elif method == 'torrent-add': elif method == 'torrent-add':
item = data['arguments']['torrent-added'] item = None
results[item['id']] = Torrent(self, item) if 'torrent-added' in data['arguments']:
item = data['arguments']['torrent-added']
elif 'torrent-duplicate' in data['arguments']:
item = data['arguments']['torrent-duplicate']
if item:
results[item['id']] = Torrent(self, item)
else:
raise TransmissionError('Invalid torrent-add response.')
elif method == 'session-get': elif method == 'session-get':
self._update_session(data['arguments']) self._update_session(data['arguments'])
elif method == 'session-stats': elif method == 'session-stats':
@ -256,67 +287,24 @@ class Client(object):
self._update_session(data['arguments']['session-stats']) self._update_session(data['arguments']['session-stats'])
else: else:
self._update_session(data['arguments']) self._update_session(data['arguments'])
elif method in ('port-test', 'blocklist-update'): elif method in ('port-test', 'blocklist-update', 'free-space', 'torrent-rename-path'):
results = data['arguments'] results = data['arguments']
else: else:
return None return None
return results return results
def _format_ids(self, args):
"""
Take things and make them valid torrent identifiers
"""
ids = []
if args is 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): def _update_session(self, data):
""" """
Update session data. Update session data.
""" """
self.session.update(data) if self.session:
self.session.from_request(data)
else:
self.session = Session(self, data)
def _update_server_version(self): def _update_server_version(self):
"""Decode the Transmission version string, if available."""
if self.server_version is None: if self.server_version is None:
version_major = 1 version_major = 1
version_minor = 30 version_minor = 30
@ -355,11 +343,12 @@ class Client(object):
Add a warning to the log if the Transmission RPC version is lower then the provided version. Add a warning to the log if the Transmission RPC version is lower then the provided version.
""" """
if self.rpc_version < 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)) 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): def add_torrent(self, torrent, timeout=None, **kwargs):
""" """
Add torrent to transfers list. Takes a base64 encoded .torrent file in data. Add torrent to transfers list. Takes a uri to a torrent or base64 encoded torrent data in ``torrent``.
Additional arguments are: Additional arguments are:
===================== ===== =========== ============================================================= ===================== ===== =========== =============================================================
@ -377,55 +366,106 @@ class Client(object):
``priority_normal`` 1 - A list of file id's that should have normal priority. ``priority_normal`` 1 - A list of file id's that should have normal priority.
===================== ===== =========== ============================================================= ===================== ===== =========== =============================================================
Returns a Torrent object with the fields.
"""
if torrent is None:
raise ValueError('add_torrent requires data or a URI.')
torrent_data = None
parsed_uri = urlparse(torrent)
if parsed_uri.scheme in ['ftp', 'ftps', 'http', 'https']:
# there has been some problem with T's built in torrent fetcher,
# use a python one instead
torrent_file = urlopen(torrent)
torrent_data = torrent_file.read()
torrent_data = base64.b64encode(torrent_data).decode('utf-8')
if parsed_uri.scheme in ['file']:
filepath = torrent
# uri decoded different on linux / windows ?
if len(parsed_uri.path) > 0:
filepath = parsed_uri.path
elif len(parsed_uri.netloc) > 0:
filepath = parsed_uri.netloc
torrent_file = open(filepath, 'rb')
torrent_data = torrent_file.read()
torrent_data = base64.b64encode(torrent_data).decode('utf-8')
if not torrent_data:
if torrent.endswith('.torrent') or torrent.startswith('magnet:'):
torrent_data = None
else:
might_be_base64 = False
try:
# check if this is base64 data
if PY3:
base64.b64decode(torrent.encode('utf-8'))
else:
base64.b64decode(torrent)
might_be_base64 = True
except Exception:
pass
if might_be_base64:
torrent_data = torrent
args = {}
if torrent_data:
args = {'metainfo': torrent_data}
else:
args = {'filename': torrent}
for key, value in iteritems(kwargs):
argument = make_rpc_name(key)
(arg, val) = argument_value_convert('torrent-add', argument, value, self.rpc_version)
args[arg] = val
return list(self._request('torrent-add', args, timeout=timeout).values())[0]
def add(self, data, timeout=None, **kwargs):
"""
.. WARNING::
Deprecated, please use add_torrent.
""" """
args = {} args = {}
if data: if data:
args = {'metainfo': data} args = {'metainfo': data}
elif 'metainfo' not in kwargs and 'filename' not in kwargs: elif 'metainfo' not in kwargs and 'filename' not in kwargs:
raise ValueError('No torrent data or torrent uri.') raise ValueError('No torrent data or torrent uri.')
for key, value in kwargs.iteritems(): for key, value in iteritems(kwargs):
argument = make_rpc_name(key) argument = make_rpc_name(key)
(arg, val) = argument_value_convert('torrent-add', (arg, val) = argument_value_convert('torrent-add', argument, value, self.rpc_version)
argument, value, self.rpc_version)
args[arg] = val args[arg] = val
warnings.warn('add has been deprecated, please use add_torrent instead.', DeprecationWarning)
return self._request('torrent-add', args, timeout=timeout) return self._request('torrent-add', args, timeout=timeout)
def add_uri(self, uri, **kwargs): 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:
===================== ===== =========== ============================================================= .. WARNING::
Argument RPC Replaced by Description Deprecated, please use add_torrent.
===================== ===== =========== =============================================================
``bandwidthPriority`` 8 - Priority for this transfer.
``cookies`` 13 - One or more HTTP cookie(s).
``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 is None: if uri is None:
raise ValueError('add_uri requires a URI.') raise ValueError('add_uri requires a URI.')
# there has been some problem with T's built in torrent fetcher, # there has been some problem with T's built in torrent fetcher,
# use a python one instead # use a python one instead
parsed_uri = urlparse.urlparse(uri) parsed_uri = urlparse(uri)
torrent_data = None torrent_data = None
if parsed_uri.scheme in ['file', 'ftp', 'ftps', 'http', 'https']: if parsed_uri.scheme in ['ftp', 'ftps', 'http', 'https']:
torrent_file = urllib2.urlopen(uri) torrent_file = urlopen(uri)
torrent_data = base64.b64encode(torrent_file.read()) torrent_data = torrent_file.read()
torrent_data = base64.b64encode(torrent_data).decode('utf-8')
if parsed_uri.scheme in ['file']:
filepath = uri
# uri decoded different on linux / windows ?
if len(parsed_uri.path) > 0:
filepath = parsed_uri.path
elif len(parsed_uri.netloc) > 0:
filepath = parsed_uri.netloc
torrent_file = open(filepath, 'rb')
torrent_data = torrent_file.read()
torrent_data = base64.b64encode(torrent_data).decode('utf-8')
warnings.warn('add_uri has been deprecated, please use add_torrent instead.', DeprecationWarning)
if torrent_data: if torrent_data:
return self.add(torrent_data, **kwargs) return self.add(torrent_data, **kwargs)
else: else:
return self.add(None, filename=uri, **kwargs) return self.add(None, filename=uri, **kwargs)
def remove(self, ids, delete_data=False, timeout=None): def remove_torrent(self, ids, delete_data=False, timeout=None):
""" """
remove torrent(s) with provided id(s). Local data is removed if remove torrent(s) with provided id(s). Local data is removed if
delete_data is True, otherwise not. delete_data is True, otherwise not.
@ -434,32 +474,139 @@ class Client(object):
self._request('torrent-remove', self._request('torrent-remove',
{'delete-local-data':rpc_bool(delete_data)}, ids, True, timeout=timeout) {'delete-local-data':rpc_bool(delete_data)}, ids, True, timeout=timeout)
def start(self, ids, bypass_queue=False, timeout=None): def remove(self, ids, delete_data=False, timeout=None):
"""start torrent(s) with provided id(s)""" """
.. WARNING::
Deprecated, please use remove_torrent.
"""
warnings.warn('remove has been deprecated, please use remove_torrent instead.', DeprecationWarning)
self.remove_torrent(ids, delete_data, timeout)
def start_torrent(self, ids, bypass_queue=False, timeout=None):
"""Start torrent(s) with provided id(s)"""
method = 'torrent-start' method = 'torrent-start'
if bypass_queue and self.rpc_version >= 14: if bypass_queue and self.rpc_version >= 14:
method = 'torrent-start-now' method = 'torrent-start-now'
self._request(method, {}, ids, True, timeout=timeout) self._request(method, {}, ids, True, timeout=timeout)
def stop(self, ids, timeout=None): def start(self, ids, bypass_queue=False, timeout=None):
"""
.. WARNING::
Deprecated, please use start_torrent.
"""
warnings.warn('start has been deprecated, please use start_torrent instead.', DeprecationWarning)
self.start_torrent(ids, bypass_queue, timeout)
def start_all(self, bypass_queue=False, timeout=None):
"""Start all torrents respecting the queue order"""
torrent_list = self.get_torrents()
method = 'torrent-start'
if self.rpc_version >= 14:
if bypass_queue:
method = 'torrent-start-now'
torrent_list = sorted(torrent_list, key=operator.attrgetter('queuePosition'))
ids = [x.id for x in torrent_list]
self._request(method, {}, ids, True, timeout=timeout)
def stop_torrent(self, ids, timeout=None):
"""stop torrent(s) with provided id(s)""" """stop torrent(s) with provided id(s)"""
self._request('torrent-stop', {}, ids, True, timeout=timeout) self._request('torrent-stop', {}, ids, True, timeout=timeout)
def verify(self, ids, timeout=None): def stop(self, ids, timeout=None):
"""
.. WARNING::
Deprecated, please use stop_torrent.
"""
warnings.warn('stop has been deprecated, please use stop_torrent instead.', DeprecationWarning)
self.stop_torrent(ids, timeout)
def verify_torrent(self, ids, timeout=None):
"""verify torrent(s) with provided id(s)""" """verify torrent(s) with provided id(s)"""
self._request('torrent-verify', {}, ids, True, timeout=timeout) self._request('torrent-verify', {}, ids, True, timeout=timeout)
def reannounce(self, ids, timeout=None): def verify(self, ids, timeout=None):
"""
.. WARNING::
Deprecated, please use verify_torrent.
"""
warnings.warn('verify has been deprecated, please use verify_torrent instead.', DeprecationWarning)
self.verify_torrent(ids, timeout)
def reannounce_torrent(self, ids, timeout=None):
"""Reannounce torrent(s) with provided id(s)""" """Reannounce torrent(s) with provided id(s)"""
self._rpc_version_warning(5) self._rpc_version_warning(5)
self._request('torrent-reannounce', {}, ids, True, timeout=timeout) self._request('torrent-reannounce', {}, ids, True, timeout=timeout)
def reannounce(self, ids, timeout=None):
"""
.. WARNING::
Deprecated, please use reannounce_torrent.
"""
warnings.warn('reannounce has been deprecated, please use reannounce_torrent instead.', DeprecationWarning)
self.reannounce_torrent(ids, timeout)
def get_torrent(self, torrent_id, arguments=None, timeout=None):
"""
Get information for torrent with provided id.
``arguments`` contains a list of field names to be returned, when None
all fields are requested. See the Torrent class for more information.
Returns a Torrent object with the requested fields.
"""
if not arguments:
arguments = self.torrent_get_arguments
torrent_id = parse_torrent_id(torrent_id)
if torrent_id is None:
raise ValueError("Invalid id")
result = self._request('torrent-get', {'fields': arguments}, torrent_id, require_ids=True, timeout=timeout)
if torrent_id in result:
return result[torrent_id]
else:
for torrent in result.values():
if torrent.hashString == torrent_id:
return torrent
raise KeyError("Torrent not found in result")
def get_torrents(self, ids=None, arguments=None, timeout=None):
"""
Get information for torrents with provided ids. For more information see get_torrent.
Returns a list of Torrent object.
"""
if not arguments:
arguments = self.torrent_get_arguments
return list(self._request('torrent-get', {'fields': arguments}, ids, timeout=timeout).values())
def info(self, ids=None, arguments=None, timeout=None): def info(self, ids=None, arguments=None, timeout=None):
"""Get detailed information for torrent(s) with provided id(s).""" """
.. WARNING::
Deprecated, please use get_torrent or get_torrents. Please note that the return argument has changed in
the new methods. info returns a dictionary indexed by torrent id.
"""
warnings.warn('info has been deprecated, please use get_torrent or get_torrents instead.', DeprecationWarning)
if not arguments: if not arguments:
arguments = self.torrent_get_arguments arguments = self.torrent_get_arguments
return self._request('torrent-get', {'fields': arguments}, ids, timeout=timeout) return self._request('torrent-get', {'fields': arguments}, ids, timeout=timeout)
def list(self, timeout=None):
"""
.. WARNING::
Deprecated, please use get_torrent or get_torrents. Please note that the return argument has changed in
the new methods. list returns a dictionary indexed by torrent id.
"""
warnings.warn('list has been deprecated, please use get_torrent or get_torrents instead.', DeprecationWarning)
fields = ['id', 'hashString', 'name', 'sizeWhenDone', 'leftUntilDone'
, 'eta', 'status', 'rateUpload', 'rateDownload', 'uploadedEver'
, 'downloadedEver', 'uploadRatio', 'queuePosition']
return self._request('torrent-get', {'fields': fields}, timeout=timeout)
def get_files(self, ids=None, timeout=None): def get_files(self, ids=None, timeout=None):
""" """
Get list of files for provided torrent id(s). If ids is empty, Get list of files for provided torrent id(s). If ids is empty,
@ -487,7 +634,7 @@ class Client(object):
fields = ['id', 'name', 'hashString', 'files', 'priorities', 'wanted'] fields = ['id', 'name', 'hashString', 'files', 'priorities', 'wanted']
request_result = self._request('torrent-get', {'fields': fields}, ids, timeout=timeout) request_result = self._request('torrent-get', {'fields': fields}, ids, timeout=timeout)
result = {} result = {}
for tid, torrent in request_result.iteritems(): for tid, torrent in iteritems(request_result):
result[tid] = torrent.files() result[tid] = torrent.files()
return result return result
@ -513,7 +660,7 @@ class Client(object):
""" """
if not isinstance(items, dict): if not isinstance(items, dict):
raise ValueError('Invalid file description') raise ValueError('Invalid file description')
for tid, files in items.iteritems(): for tid, files in iteritems(items):
if not isinstance(files, dict): if not isinstance(files, dict):
continue continue
wanted = [] wanted = []
@ -521,7 +668,7 @@ class Client(object):
high = [] high = []
normal = [] normal = []
low = [] low = []
for fid, file_desc in files.iteritems(): for fid, file_desc in iteritems(files):
if not isinstance(file_desc, dict): if not isinstance(file_desc, dict):
continue continue
if 'selected' in file_desc and file_desc['selected']: if 'selected' in file_desc and file_desc['selected']:
@ -536,9 +683,7 @@ class Client(object):
elif file_desc['priority'] == 'low': elif file_desc['priority'] == 'low':
low.append(fid) low.append(fid)
args = { args = {
'timeout': timeout, 'timeout': timeout
'files_wanted': wanted,
'files_unwanted': unwanted,
} }
if len(high) > 0: if len(high) > 0:
args['priority_high'] = high args['priority_high'] = high
@ -546,16 +691,13 @@ class Client(object):
args['priority_normal'] = normal args['priority_normal'] = normal
if len(low) > 0: if len(low) > 0:
args['priority_low'] = low args['priority_low'] = low
self.change([tid], **args) if len(wanted) > 0:
args['files_wanted'] = wanted
if len(unwanted) > 0:
args['files_unwanted'] = unwanted
self.change_torrent([tid], **args)
def list(self, timeout=None): def change_torrent(self, ids, timeout=None, **kwargs):
"""list all torrents"""
fields = ['id', 'hashString', 'name', 'sizeWhenDone', 'leftUntilDone'
, 'eta', 'status', 'rateUpload', 'rateDownload', 'uploadedEver'
, 'downloadedEver', 'uploadRatio']
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 Change torrent parameters for the torrent(s) with the supplied id's. The
parameters are: parameters are:
@ -594,10 +736,9 @@ class Client(object):
transmissionrpc will try to automatically fix argument errors. transmissionrpc will try to automatically fix argument errors.
""" """
args = {} args = {}
for key, value in kwargs.iteritems(): for key, value in iteritems(kwargs):
argument = make_rpc_name(key) argument = make_rpc_name(key)
(arg, val) = argument_value_convert('torrent-set' (arg, val) = argument_value_convert('torrent-set' , argument, value, self.rpc_version)
, argument, value, self.rpc_version)
args[arg] = val args[arg] = val
if len(args) > 0: if len(args) > 0:
@ -605,18 +746,61 @@ class Client(object):
else: else:
ValueError("No arguments to set") ValueError("No arguments to set")
def move(self, ids, location, timeout=None): def change(self, ids, timeout=None, **kwargs):
"""
.. WARNING::
Deprecated, please use change_torrent.
"""
warnings.warn('change has been deprecated, please use change_torrent instead.', DeprecationWarning)
self.change_torrent(ids, timeout, **kwargs)
def move_torrent_data(self, ids, location, timeout=None):
"""Move torrent data to the new location.""" """Move torrent data to the new location."""
self._rpc_version_warning(6) self._rpc_version_warning(6)
args = {'location': location, 'move': True} args = {'location': location, 'move': True}
self._request('torrent-set-location', args, ids, True, timeout=timeout) self._request('torrent-set-location', args, ids, True, timeout=timeout)
def locate(self, ids, location, timeout=None): def move(self, ids, location, timeout=None):
"""Locate torrent data at the location.""" """
.. WARNING::
Deprecated, please use move_torrent_data.
"""
warnings.warn('move has been deprecated, please use move_torrent_data instead.', DeprecationWarning)
self.move_torrent_data(ids, location, timeout)
def locate_torrent_data(self, ids, location, timeout=None):
"""Locate torrent data at the provided location."""
self._rpc_version_warning(6) self._rpc_version_warning(6)
args = {'location': location, 'move': False} args = {'location': location, 'move': False}
self._request('torrent-set-location', args, ids, True, timeout=timeout) self._request('torrent-set-location', args, ids, True, timeout=timeout)
def locate(self, ids, location, timeout=None):
"""
.. WARNING::
Deprecated, please use locate_torrent_data.
"""
warnings.warn('locate has been deprecated, please use locate_torrent_data instead.', DeprecationWarning)
self.locate_torrent_data(ids, location, timeout)
def rename_torrent_path(self, torrent_id, location, name, timeout=None):
"""
Rename directory and/or files for torrent.
Remember to use get_torrent or get_torrents to update your file information.
"""
self._rpc_version_warning(15)
torrent_id = parse_torrent_id(torrent_id)
if torrent_id is None:
raise ValueError("Invalid id")
dirname = os.path.dirname(name)
if len(dirname) > 0:
raise ValueError("Target name cannot contain a path delimiter")
args = {'path': location, 'name': name}
result = self._request('torrent-rename-path', args, torrent_id, True, timeout=timeout)
return (result['path'], result['name'])
def queue_top(self, ids, timeout=None): def queue_top(self, ids, timeout=None):
"""Move transfer to the top of the queue.""" """Move transfer to the top of the queue."""
self._rpc_version_warning(14) self._rpc_version_warning(14)
@ -638,7 +822,9 @@ class Client(object):
self._request('queue-move-down', ids=ids, require_ids=True, timeout=timeout) self._request('queue-move-down', ids=ids, require_ids=True, timeout=timeout)
def get_session(self, timeout=None): def get_session(self, timeout=None):
"""Get session parameters""" """
Get session parameters. See the Session class for more information.
"""
self._request('session-get', timeout=timeout) self._request('session-get', timeout=timeout)
self._update_server_version() self._update_server_version()
return self.session return self.session
@ -662,17 +848,17 @@ class Client(object):
``cache_size_mb`` 10 - The maximum size of the disk cache in MB ``cache_size_mb`` 10 - The maximum size of the disk cache in MB
``dht_enabled`` 6 - Enables DHT. ``dht_enabled`` 6 - Enables DHT.
``download_dir`` 1 - Set the session download directory. ``download_dir`` 1 - Set the session download directory.
``download_queue_enabled`` 14 - Enable parallel download restriction. ``download_queue_enabled`` 14 - Enables download queue.
``download_queue_size`` 14 - Number of parallel downloads. ``download_queue_size`` 14 - Number of slots in the download queue.
``encryption`` 1 - Set the session encryption mode, one of ``required``, ``preferred`` or ``tolerated``. ``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`` 10 - The default seed inactivity limit in minutes.
``idle_seeding_limit_enabled`` 10 - Enables the default seed inactivity limit ``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`` 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. ``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. ``lpd_enabled`` 9 - Enables local peer discovery for public torrents.
``peer_limit`` 1 - 5 peer-limit-global Maximum number of peers ``peer_limit`` 1 - 5 peer-limit-global Maximum number of peers.
``peer_limit_global`` 5 - Maximum number of peers ``peer_limit_global`` 5 - Maximum number of peers.
``peer_limit_per_torrent`` 5 - Maximum number of peers per transfer ``peer_limit_per_torrent`` 5 - Maximum number of peers per transfer.
``peer_port`` 5 - Peer port. ``peer_port`` 5 - Peer port.
``peer_port_random_on_start`` 5 - Enables randomized peer port on start of Transmission. ``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_allowed`` 1 - 5 pex-enabled Allowing PEX in public torrents.
@ -684,8 +870,8 @@ class Client(object):
``rename_partial_files`` 8 - Appends ".part" to incomplete files ``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_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. ``script_torrent_done_filename`` 9 - Filename of the script to run when the transfer is done.
``seed_queue_enabled`` 14 - Enable parallel upload restriction. ``seed_queue_enabled`` 14 - Enables upload queue.
``seed_queue_size`` 14 - Number of parallel uploads. ``seed_queue_size`` 14 - Number of slots in the upload queue.
``seedRatioLimit`` 5 - Seed ratio limit. 1.0 means 1:1 download and upload ratio. ``seedRatioLimit`` 5 - Seed ratio limit. 1.0 means 1:1 download and upload ratio.
``seedRatioLimited`` 5 - Enables seed ration limit. ``seedRatioLimited`` 5 - Enables seed ration limit.
``speed_limit_down`` 1 - Download speed limit (in Kib/s). ``speed_limit_down`` 1 - Download speed limit (in Kib/s).
@ -701,12 +887,11 @@ class Client(object):
transmissionrpc will try to automatically fix argument errors. transmissionrpc will try to automatically fix argument errors.
""" """
args = {} args = {}
for key, value in kwargs.iteritems(): for key, value in iteritems(kwargs):
if key == 'encryption' and value not in ['required', 'preferred', 'tolerated']: if key == 'encryption' and value not in ['required', 'preferred', 'tolerated']:
raise ValueError('Invalid encryption value') raise ValueError('Invalid encryption value')
argument = make_rpc_name(key) argument = make_rpc_name(key)
(arg, val) = argument_value_convert('session-set' (arg, val) = argument_value_convert('session-set' , argument, value, self.rpc_version)
, argument, value, self.rpc_version)
args[arg] = val args[arg] = val
if len(args) > 0: if len(args) > 0:
self._request('session-set', args, timeout=timeout) self._request('session-set', args, timeout=timeout)
@ -730,6 +915,16 @@ class Client(object):
return result['port-is-open'] return result['port-is-open']
return None return None
def free_space(self, path, timeout=None):
"""
Get the ammount of free space (in bytes) at the provided location.
"""
self._rpc_version_warning(15)
result = self._request('free-space', {'path': path}, timeout=timeout)
if result['path'] == path:
return result['size-bytes']
return None
def session_stats(self, timeout=None): def session_stats(self, timeout=None):
"""Get session statistics""" """Get session statistics"""
self._request('session-stats', timeout=timeout) self._request('session-stats', timeout=timeout)

288
resources/lib/transmissionrpc/constants.py Normal file → Executable file
View file

@ -1,8 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2008-2011 Erik Svensson <erik.public@gmail.com> # Copyright (c) 2008-2013 Erik Svensson <erik.public@gmail.com>
# Licensed under the MIT license. # Licensed under the MIT license.
import logging import logging
from six import iteritems
LOGGER = logging.getLogger('transmissionrpc') LOGGER = logging.getLogger('transmissionrpc')
LOGGER.setLevel(logging.ERROR) LOGGER.setLevel(logging.ERROR)
@ -11,7 +12,7 @@ def mirror_dict(source):
""" """
Creates a dictionary with all values as keys and all keys as values. Creates a dictionary with all values as keys and all keys as values.
""" """
source.update(dict((value, key) for key, value in source.iteritems())) source.update(dict((value, key) for key, value in iteritems(source)))
return source return source
DEFAULT_PORT = 9091 DEFAULT_PORT = 9091
@ -60,87 +61,90 @@ IDLE_LIMIT = mirror_dict({
# Arguments for torrent methods # Arguments for torrent methods
TORRENT_ARGS = { TORRENT_ARGS = {
'get' : { 'get' : {
'activityDate': ('number', 1, None, None, None, ''), 'activityDate': ('number', 1, None, None, None, 'Last time of upload or download activity.'),
'addedDate': ('number', 1, None, None, None, ''), 'addedDate': ('number', 1, None, None, None, 'The date when this torrent was first added.'),
'announceResponse': ('string', 1, 7, None, None, ''), 'announceResponse': ('string', 1, 7, None, None, 'The announce message from the tracker.'),
'announceURL': ('string', 1, 7, None, None, ''), 'announceURL': ('string', 1, 7, None, None, 'Current announce URL.'),
'bandwidthPriority': ('number', 5, None, None, None, ''), 'bandwidthPriority': ('number', 5, None, None, None, 'Bandwidth priority. Low (-1), Normal (0) or High (1).'),
'comment': ('string', 1, None, None, None, ''), 'comment': ('string', 1, None, None, None, 'Torrent comment.'),
'corruptEver': ('number', 1, None, None, None, ''), 'corruptEver': ('number', 1, None, None, None, 'Number of bytes of corrupt data downloaded.'),
'creator': ('string', 1, None, None, None, ''), 'creator': ('string', 1, None, None, None, 'Torrent creator.'),
'dateCreated': ('number', 1, None, None, None, ''), 'dateCreated': ('number', 1, None, None, None, 'Torrent creation date.'),
'desiredAvailable': ('number', 1, None, None, None, ''), 'desiredAvailable': ('number', 1, None, None, None, 'Number of bytes avalable and left to be downloaded.'),
'doneDate': ('number', 1, None, None, None, ''), 'doneDate': ('number', 1, None, None, None, 'The date when the torrent finished downloading.'),
'downloadDir': ('string', 4, None, None, None, ''), 'downloadDir': ('string', 4, None, None, None, 'The directory path where the torrent is downloaded to.'),
'downloadedEver': ('number', 1, None, None, None, ''), 'downloadedEver': ('number', 1, None, None, None, 'Number of bytes of good data downloaded.'),
'downloaders': ('number', 4, 7, None, None, ''), 'downloaders': ('number', 4, 7, None, None, 'Number of downloaders.'),
'downloadLimit': ('number', 1, None, None, None, ''), 'downloadLimit': ('number', 1, None, None, None, 'Download limit in Kbps.'),
'downloadLimited': ('boolean', 5, None, None, None, ''), 'downloadLimited': ('boolean', 5, None, None, None, 'Download limit is enabled'),
'downloadLimitMode': ('number', 1, 5, None, None, ''), 'downloadLimitMode': ('number', 1, 5, None, None, 'Download limit mode. 0 means global, 1 means signle, 2 unlimited.'),
'error': ('number', 1, None, None, None, ''), 'error': ('number', 1, None, None, None, 'Kind of error. 0 means OK, 1 means tracker warning, 2 means tracker error, 3 means local error.'),
'errorString': ('number', 1, None, None, None, ''), 'errorString': ('number', 1, None, None, None, 'Error message.'),
'eta': ('number', 1, None, None, None, ''), 'eta': ('number', 1, None, None, None, 'Estimated number of seconds left when downloading or seeding. -1 means not available and -2 means unknown.'),
'files': ('array', 1, None, None, None, ''), 'etaIdle': ('number', 15, None, None, None, 'Estimated number of seconds left until the idle time limit is reached. -1 means not available and -2 means unknown.'),
'fileStats': ('array', 5, None, None, None, ''), 'files': ('array', 1, None, None, None, 'Array of file object containing key, bytesCompleted, length and name.'),
'hashString': ('string', 1, None, None, None, ''), 'fileStats': ('array', 5, None, None, None, 'Aray of file statistics containing bytesCompleted, wanted and priority.'),
'haveUnchecked': ('number', 1, None, None, None, ''), 'hashString': ('string', 1, None, None, None, 'Hashstring unique for the torrent even between sessions.'),
'haveValid': ('number', 1, None, None, None, ''), 'haveUnchecked': ('number', 1, None, None, None, 'Number of bytes of partial pieces.'),
'honorsSessionLimits': ('boolean', 5, None, None, None, ''), 'haveValid': ('number', 1, None, None, None, 'Number of bytes of checksum verified data.'),
'id': ('number', 1, None, None, None, ''), 'honorsSessionLimits': ('boolean', 5, None, None, None, 'True if session upload limits are honored'),
'isFinished': ('boolean', 9, None, None, None, ''), 'id': ('number', 1, None, None, None, 'Session unique torrent id.'),
'isPrivate': ('boolean', 1, None, None, None, ''), 'isFinished': ('boolean', 9, None, None, None, 'True if the torrent is finished. Downloaded and seeded.'),
'isStalled': ('boolean', 14, None, None, None, ''), 'isPrivate': ('boolean', 1, None, None, None, 'True if the torrent is private.'),
'lastAnnounceTime': ('number', 1, 7, None, None, ''), 'isStalled': ('boolean', 14, None, None, None, 'True if the torrent has stalled (been idle for a long time).'),
'lastScrapeTime': ('number', 1, 7, None, None, ''), 'lastAnnounceTime': ('number', 1, 7, None, None, 'The time of the last announcement.'),
'leechers': ('number', 1, 7, None, None, ''), 'lastScrapeTime': ('number', 1, 7, None, None, 'The time af the last successful scrape.'),
'leftUntilDone': ('number', 1, None, None, None, ''), 'leechers': ('number', 1, 7, None, None, 'Number of leechers.'),
'magnetLink': ('string', 7, None, None, None, ''), 'leftUntilDone': ('number', 1, None, None, None, 'Number of bytes left until the download is done.'),
'manualAnnounceTime': ('number', 1, None, None, None, ''), 'magnetLink': ('string', 7, None, None, None, 'The magnet link for this torrent.'),
'maxConnectedPeers': ('number', 1, None, None, None, ''), 'manualAnnounceTime': ('number', 1, None, None, None, 'The time until you manually ask for more peers.'),
'metadataPercentComplete': ('number', 7, None, None, None, ''), 'maxConnectedPeers': ('number', 1, None, None, None, 'Maximum of connected peers.'),
'name': ('string', 1, None, None, None, ''), 'metadataPercentComplete': ('number', 7, None, None, None, 'Download progress of metadata. 0.0 to 1.0.'),
'nextAnnounceTime': ('number', 1, 7, None, None, ''), 'name': ('string', 1, None, None, None, 'Torrent name.'),
'nextScrapeTime': ('number', 1, 7, None, None, ''), 'nextAnnounceTime': ('number', 1, 7, None, None, 'Next announce time.'),
'peer-limit': ('number', 5, None, None, None, ''), 'nextScrapeTime': ('number', 1, 7, None, None, 'Next scrape time.'),
'peers': ('array', 2, None, None, None, ''), 'peer-limit': ('number', 5, None, None, None, 'Maximum number of peers.'),
'peersConnected': ('number', 1, None, None, None, ''), 'peers': ('array', 2, None, None, None, 'Array of peer objects.'),
'peersFrom': ('object', 1, None, None, None, ''), 'peersConnected': ('number', 1, None, None, None, 'Number of peers we are connected to.'),
'peersGettingFromUs': ('number', 1, None, None, None, ''), 'peersFrom': ('object', 1, None, None, None, 'Object containing download peers counts for different peer types.'),
'peersKnown': ('number', 1, 13, None, None, ''), 'peersGettingFromUs': ('number', 1, None, None, None, 'Number of peers we are sending data to.'),
'peersSendingToUs': ('number', 1, None, None, None, ''), 'peersKnown': ('number', 1, 13, None, None, 'Number of peers that the tracker knows.'),
'percentDone': ('double', 5, None, None, None, ''), 'peersSendingToUs': ('number', 1, None, None, None, 'Number of peers sending to us'),
'pieces': ('string', 5, None, None, None, ''), 'percentDone': ('double', 5, None, None, None, 'Download progress of selected files. 0.0 to 1.0.'),
'pieceCount': ('number', 1, None, None, None, ''), 'pieces': ('string', 5, None, None, None, 'String with base64 encoded bitfield indicating finished pieces.'),
'pieceSize': ('number', 1, None, None, None, ''), 'pieceCount': ('number', 1, None, None, None, 'Number of pieces.'),
'priorities': ('array', 1, None, None, None, ''), 'pieceSize': ('number', 1, None, None, None, 'Number of bytes in a piece.'),
'queuePosition': ('number', 14, None, None, None, ''), 'priorities': ('array', 1, None, None, None, 'Array of file priorities.'),
'rateDownload': ('number', 1, None, None, None, ''), 'queuePosition': ('number', 14, None, None, None, 'The queue position.'),
'rateUpload': ('number', 1, None, None, None, ''), 'rateDownload': ('number', 1, None, None, None, 'Download rate in bps.'),
'recheckProgress': ('double', 1, None, None, None, ''), 'rateUpload': ('number', 1, None, None, None, 'Upload rate in bps.'),
'scrapeResponse': ('string', 1, 7, None, None, ''), 'recheckProgress': ('double', 1, None, None, None, 'Progress of recheck. 0.0 to 1.0.'),
'scrapeURL': ('string', 1, 7, None, None, ''), 'secondsDownloading': ('number', 15, None, None, None, ''),
'seeders': ('number', 1, 7, None, None, ''), 'secondsSeeding': ('number', 15, None, None, None, ''),
'seedIdleLimit': ('number', 10, None, None, None, ''), 'scrapeResponse': ('string', 1, 7, None, None, 'Scrape response message.'),
'seedIdleMode': ('number', 10, None, None, None, ''), 'scrapeURL': ('string', 1, 7, None, None, 'Current scrape URL'),
'seedRatioLimit': ('double', 5, None, None, None, ''), 'seeders': ('number', 1, 7, None, None, 'Number of seeders reported by the tracker.'),
'seedRatioMode': ('number', 5, None, None, None, ''), 'seedIdleLimit': ('number', 10, None, None, None, 'Idle limit in minutes.'),
'sizeWhenDone': ('number', 1, None, None, None, ''), 'seedIdleMode': ('number', 10, None, None, None, 'Use global (0), torrent (1), or unlimited (2) limit.'),
'startDate': ('number', 1, None, None, None, ''), 'seedRatioLimit': ('double', 5, None, None, None, 'Seed ratio limit.'),
'status': ('number', 1, None, None, None, ''), 'seedRatioMode': ('number', 5, None, None, None, 'Use global (0), torrent (1), or unlimited (2) limit.'),
'swarmSpeed': ('number', 1, 7, None, None, ''), 'sizeWhenDone': ('number', 1, None, None, None, 'Size of the torrent download in bytes.'),
'timesCompleted': ('number', 1, 7, None, None, ''), 'startDate': ('number', 1, None, None, None, 'The date when the torrent was last started.'),
'trackers': ('array', 1, None, None, None, ''), 'status': ('number', 1, None, None, None, 'Current status, see source'),
'trackerStats': ('object', 7, None, None, None, ''), 'swarmSpeed': ('number', 1, 7, None, None, 'Estimated speed in Kbps in the swarm.'),
'totalSize': ('number', 1, None, None, None, ''), 'timesCompleted': ('number', 1, 7, None, None, 'Number of successful downloads reported by the tracker.'),
'torrentFile': ('string', 5, None, None, None, ''), 'trackers': ('array', 1, None, None, None, 'Array of tracker objects.'),
'uploadedEver': ('number', 1, None, None, None, ''), 'trackerStats': ('object', 7, None, None, None, 'Array of object containing tracker statistics.'),
'uploadLimit': ('number', 1, None, None, None, ''), 'totalSize': ('number', 1, None, None, None, 'Total size of the torrent in bytes'),
'uploadLimitMode': ('number', 1, 5, None, None, ''), 'torrentFile': ('string', 5, None, None, None, 'Path to .torrent file.'),
'uploadLimited': ('boolean', 5, None, None, None, ''), 'uploadedEver': ('number', 1, None, None, None, 'Number of bytes uploaded, ever.'),
'uploadRatio': ('double', 1, None, None, None, ''), 'uploadLimit': ('number', 1, None, None, None, 'Upload limit in Kbps'),
'wanted': ('array', 1, None, None, None, ''), 'uploadLimitMode': ('number', 1, 5, None, None, 'Upload limit mode. 0 means global, 1 means signle, 2 unlimited.'),
'webseeds': ('array', 1, None, None, None, ''), 'uploadLimited': ('boolean', 5, None, None, None, 'Upload limit enabled.'),
'webseedsSendingToUs': ('number', 1, None, None, None, ''), 'uploadRatio': ('double', 1, None, None, None, 'Seed ratio.'),
'wanted': ('array', 1, None, None, None, 'Array of booleans indicated wanted files.'),
'webseeds': ('array', 1, None, None, None, 'Array of webseeds objects'),
'webseedsSendingToUs': ('number', 1, None, None, None, 'Number of webseeds seeding to us.'),
}, },
'set': { 'set': {
'bandwidthPriority': ('number', 5, None, None, None, 'Priority for this transfer.'), 'bandwidthPriority': ('number', 5, None, None, None, 'Priority for this transfer.'),
@ -188,58 +192,58 @@ TORRENT_ARGS = {
# Arguments for session methods # Arguments for session methods
SESSION_ARGS = { SESSION_ARGS = {
'get': { 'get': {
"alt-speed-down": ('number', 5, 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, ''), "alt-speed-enabled": ('boolean', 5, None, None, None, 'True if alternate global download speed limiter is ebabled.'),
"alt-speed-time-begin": ('number', 5, None, None, None, ''), "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, ''), "alt-speed-time-enabled": ('boolean', 5, None, None, None, 'True if alternate speeds scheduling is enabled.'),
"alt-speed-time-end": ('number', 5, None, None, None, ''), "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, ''), "alt-speed-time-day": ('number', 5, None, None, None, 'Days alternate speeds scheduling is enabled.'),
"alt-speed-up": ('number', 5, None, None, None, ''), "alt-speed-up": ('number', 5, None, None, None, 'Alternate session upload speed limit (in Kib/s)'),
"blocklist-enabled": ('boolean', 5, None, None, None, ''), "blocklist-enabled": ('boolean', 5, None, None, None, 'True when blocklist is enabled.'),
"blocklist-size": ('number', 5, None, None, None, ''), "blocklist-size": ('number', 5, None, None, None, 'Number of rules in the blocklist'),
"blocklist-url": ('string', 11, None, None, None, ''), "blocklist-url": ('string', 11, None, None, None, 'Location of the block list. Updated with blocklist-update.'),
"cache-size-mb": ('number', 10, None, None, None, ''), "cache-size-mb": ('number', 10, None, None, None, 'The maximum size of the disk cache in MB'),
"config-dir": ('string', 8, None, None, None, ''), "config-dir": ('string', 8, None, None, None, 'location of transmissions configuration directory'),
"dht-enabled": ('boolean', 6, None, None, None, ''), "dht-enabled": ('boolean', 6, None, None, None, 'True if DHT enabled.'),
"download-dir": ('string', 1, None, None, None, ''), "download-dir": ('string', 1, None, None, None, 'The download directory.'),
"download-dir-free-space": ('number', 12, None, None, None, ''), "download-dir-free-space": ('number', 12, None, None, None, 'Free space in the download directory, in bytes'),
"download-queue-size": ('number', 14, None, None, None, ''), "download-queue-size": ('number', 14, None, None, None, 'Number of slots in the download queue.'),
"download-queue-enabled": ('boolean', 14, None, None, None, ''), "download-queue-enabled": ('boolean', 14, None, None, None, 'True if the download queue is enabled.'),
"encryption": ('string', 1, None, None, None, ''), "encryption": ('string', 1, None, None, None, 'Encryption mode, one of ``required``, ``preferred`` or ``tolerated``.'),
"idle-seeding-limit": ('number', 10, None, None, None, ''), "idle-seeding-limit": ('number', 10, None, None, None, 'Seed inactivity limit in minutes.'),
"idle-seeding-limit-enabled": ('boolean', 10, None, None, None, ''), "idle-seeding-limit-enabled": ('boolean', 10, None, None, None, 'True if the seed activity limit is enabled.'),
"incomplete-dir": ('string', 7, None, None, None, ''), "incomplete-dir": ('string', 7, None, None, None, 'The path to the directory for incomplete torrent transfer data.'),
"incomplete-dir-enabled": ('boolean', 7, None, None, None, ''), "incomplete-dir-enabled": ('boolean', 7, None, None, None, 'True if the incomplete dir is enabled.'),
"lpd-enabled": ('boolean', 9, None, None, None, ''), "lpd-enabled": ('boolean', 9, None, None, None, 'True if local peer discovery is enabled.'),
"peer-limit": ('number', 1, 5, None, None, ''), "peer-limit": ('number', 1, 5, None, 'peer-limit-global', 'Maximum number of peers.'),
"peer-limit-global": ('number', 5, None, None, None, ''), "peer-limit-global": ('number', 5, None, 'peer-limit', None, 'Maximum number of peers.'),
"peer-limit-per-torrent": ('number', 5, None, None, None, ''), "peer-limit-per-torrent": ('number', 5, None, None, None, 'Maximum number of peers per transfer.'),
"pex-allowed": ('boolean', 1, 5, None, None, ''), "pex-allowed": ('boolean', 1, 5, None, 'pex-enabled', 'True if PEX is allowed.'),
"pex-enabled": ('boolean', 5, None, None, None, ''), "pex-enabled": ('boolean', 5, None, 'pex-allowed', None, 'True if PEX is enabled.'),
"port": ('number', 1, 5, None, None, ''), "port": ('number', 1, 5, None, 'peer-port', 'Peer port.'),
"peer-port": ('number', 5, None, None, None, ''), "peer-port": ('number', 5, None, 'port', None, 'Peer port.'),
"peer-port-random-on-start": ('boolean', 5, None, None, None, ''), "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, ''), "port-forwarding-enabled": ('boolean', 1, None, None, None, 'True if port forwarding is enabled.'),
"queue-stalled-minutes": ('number', 14, None, None, None, ''), "queue-stalled-minutes": ('number', 14, None, None, None, 'Number of minutes of idle that marks a transfer as stalled.'),
"queue-stalled-enabled": ('boolean', 14, None, None, None, ''), "queue-stalled-enabled": ('boolean', 14, None, None, None, 'True if stalled tracking of transfers is enabled.'),
"rename-partial-files": ('boolean', 8, None, None, None, ''), "rename-partial-files": ('boolean', 8, None, None, None, 'True if ".part" is appended to incomplete files'),
"rpc-version": ('number', 4, None, None, None, ''), "rpc-version": ('number', 4, None, None, None, 'Transmission RPC API Version.'),
"rpc-version-minimum": ('number', 4, None, None, None, ''), "rpc-version-minimum": ('number', 4, None, None, None, 'Minimum accepted RPC API Version.'),
"script-torrent-done-enabled": ('boolean', 9, None, None, None, ''), "script-torrent-done-enabled": ('boolean', 9, None, None, None, 'True if the done script is enabled.'),
"script-torrent-done-filename": ('string', 9, None, None, None, ''), "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, ''), "seedRatioLimit": ('double', 5, None, None, None, 'Seed ratio limit. 1.0 means 1:1 download and upload ratio.'),
"seedRatioLimited": ('boolean', 5, None, None, None, ''), "seedRatioLimited": ('boolean', 5, None, None, None, 'True if seed ration limit is enabled.'),
"seed-queue-size": ('number', 14, None, None, None, ''), "seed-queue-size": ('number', 14, None, None, None, 'Number of slots in the upload queue.'),
"seed-queue-enabled": ('boolean', 14, None, None, None, ''), "seed-queue-enabled": ('boolean', 14, None, None, None, 'True if upload queue is enabled.'),
"speed-limit-down": ('number', 1, None, None, None, ''), "speed-limit-down": ('number', 1, None, None, None, 'Download speed limit (in Kib/s).'),
"speed-limit-down-enabled": ('boolean', 1, None, None, None, ''), "speed-limit-down-enabled": ('boolean', 1, None, None, None, 'True if the download speed is limited.'),
"speed-limit-up": ('number', 1, None, None, None, ''), "speed-limit-up": ('number', 1, None, None, None, 'Upload speed limit (in Kib/s).'),
"speed-limit-up-enabled": ('boolean', 1, None, None, None, ''), "speed-limit-up-enabled": ('boolean', 1, None, None, None, 'True if the upload speed is limited.'),
"start-added-torrents": ('boolean', 9, None, None, None, ''), "start-added-torrents": ('boolean', 9, None, None, None, 'When true uploaded torrents will start right away.'),
"trash-original-torrent-files": ('boolean', 9, None, None, None, ''), "trash-original-torrent-files": ('boolean', 9, None, None, None, 'When true added .torrent files will be deleted.'),
'units': ('object', 10, None, None, None, ''), 'units': ('object', 10, None, None, None, 'An object containing units for size and speed.'),
'utp-enabled': ('boolean', 13, None, None, None, ''), 'utp-enabled': ('boolean', 13, None, None, None, 'True if Micro Transport Protocol (UTP) is enabled.'),
"version": ('string', 3, None, None, None, ''), "version": ('string', 3, None, None, None, 'Transmission version.'),
}, },
'set': { 'set': {
"alt-speed-down": ('number', 5, None, None, None, 'Alternate session download speed limit (in Kib/s).'), "alt-speed-down": ('number', 5, None, None, None, 'Alternate session download speed limit (in Kib/s).'),
@ -254,17 +258,17 @@ SESSION_ARGS = {
"cache-size-mb": ('number', 10, None, None, None, 'The maximum size of the disk cache in MB'), "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.'), "dht-enabled": ('boolean', 6, None, None, None, 'Enables DHT.'),
"download-dir": ('string', 1, None, None, None, 'Set the session download directory.'), "download-dir": ('string', 1, None, None, None, 'Set the session download directory.'),
"download-queue-size": ('number', 14, None, None, None, 'Number of parallel downloads.'), "download-queue-size": ('number', 14, None, None, None, 'Number of slots in the download queue.'),
"download-queue-enabled": ('boolean', 14, None, None, None, 'Enable parallel download restriction.'), "download-queue-enabled": ('boolean', 14, None, None, None, 'Enables download queue.'),
"encryption": ('string', 1, None, None, None, 'Set the session encryption mode, one of ``required``, ``preferred`` or ``tolerated``.'), "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": ('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'), "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": ('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.'), "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.'), "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": ('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-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'), "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-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.'), "pex-enabled": ('boolean', 5, None, 'pex-allowed', None, 'Allowing PEX in public torrents.'),
"port": ('number', 1, 5, None, 'peer-port', 'Peer port.'), "port": ('number', 1, 5, None, 'peer-port', 'Peer port.'),
@ -276,8 +280,8 @@ SESSION_ARGS = {
"queue-stalled-enabled": ('boolean', 14, None, None, None, 'Enable tracking of stalled transfers.'), "queue-stalled-enabled": ('boolean', 14, None, None, None, 'Enable tracking of stalled transfers.'),
"script-torrent-done-enabled": ('boolean', 9, None, None, None, 'Whether or not to call the "done" script.'), "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.'), "script-torrent-done-filename": ('string', 9, None, None, None, 'Filename of the script to run when the transfer is done.'),
"seed-queue-size": ('number', 14, None, None, None, 'Number of parallel uploads.'), "seed-queue-size": ('number', 14, None, None, None, 'Number of slots in the upload queue.'),
"seed-queue-enabled": ('boolean', 14, None, None, None, 'Enable parallel upload restriction.'), "seed-queue-enabled": ('boolean', 14, None, None, None, 'Enables upload queue.'),
"seedRatioLimit": ('double', 5, None, None, None, 'Seed ratio limit. 1.0 means 1:1 download and upload ratio.'), "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.'), "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": ('number', 1, None, None, None, 'Download speed limit (in Kib/s).'),

View file

@ -1,7 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2008-2011 Erik Svensson <erik.public@gmail.com> # Copyright (c) 2008-2013 Erik Svensson <erik.public@gmail.com>
# Licensed under the MIT license. # Licensed under the MIT license.
from six import string_types, integer_types
class TransmissionError(Exception): class TransmissionError(Exception):
""" """
This exception is raised when there has occurred an error related to This exception is raised when there has occurred an error related to
@ -9,7 +11,7 @@ class TransmissionError(Exception):
""" """
def __init__(self, message='', original=None): def __init__(self, message='', original=None):
Exception.__init__(self) Exception.__init__(self)
self._message = message self.message = message
self.original = original self.original = original
def __str__(self): def __str__(self):
@ -28,25 +30,25 @@ class HTTPHandlerError(Exception):
Exception.__init__(self) Exception.__init__(self)
self.url = '' self.url = ''
self.code = 600 self.code = 600
self._message = '' self.message = ''
self.headers = {} self.headers = {}
self.data = '' self.data = ''
if isinstance(httpurl, (str, unicode)): if isinstance(httpurl, string_types):
self.url = httpurl self.url = httpurl
if isinstance(httpcode, (int, long)): if isinstance(httpcode, integer_types):
self.code = httpcode self.code = httpcode
if isinstance(httpmsg, (str, unicode)): if isinstance(httpmsg, string_types):
self._message = httpmsg self.message = httpmsg
if isinstance(httpheaders, dict): if isinstance(httpheaders, dict):
self.headers = httpheaders self.headers = httpheaders
if isinstance(httpdata, (str, unicode)): if isinstance(httpdata, string_types):
self.data = httpdata self.data = httpdata
def __repr__(self): def __repr__(self):
return '<HTTPHandlerError %d, %s>' % (self.code, self._message) return '<HTTPHandlerError %d, %s>' % (self.code, self.message)
def __str__(self): def __str__(self):
return 'HTTPHandlerError %d: %s' % (self.code, self._message) return 'HTTPHandlerError %d: %s' % (self.code, self.message)
def __unicode__(self): def __unicode__(self):
return u'HTTPHandlerError %d: %s' % (self.code, self._message) return 'HTTPHandlerError %d: %s' % (self.code, self.message)

View file

@ -1,11 +1,24 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2011 Erik Svensson <erik.public@gmail.com> # Copyright (c) 2011-2013 Erik Svensson <erik.public@gmail.com>
# Licensed under the MIT license. # Licensed under the MIT license.
import sys, httplib, urllib2 import sys
from transmissionrpc.error import HTTPHandlerError from transmissionrpc.error import HTTPHandlerError
from six import PY3
if PY3:
from urllib.request import Request, build_opener, \
HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, HTTPDigestAuthHandler
from urllib.error import HTTPError, URLError
from http.client import BadStatusLine
else:
from urllib2 import Request, build_opener, \
HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, HTTPDigestAuthHandler
from urllib2 import HTTPError, URLError
from httplib import BadStatusLine
class HTTPHandler(object): class HTTPHandler(object):
""" """
Prototype for HTTP handling. Prototype for HTTP handling.
@ -38,35 +51,32 @@ class DefaultHTTPHandler(HTTPHandler):
""" """
def __init__(self): def __init__(self):
HTTPHandler.__init__(self) HTTPHandler.__init__(self)
self.http_opener = build_opener()
def set_authentication(self, uri, login, password): def set_authentication(self, uri, login, password):
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() password_manager = HTTPPasswordMgrWithDefaultRealm()
password_manager.add_password(realm=None, uri=uri, user=login, passwd=password) password_manager.add_password(realm=None, uri=uri, user=login, passwd=password)
opener = urllib2.build_opener( self.http_opener = build_opener(HTTPBasicAuthHandler(password_manager), HTTPDigestAuthHandler(password_manager))
urllib2.HTTPBasicAuthHandler(password_manager)
, urllib2.HTTPDigestAuthHandler(password_manager)
)
urllib2.install_opener(opener)
def request(self, url, query, headers, timeout): def request(self, url, query, headers, timeout):
request = urllib2.Request(url, query, headers) request = Request(url, query.encode('utf-8'), headers)
try: try:
if (sys.version_info[0] == 2 and sys.version_info[1] > 5) or sys.version_info[0] > 2: if (sys.version_info[0] == 2 and sys.version_info[1] > 5) or sys.version_info[0] > 2:
response = urllib2.urlopen(request, timeout=timeout) response = self.http_opener.open(request, timeout=timeout)
else: else:
response = urllib2.urlopen(request) response = self.http_opener.open(request)
except urllib2.HTTPError, error: except HTTPError as error:
if error.fp is None: if error.fp is None:
raise HTTPHandlerError(error.filename, error.code, error.msg, dict(error.hdrs)) raise HTTPHandlerError(error.filename, error.code, error.msg, dict(error.hdrs))
else: else:
raise HTTPHandlerError(error.filename, error.code, error.msg, dict(error.hdrs), error.read()) raise HTTPHandlerError(error.filename, error.code, error.msg, dict(error.hdrs), error.read())
except urllib2.URLError, error: except URLError as error:
# urllib2.URLError documentation is horrendous! # urllib2.URLError documentation is horrendous!
# Try to get the tuple arguments of URLError # 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: 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]) raise HTTPHandlerError(httpcode=error.reason.args[0], httpmsg=error.reason.args[1])
else: else:
raise HTTPHandlerError(httpmsg='urllib2.URLError: %s' % (error.reason)) raise HTTPHandlerError(httpmsg='urllib2.URLError: %s' % (error.reason))
except httplib.BadStatusLine, error: except BadStatusLine as error:
raise HTTPHandlerError(httpmsg='httplib.BadStatusLine: %s' % (error.line)) raise HTTPHandlerError(httpmsg='httplib.BadStatusLine: %s' % (error.line))
return response.read() return response.read().decode('utf-8')

View file

@ -1,7 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2008-2011 Erik Svensson <erik.public@gmail.com> # Copyright (c) 2008-2013 Erik Svensson <erik.public@gmail.com>
# Licensed under the MIT license. # Licensed under the MIT license.
from transmissionrpc.utils import Field
from six import iteritems, integer_types
class Session(object): class Session(object):
""" """
Session is a class holding the session data for a Transmission daemon. Session is a class holding the session data for a Transmission daemon.
@ -12,33 +16,96 @@ class Session(object):
``download-dir`` -> ``download_dir``. ``download-dir`` -> ``download_dir``.
""" """
def __init__(self, fields=None): def __init__(self, client=None, fields=None):
self.fields = {} self._client = client
self._fields = {}
if fields is not None: if fields is not None:
self.update(fields) self._update_fields(fields)
def update(self, other):
"""Update the session data from a session arguments dictionary"""
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): def __getattr__(self, name):
try: try:
return self.fields[name] return self._fields[name].value
except KeyError: except KeyError:
raise AttributeError('No attribute %s' % name) raise AttributeError('No attribute %s' % name)
def __str__(self): def __str__(self):
text = '' text = ''
for key in sorted(self.fields.keys()): for key in sorted(self._fields.keys()):
text += "% 32s: %s\n" % (key[-32:], self.fields[key]) text += "% 32s: %s\n" % (key[-32:], self._fields[key].value)
return text return text
def _update_fields(self, other):
"""
Update the session data from a Transmission JSON-RPC arguments dictionary
"""
if isinstance(other, dict):
for key, value in iteritems(other):
self._fields[key.replace('-', '_')] = Field(value, False)
elif isinstance(other, Session):
for key in list(other._fields.keys()):
self._fields[key] = Field(other._fields[key].value, False)
else:
raise ValueError('Cannot update with supplied data')
def _dirty_fields(self):
"""Enumerate changed fields"""
outgoing_keys = ['peer_port', 'pex_enabled']
fields = []
for key in outgoing_keys:
if key in self._fields and self._fields[key].dirty:
fields.append(key)
return fields
def _push(self):
"""Push changed fields to the server"""
dirty = self._dirty_fields()
args = {}
for key in dirty:
args[key] = self._fields[key].value
self._fields[key] = self._fields[key]._replace(dirty=False)
if len(args) > 0:
self._client.set_session(**args)
def update(self, timeout=None):
"""Update the session information."""
self._push()
session = self._client.get_session(timeout=timeout)
self._update_fields(session)
session = self._client.session_stats(timeout=timeout)
self._update_fields(session)
def from_request(self, data):
"""Update the session information."""
self._update_fields(data)
def _get_peer_port(self):
"""
Get the peer port.
"""
return self._fields['peer_port'].value
def _set_peer_port(self, port):
"""
Set the peer port.
"""
if isinstance(port, integer_types):
self._fields['peer_port'] = Field(port, True)
self._push()
else:
raise ValueError("Not a valid limit")
peer_port = property(_get_peer_port, _set_peer_port, None, "Peer port. This is a mutator.")
def _get_pex_enabled(self):
"""Is peer exchange enabled?"""
return self._fields['pex_enabled'].value
def _set_pex_enabled(self, enabled):
"""Enable/disable peer exchange."""
if isinstance(enabled, bool):
self._fields['pex_enabled'] = Field(enabled, True)
self._push()
else:
raise TypeError("Not a valid type")
pex_enabled = property(_get_pex_enabled, _set_pex_enabled, None, "Enable peer exchange. This is a mutator.")

405
resources/lib/transmissionrpc/torrent.py Normal file → Executable file
View file

@ -1,15 +1,43 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2008-2011 Erik Svensson <erik.public@gmail.com> # Copyright (c) 2008-2013 Erik Svensson <erik.public@gmail.com>
# Licensed under the MIT license. # Licensed under the MIT license.
import sys, datetime import sys, datetime
from transmissionrpc.constants import PRIORITY from transmissionrpc.constants import PRIORITY, RATIO_LIMIT, IDLE_LIMIT
from transmissionrpc.utils import format_timedelta from transmissionrpc.utils import Field, format_timedelta
from six import integer_types, string_types, text_type, iteritems
def get_status_old(code):
"""Get the torrent status using old status codes"""
mapping = {
(1<<0): 'check pending',
(1<<1): 'checking',
(1<<2): 'downloading',
(1<<3): 'seeding',
(1<<4): 'stopped',
}
return mapping[code]
def get_status_new(code):
"""Get the torrent status using new status codes"""
mapping = {
0: 'stopped',
1: 'check pending',
2: 'checking',
3: 'download pending',
4: 'downloading',
5: 'seed pending',
6: 'seeding',
}
return mapping[code]
class Torrent(object): class Torrent(object):
""" """
Torrent is a class holding the data received from Transmission regarding a bittorrent transfer. Torrent is a class holding the data received from Transmission regarding a bittorrent transfer.
All fetched torrent fields are accessible through this class using attributes. All fetched torrent fields are accessible through this class using attributes.
This class has a few convenience properties using the torrent data. This class has a few convenience properties using the torrent data.
""" """
@ -17,19 +45,22 @@ class Torrent(object):
def __init__(self, client, fields): def __init__(self, client, fields):
if 'id' not in fields: if 'id' not in fields:
raise ValueError('Torrent requires an id') raise ValueError('Torrent requires an id')
self.fields = {} self._fields = {}
self.update(fields) self._update_fields(fields)
self.client = client self._incoming_pending = False
self._outgoing_pending = False
self._client = client
def _getNameString(self, codec=None): def _get_name_string(self, codec=None):
"""Get the name"""
if codec is None: if codec is None:
codec = sys.getdefaultencoding() codec = sys.getdefaultencoding()
name = None name = None
# try to find name # try to find name
if 'name' in self.fields: if 'name' in self._fields:
name = self.fields['name'] name = self._fields['name'].value
# if name is unicode, try to decode # if name is unicode, try to decode
if isinstance(name, unicode): if isinstance(name, text_type):
try: try:
name = name.encode(codec) name = name.encode(codec)
except UnicodeError: except UnicodeError:
@ -37,70 +68,77 @@ class Torrent(object):
return name return name
def __repr__(self): def __repr__(self):
tid = self.fields['id'] tid = self._fields['id'].value
name = self._getNameString() name = self._get_name_string()
if isinstance(name, str): if isinstance(name, str):
return '<Torrent %d \"%s\">' % (tid, name) return '<Torrent %d \"%s\">' % (tid, name)
else: else:
return '<Torrent %d>' % (tid) return '<Torrent %d>' % (tid)
def __str__(self): def __str__(self):
name = self._getNameString() name = self._get_name_string()
if isinstance(name, str): if isinstance(name, str):
return 'Torrent \"%s\"' % (name) return 'Torrent \"%s\"' % (name)
else: else:
return 'Torrent' return 'Torrent'
def __copy__(self): def __copy__(self):
return Torrent(self.client, self.fields) return Torrent(self._client, self._fields)
def __getattr__(self, name):
try:
return self._fields[name].value
except KeyError:
raise AttributeError('No attribute %s' % name)
def _rpc_version(self): def _rpc_version(self):
if self.client: """Get the Transmission RPC API version."""
return self.client.rpc_version if self._client:
return self._client.rpc_version
return 2 return 2
def _status_old(self, code): def _dirty_fields(self):
mapping = { """Enumerate changed fields"""
(1<<0): 'check pending', outgoing_keys = ['bandwidthPriority', 'downloadLimit', 'downloadLimited', 'peer_limit', 'queuePosition'
(1<<1): 'checking', , 'seedIdleLimit', 'seedIdleMode', 'seedRatioLimit', 'seedRatioMode', 'uploadLimit', 'uploadLimited']
(1<<2): 'downloading', fields = []
(1<<3): 'seeding', for key in outgoing_keys:
(1<<4): 'stopped', if key in self._fields and self._fields[key].dirty:
} fields.append(key)
return mapping[code] return fields
def _status_new(self, code): def _push(self):
mapping = { """Push changed fields to the server"""
0: 'stopped', dirty = self._dirty_fields()
1: 'check pending', args = {}
2: 'checking', for key in dirty:
3: 'download pending', args[key] = self._fields[key].value
4: 'downloading', self._fields[key] = self._fields[key]._replace(dirty=False)
5: 'seed pending', if len(args) > 0:
6: 'seeding', self._client.change_torrent(self.id, **args)
}
return mapping[code]
def _status(self): def _update_fields(self, other):
code = self.fields['status']
if self._rpc_version() >= 14:
return self._status_new(code)
else:
return self._status_old(code)
def update(self, other):
""" """
Update the torrent data from a Transmission JSON-RPC arguments dictionary Update the torrent data from a Transmission JSON-RPC arguments dictionary
""" """
fields = None fields = None
if isinstance(other, dict): if isinstance(other, dict):
fields = other for key, value in iteritems(other):
self._fields[key.replace('-', '_')] = Field(value, False)
elif isinstance(other, Torrent): elif isinstance(other, Torrent):
fields = other.fields for key in list(other._fields.keys()):
self._fields[key] = Field(other._fields[key].value, False)
else: else:
raise ValueError('Cannot update with supplied data') raise ValueError('Cannot update with supplied data')
for key, value in fields.iteritems(): self._incoming_pending = False
self.fields[key.replace('-', '_')] = value
def _status(self):
"""Get the torrent status"""
code = self._fields['status'].value
if self._rpc_version() >= 14:
return get_status_new(code)
else:
return get_status_old(code)
def files(self): def files(self):
""" """
@ -122,15 +160,13 @@ class Torrent(object):
} }
""" """
result = {} result = {}
if 'files' in self.fields: if 'files' in self._fields:
indices = xrange(len(self.fields['files'])) files = self._fields['files'].value
files = self.fields['files'] indices = range(len(files))
priorities = self.fields['priorities'] priorities = self._fields['priorities'].value
wanted = self.fields['wanted'] wanted = self._fields['wanted'].value
for item in zip(indices, files, priorities, wanted): for item in zip(indices, files, priorities, wanted):
selected = False selected = True if item[3] else False
if item[3]:
selected = True
priority = PRIORITY[item[2]] priority = PRIORITY[item[2]]
result[item[0]] = { result[item[0]] = {
'selected': selected, 'selected': selected,
@ -140,12 +176,6 @@ class Torrent(object):
'completed': item[1]['bytesCompleted']} 'completed': item[1]['bytesCompleted']}
return result return result
def __getattr__(self, name):
try:
return self.fields[name]
except KeyError:
raise AttributeError('No attribute %s' % name)
@property @property
def status(self): def status(self):
""" """
@ -159,43 +189,45 @@ class Torrent(object):
def progress(self): def progress(self):
"""Get the download progress in percent.""" """Get the download progress in percent."""
try: try:
return 100.0 * (self.fields['sizeWhenDone'] - self.fields['leftUntilDone']) / float(self.fields['sizeWhenDone']) size = self._fields['sizeWhenDone'].value
left = self._fields['leftUntilDone'].value
return 100.0 * (size - left) / float(size)
except ZeroDivisionError: except ZeroDivisionError:
return 0.0 return 0.0
@property @property
def ratio(self): def ratio(self):
"""Get the upload/download ratio.""" """Get the upload/download ratio."""
return float(self.fields['uploadRatio']) return float(self._fields['uploadRatio'].value)
@property @property
def eta(self): def eta(self):
"""Get the "eta" as datetime.timedelta.""" """Get the "eta" as datetime.timedelta."""
eta = self.fields['eta'] eta = self._fields['eta'].value
if eta >= 0: if eta >= 0:
return datetime.timedelta(seconds=eta) return datetime.timedelta(seconds=eta)
else: else:
ValueError('eta not valid') raise ValueError('eta not valid')
@property @property
def date_active(self): def date_active(self):
"""Get the attribute "activityDate" as datetime.datetime.""" """Get the attribute "activityDate" as datetime.datetime."""
return datetime.datetime.fromtimestamp(self.fields['activityDate']) return datetime.datetime.fromtimestamp(self._fields['activityDate'].value)
@property @property
def date_added(self): def date_added(self):
"""Get the attribute "addedDate" as datetime.datetime.""" """Get the attribute "addedDate" as datetime.datetime."""
return datetime.datetime.fromtimestamp(self.fields['addedDate']) return datetime.datetime.fromtimestamp(self._fields['addedDate'].value)
@property @property
def date_started(self): def date_started(self):
"""Get the attribute "startDate" as datetime.datetime.""" """Get the attribute "startDate" as datetime.datetime."""
return datetime.datetime.fromtimestamp(self.fields['startDate']) return datetime.datetime.fromtimestamp(self._fields['startDate'].value)
@property @property
def date_done(self): def date_done(self):
"""Get the attribute "doneDate" as datetime.datetime.""" """Get the attribute "doneDate" as datetime.datetime."""
return datetime.datetime.fromtimestamp(self.fields['doneDate']) return datetime.datetime.fromtimestamp(self._fields['doneDate'].value)
def format_eta(self): def format_eta(self):
""" """
@ -205,7 +237,7 @@ class Torrent(object):
* If eta is -2 the result is 'unknown' * If eta is -2 the result is 'unknown'
* Otherwise eta is formatted as <days> <hours>:<minutes>:<seconds>. * Otherwise eta is formatted as <days> <hours>:<minutes>:<seconds>.
""" """
eta = self.fields['eta'] eta = self._fields['eta'].value
if eta == -1: if eta == -1:
return 'not available' return 'not available'
elif eta == -2: elif eta == -2:
@ -213,10 +245,235 @@ class Torrent(object):
else: else:
return format_timedelta(self.eta) return format_timedelta(self.eta)
@property def _get_download_limit(self):
def priority(self): """
Get the download limit.
Can be a number or None.
"""
if self._fields['downloadLimited'].value:
return self._fields['downloadLimit'].value
else:
return None
def _set_download_limit(self, limit):
"""
Get the download limit.
Can be a number, 'session' or None.
"""
if isinstance(limit, integer_types):
self._fields['downloadLimited'] = Field(True, True)
self._fields['downloadLimit'] = Field(limit, True)
self._push()
elif limit == None:
self._fields['downloadLimited'] = Field(False, True)
self._push()
else:
raise ValueError("Not a valid limit")
download_limit = property(_get_download_limit, _set_download_limit, None, "Download limit in Kbps or None. This is a mutator.")
def _get_peer_limit(self):
"""
Get the peer limit.
"""
return self._fields['peer_limit'].value
def _set_peer_limit(self, limit):
"""
Set the peer limit.
"""
if isinstance(limit, integer_types):
self._fields['peer_limit'] = Field(limit, True)
self._push()
else:
raise ValueError("Not a valid limit")
peer_limit = property(_get_peer_limit, _set_peer_limit, None, "Peer limit. This is a mutator.")
def _get_priority(self):
""" """
Get the priority as string. Get the priority as string.
Can be one of 'low', 'normal', 'high'. Can be one of 'low', 'normal', 'high'.
""" """
return PRIORITY[self.fields['bandwidthPriority']] return PRIORITY[self._fields['bandwidthPriority'].value]
def _set_priority(self, priority):
"""
Set the priority as string.
Can be one of 'low', 'normal', 'high'.
"""
if isinstance(priority, string_types):
self._fields['bandwidthPriority'] = Field(PRIORITY[priority], True)
self._push()
priority = property(_get_priority, _set_priority, None
, "Bandwidth priority as string. Can be one of 'low', 'normal', 'high'. This is a mutator.")
def _get_seed_idle_limit(self):
"""
Get the seed idle limit in minutes.
"""
return self._fields['seedIdleLimit'].value
def _set_seed_idle_limit(self, limit):
"""
Set the seed idle limit in minutes.
"""
if isinstance(limit, integer_types):
self._fields['seedIdleLimit'] = Field(limit, True)
self._push()
else:
raise ValueError("Not a valid limit")
seed_idle_limit = property(_get_seed_idle_limit, _set_seed_idle_limit, None
, "Torrent seed idle limit in minutes. Also see seed_idle_mode. This is a mutator.")
def _get_seed_idle_mode(self):
"""
Get the seed ratio mode as string. Can be one of 'global', 'single' or 'unlimited'.
"""
return IDLE_LIMIT[self._fields['seedIdleMode'].value]
def _set_seed_idle_mode(self, mode):
"""
Set the seed ratio mode as string. Can be one of 'global', 'single' or 'unlimited'.
"""
if isinstance(mode, str):
self._fields['seedIdleMode'] = Field(IDLE_LIMIT[mode], True)
self._push()
else:
raise ValueError("Not a valid limit")
seed_idle_mode = property(_get_seed_idle_mode, _set_seed_idle_mode, None,
"""
Seed idle mode as string. Can be one of 'global', 'single' or 'unlimited'.
* global, use session seed idle limit.
* single, use torrent seed idle limit. See seed_idle_limit.
* unlimited, no seed idle limit.
This is a mutator.
"""
)
def _get_seed_ratio_limit(self):
"""
Get the seed ratio limit as float.
"""
return float(self._fields['seedRatioLimit'].value)
def _set_seed_ratio_limit(self, limit):
"""
Set the seed ratio limit as float.
"""
if isinstance(limit, (integer_types, float)) and limit >= 0.0:
self._fields['seedRatioLimit'] = Field(float(limit), True)
self._push()
else:
raise ValueError("Not a valid limit")
seed_ratio_limit = property(_get_seed_ratio_limit, _set_seed_ratio_limit, None
, "Torrent seed ratio limit as float. Also see seed_ratio_mode. This is a mutator.")
def _get_seed_ratio_mode(self):
"""
Get the seed ratio mode as string. Can be one of 'global', 'single' or 'unlimited'.
"""
return RATIO_LIMIT[self._fields['seedRatioMode'].value]
def _set_seed_ratio_mode(self, mode):
"""
Set the seed ratio mode as string. Can be one of 'global', 'single' or 'unlimited'.
"""
if isinstance(mode, str):
self._fields['seedRatioMode'] = Field(RATIO_LIMIT[mode], True)
self._push()
else:
raise ValueError("Not a valid limit")
seed_ratio_mode = property(_get_seed_ratio_mode, _set_seed_ratio_mode, None,
"""
Seed ratio mode as string. Can be one of 'global', 'single' or 'unlimited'.
* global, use session seed ratio limit.
* single, use torrent seed ratio limit. See seed_ratio_limit.
* unlimited, no seed ratio limit.
This is a mutator.
"""
)
def _get_upload_limit(self):
"""
Get the upload limit.
Can be a number or None.
"""
if self._fields['uploadLimited'].value:
return self._fields['uploadLimit'].value
else:
return None
def _set_upload_limit(self, limit):
"""
Set the upload limit.
Can be a number, 'session' or None.
"""
if isinstance(limit, integer_types):
self._fields['uploadLimited'] = Field(True, True)
self._fields['uploadLimit'] = Field(limit, True)
self._push()
elif limit == None:
self._fields['uploadLimited'] = Field(False, True)
self._push()
else:
raise ValueError("Not a valid limit")
upload_limit = property(_get_upload_limit, _set_upload_limit, None, "Upload limit in Kbps or None. This is a mutator.")
def _get_queue_position(self):
"""Get the queue position for this torrent."""
if self._rpc_version() >= 14:
return self._fields['queuePosition'].value
else:
return 0
def _set_queue_position(self, position):
"""Set the queue position for this torrent."""
if self._rpc_version() >= 14:
if isinstance(position, integer_types):
self._fields['queuePosition'] = Field(position, True)
self._push()
else:
raise ValueError("Not a valid position")
else:
pass
queue_position = property(_get_queue_position, _set_queue_position, None, "Queue position")
def update(self, timeout=None):
"""Update the torrent information."""
self._push()
torrent = self._client.get_torrent(self.id, timeout=timeout)
self._update_fields(torrent)
def start(self, bypass_queue=False, timeout=None):
"""
Start the torrent.
"""
self._incoming_pending = True
self._client.start_torrent(self.id, bypass_queue=bypass_queue, timeout=timeout)
def stop(self, timeout=None):
"""Stop the torrent."""
self._incoming_pending = True
self._client.stop_torrent(self.id, timeout=timeout)
def move_data(self, location, timeout=None):
"""Move torrent data to location."""
self._incoming_pending = True
self._client.move_torrent_data(self.id, location, timeout=timeout)
def locate_data(self, location, timeout=None):
"""Locate torrent data at location."""
self._incoming_pending = True
self._client.locate_torrent_data(self.id, location, timeout=timeout)

View file

@ -1,11 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2008-2011 Erik Svensson <erik.public@gmail.com> # Copyright (c) 2008-2013 Erik Svensson <erik.public@gmail.com>
# Licensed under the MIT license. # Licensed under the MIT license.
import socket, datetime, logging import socket, datetime, logging
from collections import namedtuple
import transmissionrpc.constants as constants import transmissionrpc.constants as constants
from transmissionrpc.constants import LOGGER from transmissionrpc.constants import LOGGER
from six import string_types, iteritems
UNITS = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB'] UNITS = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB']
def format_size(size): def format_size(size):
@ -86,15 +89,12 @@ def rpc_bool(arg):
""" """
Convert between Python boolean and Transmission RPC boolean. Convert between Python boolean and Transmission RPC boolean.
""" """
if isinstance(arg, (str, unicode)): if isinstance(arg, string_types):
try: try:
arg = bool(int(arg)) arg = bool(int(arg))
except ValueError: except ValueError:
arg = arg.lower() in [u'true', u'yes'] arg = arg.lower() in ['true', 'yes']
if bool(arg): return 1 if bool(arg) else 0
return 1
else:
return 0
TR_TYPE_MAP = { TR_TYPE_MAP = {
'number' : int, 'number' : int,
@ -166,7 +166,7 @@ def get_arguments(method, rpc_version):
else: else:
return ValueError('Method "%s" not supported' % (method)) return ValueError('Method "%s" not supported' % (method))
accessible = [] accessible = []
for argument, info in args.iteritems(): for argument, info in iteritems(args):
valid_version = True valid_version = True
if rpc_version < info[1]: if rpc_version < info[1]:
valid_version = False valid_version = False
@ -184,8 +184,24 @@ def add_stdout_logger(level='debug'):
trpc_logger = logging.getLogger('transmissionrpc') trpc_logger = logging.getLogger('transmissionrpc')
loghandler = logging.StreamHandler() loghandler = logging.StreamHandler()
if level in levels.keys(): if level in list(levels.keys()):
loglevel = levels[level] loglevel = levels[level]
trpc_logger.setLevel(loglevel) trpc_logger.setLevel(loglevel)
loghandler.setLevel(loglevel) loghandler.setLevel(loglevel)
trpc_logger.addHandler(loghandler) trpc_logger.addHandler(loghandler)
def add_file_logger(filepath, 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.FileHandler(filepath, encoding='utf-8')
if level in list(levels.keys()):
loglevel = levels[level]
trpc_logger.setLevel(loglevel)
loghandler.setLevel(loglevel)
trpc_logger.addHandler(loghandler)
Field = namedtuple('Field', ['value', 'dirty'])