diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index 911ed470..e90f951a 100644 --- a/couchpotato/core/downloaders/base.py +++ b/couchpotato/core/downloaders/base.py @@ -1,8 +1,8 @@ from couchpotato.core.event import addEvent +from couchpotato.core.helpers.encoding import toSafeString from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env -from couchpotato.core.helpers.encoding import toSafeString import os log = CPLog(__name__) diff --git a/couchpotato/core/downloaders/blackhole/main.py b/couchpotato/core/downloaders/blackhole/main.py index ff96deda..9408de18 100644 --- a/couchpotato/core/downloaders/blackhole/main.py +++ b/couchpotato/core/downloaders/blackhole/main.py @@ -42,6 +42,6 @@ class Blackhole(Downloader): pass except: - log.debug('Failed to download file: %s' % data.get('name')) + log.debug('Failed to download file %s: %s' % (data.get('name'), traceback.format_exc())) return False return False diff --git a/couchpotato/core/downloaders/transmission/__init__.py b/couchpotato/core/downloaders/transmission/__init__.py index b4d986f2..34bd3c8f 100644 --- a/couchpotato/core/downloaders/transmission/__init__.py +++ b/couchpotato/core/downloaders/transmission/__init__.py @@ -29,6 +29,7 @@ config = [{ }, { 'name': 'password', + 'type': 'password', }, { 'name': 'paused', @@ -43,6 +44,7 @@ config = [{ { 'name': 'ratio', 'default': 10, + 'type': 'int', 'advanced': True, 'description': 'Stop transfer when reaching ratio', }, diff --git a/couchpotato/core/downloaders/transmission/main.py b/couchpotato/core/downloaders/transmission/main.py index 42276ddf..7677ad02 100644 --- a/couchpotato/core/downloaders/transmission/main.py +++ b/couchpotato/core/downloaders/transmission/main.py @@ -1,3 +1,4 @@ +from base64 import b64encode from couchpotato.core.downloaders.base import Downloader from couchpotato.core.helpers.encoding import isInt from couchpotato.core.logger import CPLog @@ -17,7 +18,6 @@ class Transmission(Downloader): log.info('Sending "%s" to Transmission.' % data.get('name')) - # Load host from config and split out port. host = self.conf('host').split(':') if not isInt(host[1]): @@ -26,24 +26,21 @@ class Transmission(Downloader): # Set parameters for Transmission params = { - 'paused': self.conf('paused', 0), - 'download_dir': self.conf('directory', None) - } - change_params = { - 'seedRatioLimit': self.conf('ratio'), - 'seedRatioMode': 1 if self.conf('ratio') else 0 + 'paused': self.conf('paused', default = 0), + 'download_dir': self.conf('directory', default = None) } try: tc = transmissionrpc.Client(host[0], port = host[1], user = self.conf('username'), password = self.conf('password')) - tr_id = tc.add_uri(data.get('url'), **params) + filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id')) + torrent = tc.add_torrent(b64encode(filedata), **params) # Change settings of added torrents - for item in tr_id: - try: - tc.change(item, timeout = None, **change_params) - except transmissionrpc.TransmissionError, e: - log.error('Failed to change settings for transfer in transmission: %s' % e) + try: + torrent.seed_ratio_limit = self.conf('ratio') + torrent.seed_ratio_mode = 'single' if self.conf('ratio') else 'global' + except transmissionrpc.TransmissionError, e: + log.error('Failed to change settings for transfer in transmission: %s' % e) return True diff --git a/couchpotato/core/plugins/score/main.py b/couchpotato/core/plugins/score/main.py index 8b3d267f..ee7c9806 100644 --- a/couchpotato/core/plugins/score/main.py +++ b/couchpotato/core/plugins/score/main.py @@ -23,4 +23,12 @@ class Score(Plugin): score += sizeScore(nzb['size']) + # Torrents only + if nzb.get('seeds'): + try: + score += nzb.get('seeds') / 5 + score += nzb.get('leechers') / 10 + except: + pass + return score diff --git a/couchpotato/core/plugins/searcher/__init__.py b/couchpotato/core/plugins/searcher/__init__.py index 19a1232d..14a43768 100644 --- a/couchpotato/core/plugins/searcher/__init__.py +++ b/couchpotato/core/plugins/searcher/__init__.py @@ -28,7 +28,7 @@ config = [{ { 'name': 'ignored_words', 'label': 'Ignored words', - 'default': 'german, dutch, french, danish, swedish, dubbed, swesub, korsub', + 'default': 'german, dutch, french, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub', }, ], }, { diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py index 34a9a7c0..4b4ee339 100644 --- a/couchpotato/core/plugins/searcher/main.py +++ b/couchpotato/core/plugins/searcher/main.py @@ -161,7 +161,7 @@ class Searcher(Plugin): single_category = kwargs.get('single_category', False) retention = Env.setting('retention', section = 'nzb') - if retention < nzb.get('age', 0): + if nzb.get('seeds') is None and retention < nzb.get('age', 0): log.info('Wrong: Outside retention, age is %s, needs %s or lower: %s' % (nzb['age'], retention, nzb['name'])) return False diff --git a/couchpotato/core/providers/base.py b/couchpotato/core/providers/base.py index 1a2e9425..148b2e7c 100644 --- a/couchpotato/core/providers/base.py +++ b/couchpotato/core/providers/base.py @@ -48,6 +48,20 @@ class YarrProvider(Provider): def __init__(self): addEvent('provider.belongs_to', self.belongsTo) + addEvent('%s.search' % self.type, self.search) + addEvent('yarr.search', self.search) + + addEvent('nzb.feed', self.feed) + + def download(self, url = '', nzb_id = ''): + return self.urlopen(url) + + def feed(self): + return [] + + def search(self, movie, quality): + return [] + def belongsTo(self, url, host = None): try: hostname = urlparse(url).hostname diff --git a/couchpotato/core/providers/metadata/base.py b/couchpotato/core/providers/metadata/base.py index c0540262..5d99ea9b 100644 --- a/couchpotato/core/providers/metadata/base.py +++ b/couchpotato/core/providers/metadata/base.py @@ -3,7 +3,7 @@ from couchpotato.core.helpers.variable import mergeDicts from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin import json -import os.path +import os import shutil import traceback diff --git a/couchpotato/core/providers/nzb/base.py b/couchpotato/core/providers/nzb/base.py index f8c3834e..06dd2c89 100644 --- a/couchpotato/core/providers/nzb/base.py +++ b/couchpotato/core/providers/nzb/base.py @@ -6,22 +6,5 @@ import time class NZBProvider(YarrProvider): type = 'nzb' - def __init__(self): - super(NZBProvider, self).__init__() - - addEvent('nzb.search', self.search) - addEvent('yarr.search', self.search) - - addEvent('nzb.feed', self.feed) - - def download(self, url = '', nzb_id = ''): - return self.urlopen(url) - - def feed(self): - return [] - - def search(self, movie, quality): - return [] - def calculateAge(self, unix): return int(time.time() - unix) / 24 / 60 / 60 diff --git a/couchpotato/core/providers/nzb/mysterbin/main.py b/couchpotato/core/providers/nzb/mysterbin/main.py index af6c1eee..d92595f6 100644 --- a/couchpotato/core/providers/nzb/mysterbin/main.py +++ b/couchpotato/core/providers/nzb/mysterbin/main.py @@ -1,4 +1,4 @@ -from BeautifulSoup import BeautifulSoup, SoupStrainer +from BeautifulSoup import BeautifulSoup from couchpotato.core.event import fireEvent from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import tryInt diff --git a/couchpotato/core/providers/torrent/kickasstorrents/__init__.py b/couchpotato/core/providers/torrent/kickasstorrents/__init__.py index 7daae95f..e88ac81c 100644 --- a/couchpotato/core/providers/torrent/kickasstorrents/__init__.py +++ b/couchpotato/core/providers/torrent/kickasstorrents/__init__.py @@ -3,4 +3,19 @@ from .main import KickAssTorrents def start(): return KickAssTorrents() -config = [] +config = [{ + 'name': 'kickasstorrents', + 'groups': [ + { + 'tab': 'providers', + 'name': 'KickAssTorrents', + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': False, + }, + ], + }, + ], +}] diff --git a/couchpotato/core/providers/torrent/kickasstorrents/main.py b/couchpotato/core/providers/torrent/kickasstorrents/main.py index 0c602fee..aff0508f 100644 --- a/couchpotato/core/providers/torrent/kickasstorrents/main.py +++ b/couchpotato/core/providers/torrent/kickasstorrents/main.py @@ -1,5 +1,12 @@ +from BeautifulSoup import BeautifulSoup +from couchpotato.core.event import fireEvent +from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog from couchpotato.core.providers.torrent.base import TorrentProvider +import StringIO +import gzip +import re +import traceback log = CPLog(__name__) @@ -7,8 +14,124 @@ log = CPLog(__name__) class KickAssTorrents(TorrentProvider): urls = { - 'download': 'http://torrents.thepiratebay.org/%s/%s.torrent', + 'test': 'http://www.kat.ph/', 'detail': 'http://www.kat.ph/%s-t%s.html', 'search': 'http://www.kat.ph/%s-i%s/', } + cat_ids = [ + (['cam'], ['cam']), + (['telesync'], ['ts', 'tc']), + (['screener', 'tvrip'], ['screener']), + (['x264', '720p', '1080p', 'blu-ray', 'hdrip'], ['bd50', '1080p', '720p', 'brrip']), + (['dvdrip'], ['dvdrip']), + (['dvd'], ['dvdr']), + ] + + http_time_between_calls = 1 #seconds + + def search(self, movie, quality): + + results = [] + if self.isDisabled() or not self.isAvailable(self.urls['test']): + return results + + cache_key = 'kickasstorrents.%s' % movie['library']['identifier'] + data = self.getCache(cache_key, self.urls['search'] % (movie['library']['titles'][0]['title'], movie['library']['identifier'].replace('tt', ''))) + if data: + + cat_ids = self.getCatId(quality['identifier']) + table_order = ['name', 'size', None, 'age', 'seeds', 'leechers'] + + try: + html = BeautifulSoup(data) + resultdiv = html.find('div', attrs = {'class':'tabs'}) + for result in resultdiv.findAll('div', recursive = False): + if result.get('id').lower() not in cat_ids: + continue + + try: + + try: + for temp in result.findAll('tr'): + if temp['class'] is 'firstr' or not temp.get('id'): + continue + + new = { + 'type': 'torrent', + 'check_nzb': False, + 'description': '', + 'provider': self.getName(), + 'download': self.download, + 'score': 0, + } + + nr = 0 + for td in temp.findAll('td'): + column_name = table_order[nr] + if column_name: + + if column_name is 'name': + link = td.find('div', {'class': 'torrentname'}).findAll('a')[1] + new['id'] = temp.get('id')[-8:] + new['name'] = link.text + new['url'] = td.findAll('a', 'idownload')[1]['href'] + new['score'] = 20 if td.find('a', 'iverif') else 0 + elif column_name is 'size': + new['size'] = self.parseSize(td.text) + elif column_name is 'age': + new['age'] = self.ageToDays(td.text) + elif column_name is 'seeds': + new['seeds'] = tryInt(td.text) + elif column_name is 'leechers': + new['leechers'] = tryInt(td.text) + + nr += 1 + + new['score'] += fireEvent('score.calculate', new, movie, single = True) + is_correct_movie = fireEvent('searcher.correct_movie', + nzb = new, movie = movie, quality = quality, + imdb_results = True, single_category = False, single = True) + if is_correct_movie: + results.append(new) + self.found(new) + except: + log.error('Failed parsing KickAssTorrents: %s' % traceback.format_exc()) + except: + pass + + return results + except AttributeError: + log.debug('No search results found.') + + return results + + def ageToDays(self, age_str): + age = 0 + age_str = age_str.replace(' ', ' ') + + regex = '(\d*.?\d+).(sec|hour|day|week|month|year)+' + matches = re.findall(regex, age_str) + for match in matches: + nr, size = match + mult = 1 + if size == 'week': + mult = 7 + elif size == 'month': + mult = 30.5 + elif size == 'year': + mult = 365 + + age += tryInt(nr) * mult + + return tryInt(age) + + def download(self, url = '', nzb_id = ''): + compressed_data = super(KickAssTorrents, self).download(url = url, nzb_id = nzb_id) + + compressedstream = StringIO.StringIO(compressed_data) + gzipper = gzip.GzipFile(fileobj = compressedstream) + data = gzipper.read() + + return data + diff --git a/libs/transmissionrpc/__init__.py b/libs/transmissionrpc/__init__.py index 5d3301fa..057d6784 100755 --- a/libs/transmissionrpc/__init__.py +++ b/libs/transmissionrpc/__init__.py @@ -11,6 +11,6 @@ from transmissionrpc.client import Client from transmissionrpc.utils import add_stdout_logger __author__ = u'Erik Svensson ' -__version__ = u'0.8' +__version__ = u'0.9' __copyright__ = u'Copyright (c) 2008-2011 Erik Svensson' __license__ = u'MIT' diff --git a/libs/transmissionrpc/client.py b/libs/transmissionrpc/client.py index b8f2ebba..420083b6 100755 --- a/libs/transmissionrpc/client.py +++ b/libs/transmissionrpc/client.py @@ -2,7 +2,7 @@ # Copyright (c) 2008-2011 Erik Svensson # Licensed under the MIT license. -import re, time +import re, time, operator, warnings import urllib2, urlparse, base64 try: @@ -36,7 +36,7 @@ def debug_httperror(error): 'data': data, } }, - indent=2 + indent = 2 ) ) @@ -59,7 +59,7 @@ class Client(object): Client is the class handling the Transmission JSON-RPC client protocol. """ - def __init__(self, address='localhost', port=DEFAULT_PORT, user=None, password=None, http_handler=None, timeout=None): + def __init__(self, address = 'localhost', port = DEFAULT_PORT, user = None, password = None, http_handler = None, timeout = None): if isinstance(timeout, (int, long, float)): self._query_timeout = float(timeout) else: @@ -91,7 +91,7 @@ class Client(object): elif user or password: LOGGER.warning('Either user or password missing, not using authentication.') self._sequence = 0 - self.session = Session() + self.session = None self.session_id = 0 self.server_version = None self.protocol_version = None @@ -117,9 +117,9 @@ class Client(object): """ self._query_timeout = DEFAULT_TIMEOUT - timeout = property(_get_timeout, _set_timeout, _del_timeout, doc="HTTP query timeout.") + timeout = property(_get_timeout, _set_timeout, _del_timeout, doc = "HTTP query timeout.") - def _http_query(self, query, timeout=None): + def _http_query(self, query, timeout = None): """ Query Transmission through HTTP. """ @@ -129,7 +129,7 @@ class Client(object): if timeout is None: timeout = self._query_timeout while True: - LOGGER.debug(json.dumps({'url': self.url, 'headers': headers, 'query': query, 'timeout': timeout}, indent=2)) + LOGGER.debug(json.dumps({'url': self.url, 'headers': headers, 'query': query, 'timeout': timeout}, indent = 2)) try: result = self.http_handler.request(self.url, query, headers, timeout) break @@ -150,7 +150,7 @@ class Client(object): request_count += 1 return result - def _request(self, method, arguments=None, ids=None, require_ids=False, timeout=None): + def _request(self, method, arguments = None, ids = None, require_ids = False, timeout = None): """ Send json-rpc request to Transmission using http POST """ @@ -182,7 +182,7 @@ class Client(object): LOGGER.error('HTTP data: \"%s\"' % (http_data)) raise - LOGGER.debug(json.dumps(data, indent=2)) + LOGGER.debug(json.dumps(data, indent = 2)) if 'result' in data: if data['result'] != 'success': raise TransmissionError('Query failed with result \"%s\".' % (data['result'])) @@ -264,7 +264,10 @@ class Client(object): """ 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): if self.server_version is None: @@ -305,11 +308,12 @@ class Client(object): Add a warning to the log if the Transmission RPC version is lower then the provided version. """ if self.rpc_version < version: - LOGGER.warning('Using feature not supported by server. RPC version for server %d, feature introduced in %d.' % (self.rpc_version, version)) + 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. Additional arguments are: ===================== ===== =========== ============================================================= @@ -327,6 +331,41 @@ class Client(object): ``priority_normal`` 1 - A list of file id's that should have normal priority. ===================== ===== =========== ============================================================= + Returns a Torrent object with limited fields. + """ + if torrent is None: + raise ValueError('add_torrent requires data or a URI.') + torrent_data = None + try: + # check if this is base64 data + base64.b64decode(torrent) + torrent_data = torrent + except Exception: + torrent_data = None + if not torrent_data: + parsed_uri = urlparse.urlparse(torrent) + if parsed_uri.scheme in ['file', 'ftp', 'ftps', 'http', 'https']: + # there has been some problem with T's built in torrent fetcher, + # use a python one instead + torrent_file = urllib2.urlopen(torrent) + torrent_data = torrent_file.read() + torrent_data = base64.b64encode(torrent_data) + args = {} + if torrent_data: + args = {'metainfo': torrent_data} + else: + args = {'filename': torrent} + for key, value in kwargs.iteritems(): + argument = make_rpc_name(key) + (arg, val) = argument_value_convert('torrent-add', argument, value, self.rpc_version) + args[arg] = val + return self._request('torrent-add', args, timeout = timeout).values()[0] + + def add(self, data, timeout = None, **kwargs): + """ + + .. WARNING:: + Deprecated, please use add_torrent. """ args = {} if data: @@ -335,31 +374,16 @@ class Client(object): raise ValueError('No torrent data or torrent uri.') for key, value in kwargs.iteritems(): argument = make_rpc_name(key) - (arg, val) = argument_value_convert('torrent-add', - argument, value, self.rpc_version) + (arg, val) = argument_value_convert('torrent-add', argument, value, self.rpc_version) args[arg] = val - return self._request('torrent-add', args, timeout=timeout) + warnings.warn('add has been deprecated, please use add_torrent instead.', DeprecationWarning) + return self._request('torrent-add', args, timeout = timeout) def add_uri(self, uri, **kwargs): """ - Add torrent to transfers list. Takes a uri to a torrent, supporting - all uri's supported by Transmissions torrent-add 'filename' - argument. Additional arguments are: - ===================== ===== =========== ============================================================= - Argument RPC Replaced by Description - ===================== ===== =========== ============================================================= - ``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. - ===================== ===== =========== ============================================================= + .. WARNING:: + Deprecated, please use add_torrent. """ if uri is None: raise ValueError('add_uri requires a URI.') @@ -370,47 +394,145 @@ class Client(object): if parsed_uri.scheme in ['file', 'ftp', 'ftps', 'http', 'https']: torrent_file = urllib2.urlopen(uri) torrent_data = base64.b64encode(torrent_file.read()) + warnings.warn('add_uri has been deprecated, please use add_torrent instead.', DeprecationWarning) if torrent_data: return self.add(torrent_data, **kwargs) 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 delete_data is True, otherwise not. """ self._rpc_version_warning(3) self._request('torrent-remove', - {'delete-local-data':rpc_bool(delete_data)}, ids, True, timeout=timeout) + {'delete-local-data':rpc_bool(delete_data)}, ids, True, timeout = timeout) - def start(self, ids, bypass_queue=False, timeout=None): - """start torrent(s) with provided id(s)""" + def remove(self, ids, delete_data = False, timeout = None): + """ + + .. 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' if bypass_queue and self.rpc_version >= 14: 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)""" - 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)""" - 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)""" self._rpc_version_warning(5) - self._request('torrent-reannounce', {}, ids, True, timeout=timeout) + self._request('torrent-reannounce', {}, ids, True, timeout = timeout) - def info(self, ids=None, arguments=None, timeout=None): - """Get detailed information for torrent(s) with provided id(s).""" + 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, id, arguments = None, timeout = None): + """ + Get information for torrent with provided id. + + Returns a Torrent object. + """ if not arguments: arguments = self.torrent_get_arguments - return self._request('torrent-get', {'fields': arguments}, ids, timeout=timeout) + if not isinstance(id, (int, long, str, unicode)): + raise ValueError("Invalid id") + return self._request('torrent-get', {'fields': arguments}, id, require_ids = True, timeout = timeout)[id] - def get_files(self, ids=None, timeout=None): + def get_torrents(self, ids = None, arguments = None, timeout = None): + """ + Get information for torrents with provided ids. + + Returns a list of Torrent object. + """ + if not arguments: + arguments = self.torrent_get_arguments + return self._request('torrent-get', {'fields': arguments}, ids, timeout = timeout).values() + + def info(self, ids = None, arguments = None, timeout = None): + """ + + .. 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: + arguments = self.torrent_get_arguments + 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): """ Get list of files for provided torrent id(s). If ids is empty, information for all torrents are fetched. This function returns a dictionary @@ -435,13 +557,13 @@ class Client(object): } """ 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 = {} for tid, torrent in request_result.iteritems(): result[tid] = torrent.files() return result - def set_files(self, items, timeout=None): + def set_files(self, items, timeout = None): """ Set file properties. Takes a dictionary with similar contents as the result of `get_files`. @@ -498,14 +620,7 @@ class Client(object): args['priority_low'] = low self.change([tid], **args) - def list(self, timeout=None): - """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): + def change_torrent(self, ids, timeout = None, **kwargs): """ Change torrent parameters for the torrent(s) with the supplied id's. The parameters are: @@ -546,54 +661,80 @@ class Client(object): args = {} for key, value in kwargs.iteritems(): argument = make_rpc_name(key) - (arg, val) = argument_value_convert('torrent-set' - , argument, value, self.rpc_version) + (arg, val) = argument_value_convert('torrent-set' , argument, value, self.rpc_version) args[arg] = val if len(args) > 0: - self._request('torrent-set', args, ids, True, timeout=timeout) + self._request('torrent-set', args, ids, True, timeout = timeout) else: 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.""" self._rpc_version_warning(6) 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): - """Locate torrent data at the location.""" + def move(self, ids, location, timeout = None): + """ + + .. 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) 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 queue_top(self, ids, timeout=None): + 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 queue_top(self, ids, timeout = None): """Move transfer to the top of the queue.""" self._rpc_version_warning(14) - self._request('queue-move-top', ids=ids, require_ids=True, timeout=timeout) + self._request('queue-move-top', ids = ids, require_ids = True, timeout = timeout) - def queue_bottom(self, ids, timeout=None): + def queue_bottom(self, ids, timeout = None): """Move transfer to the bottom of the queue.""" self._rpc_version_warning(14) - self._request('queue-move-bottom', ids=ids, require_ids=True, timeout=timeout) - - def queue_up(self, ids, timeout=None): + self._request('queue-move-bottom', ids = ids, require_ids = True, timeout = timeout) + + def queue_up(self, ids, timeout = None): """Move transfer up in the queue.""" self._rpc_version_warning(14) - self._request('queue-move-up', ids=ids, require_ids=True, timeout=timeout) + self._request('queue-move-up', ids = ids, require_ids = True, timeout = timeout) - def queue_down(self, ids, timeout=None): + def queue_down(self, ids, timeout = None): """Move transfer down in the queue.""" self._rpc_version_warning(14) - 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""" - self._request('session-get', timeout=timeout) + self._request('session-get', timeout = timeout) self._update_server_version() return self.session - def set_session(self, timeout=None, **kwargs): + def set_session(self, timeout = None, **kwargs): """ Set session parameters. The parameters are: @@ -655,32 +796,31 @@ class Client(object): if key == 'encryption' and value not in ['required', 'preferred', 'tolerated']: raise ValueError('Invalid encryption value') argument = make_rpc_name(key) - (arg, val) = argument_value_convert('session-set' - , argument, value, self.rpc_version) + (arg, val) = argument_value_convert('session-set' , argument, value, self.rpc_version) args[arg] = val if len(args) > 0: - self._request('session-set', args, timeout=timeout) + self._request('session-set', args, timeout = timeout) - def blocklist_update(self, timeout=None): + def blocklist_update(self, timeout = None): """Update block list. Returns the size of the block list.""" self._rpc_version_warning(5) - result = self._request('blocklist-update', timeout=timeout) + result = self._request('blocklist-update', timeout = timeout) if 'blocklist-size' in result: return result['blocklist-size'] return None - def port_test(self, timeout=None): + def port_test(self, timeout = None): """ Tests to see if your incoming peer port is accessible from the outside world. """ self._rpc_version_warning(5) - result = self._request('port-test', timeout=timeout) + result = self._request('port-test', timeout = timeout) if 'port-is-open' in result: return result['port-is-open'] return None - def session_stats(self, timeout=None): + def session_stats(self, timeout = None): """Get session statistics""" - self._request('session-stats', timeout=timeout) + self._request('session-stats', timeout = timeout) return self.session diff --git a/libs/transmissionrpc/error.py b/libs/transmissionrpc/error.py index 9b50e00c..5e2c1e28 100755 --- a/libs/transmissionrpc/error.py +++ b/libs/transmissionrpc/error.py @@ -15,9 +15,9 @@ class TransmissionError(Exception): def __str__(self): if self.original: original_name = type(self.original).__name__ - return '%s Original exception: %s, "%s"' % (self.message, original_name, str(self.original)) + return '%s Original exception: %s, "%s"' % (self._message, original_name, str(self.original)) else: - return self.message + return self._message class HTTPHandlerError(Exception): """ diff --git a/libs/transmissionrpc/session.py b/libs/transmissionrpc/session.py index 3f71a2ae..532553cf 100755 --- a/libs/transmissionrpc/session.py +++ b/libs/transmissionrpc/session.py @@ -2,6 +2,8 @@ # Copyright (c) 2008-2011 Erik Svensson # Licensed under the MIT license. +from transmissionrpc.utils import Field + class Session(object): """ Session is a class holding the session data for a Transmission daemon. @@ -12,33 +14,95 @@ class Session(object): ``download-dir`` -> ``download_dir``. """ - def __init__(self, fields=None): - self.fields = {} + def __init__(self, client=None, fields=None): + self._client = client + self._fields = {} if fields is not None: - self.update(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 + self._update_fields(fields) def __getattr__(self, name): try: - return self.fields[name] + return self._fields[name].value except KeyError: raise AttributeError('No attribute %s' % name) def __str__(self): text = '' - for key in sorted(self.fields.keys()): - text += "% 32s: %s\n" % (key[-32:], self.fields[key]) + for key in sorted(self._fields.keys()): + text += "% 32s: %s\n" % (key[-32:], self._fields[key].value) return text + + def _update_fields(self, other): + """ + Update the session data from a Transmission JSON-RPC arguments dictionary + """ + fields = None + if isinstance(other, dict): + for key, value in other.iteritems(): + self._fields[key.replace('-', '_')] = Field(value, False) + elif isinstance(other, Session): + for key in 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, (int, long)): + 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): + return self._fields['pex_enabled'].value + + def _set_pex_enabled(self, enabled): + 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 PEX. This is a mutator.") diff --git a/libs/transmissionrpc/torrent.py b/libs/transmissionrpc/torrent.py index 357d0a19..05008784 100755 --- a/libs/transmissionrpc/torrent.py +++ b/libs/transmissionrpc/torrent.py @@ -4,8 +4,8 @@ import sys, datetime -from transmissionrpc.constants import PRIORITY -from transmissionrpc.utils import format_timedelta +from transmissionrpc.constants import PRIORITY, RATIO_LIMIT, IDLE_LIMIT +from transmissionrpc.utils import Field, format_timedelta class Torrent(object): """ @@ -17,17 +17,19 @@ class Torrent(object): def __init__(self, client, fields): if 'id' not in fields: raise ValueError('Torrent requires an id') - self.fields = {} - self.update(fields) - self.client = client + self._fields = {} + self._update_fields(fields) + self._incoming_pending= False + self._outgoing_pending= False + self._client = client def _getNameString(self, codec=None): if codec is None: codec = sys.getdefaultencoding() name = None # try to find name - if 'name' in self.fields: - name = self.fields['name'] + if 'name' in self._fields: + name = self._fields['name'].value # if name is unicode, try to decode if isinstance(name, unicode): try: @@ -37,7 +39,7 @@ class Torrent(object): return name def __repr__(self): - tid = self.fields['id'] + tid = self._fields['id'].value name = self._getNameString() if isinstance(name, str): return '' % (tid, name) @@ -52,12 +54,53 @@ class Torrent(object): return 'Torrent' 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): - if self.client: - return self.client.rpc_version + if self._client: + return self._client.rpc_version return 2 + + def _dirty_fields(self): + """Enumerate changed fields""" + outgoing_keys = ['bandwidthPriority', 'downloadLimit', 'downloadLimited', 'peer_limit', 'queuePosition' + , 'seedIdleLimit', 'seedIdleMode', 'seedRatioLimit', 'seedRatioMode', 'uploadLimit', 'uploadLimited'] + 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.change_torrent(self.id, **args) + + def _update_fields(self, other): + """ + Update the torrent data from a Transmission JSON-RPC arguments dictionary + """ + fields = None + if isinstance(other, dict): + for key, value in other.iteritems(): + self._fields[key.replace('-', '_')] = Field(value, False) + elif isinstance(other, Torrent): + for key in other._fields.keys(): + self._fields[key] = Field(other._fields[key].value, False) + else: + raise ValueError('Cannot update with supplied data') + self._incoming_pending = False def _status_old(self, code): mapping = { @@ -82,26 +125,12 @@ class Torrent(object): return mapping[code] def _status(self): - code = self.fields['status'] + code = self._fields['status'].value 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 - """ - fields = None - if isinstance(other, dict): - fields = other - elif isinstance(other, Torrent): - fields = other.fields - else: - raise ValueError('Cannot update with supplied data') - for key, value in fields.iteritems(): - self.fields[key.replace('-', '_')] = value - def files(self): """ Get list of files for this torrent. @@ -122,11 +151,11 @@ class Torrent(object): } """ result = {} - if 'files' in self.fields: - indices = xrange(len(self.fields['files'])) - files = self.fields['files'] - priorities = self.fields['priorities'] - wanted = self.fields['wanted'] + if 'files' in self._fields: + files = self._fields['files'].value + indices = xrange(len(files)) + priorities = self._fields['priorities'].value + wanted = self._fields['wanted'].value for item in zip(indices, files, priorities, wanted): selected = True if item[3] else False priority = PRIORITY[item[2]] @@ -138,12 +167,6 @@ class Torrent(object): 'completed': item[1]['bytesCompleted']} return result - def __getattr__(self, name): - try: - return self.fields[name] - except KeyError: - raise AttributeError('No attribute %s' % name) - @property def status(self): """ @@ -157,19 +180,21 @@ class Torrent(object): def progress(self): """Get the download progress in percent.""" 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: return 0.0 @property def ratio(self): """Get the upload/download ratio.""" - return float(self.fields['uploadRatio']) + return float(self._fields['uploadRatio'].value) @property def eta(self): """Get the "eta" as datetime.timedelta.""" - eta = self.fields['eta'] + eta = self._fields['eta'].value if eta >= 0: return datetime.timedelta(seconds=eta) else: @@ -178,22 +203,22 @@ class Torrent(object): @property def date_active(self): """Get the attribute "activityDate" as datetime.datetime.""" - return datetime.datetime.fromtimestamp(self.fields['activityDate']) + return datetime.datetime.fromtimestamp(self._fields['activityDate'].value) @property def date_added(self): """Get the attribute "addedDate" as datetime.datetime.""" - return datetime.datetime.fromtimestamp(self.fields['addedDate']) + return datetime.datetime.fromtimestamp(self._fields['addedDate'].value) @property def date_started(self): """Get the attribute "startDate" as datetime.datetime.""" - return datetime.datetime.fromtimestamp(self.fields['startDate']) + return datetime.datetime.fromtimestamp(self._fields['startDate'].value) @property def date_done(self): """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): """ @@ -203,7 +228,7 @@ class Torrent(object): * If eta is -2 the result is 'unknown' * Otherwise eta is formatted as ::. """ - eta = self.fields['eta'] + eta = self._fields['eta'].value if eta == -1: return 'not available' elif eta == -2: @@ -211,10 +236,233 @@ class Torrent(object): else: return format_timedelta(self.eta) - @property - def priority(self): + def _get_download_limit(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, (int, long)): + 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, (int, long)): + 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. Can be one of 'low', 'normal', 'high'. """ - return PRIORITY[self.fields['bandwidthPriority']] \ No newline at end of file + 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, (str, unicode)): + 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, (int, long)): + 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, (int, long, 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, (int, long)): + 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): + if self._rpc_version() >= 14: + return self._fields['queuePosition'].value + else: + return 0 + + def _set_queue_position(self, position): + if self._rpc_version() >= 14: + if isinstance(position, (int, long)): + 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) diff --git a/libs/transmissionrpc/utils.py b/libs/transmissionrpc/utils.py index 8a74c11d..59f207a6 100755 --- a/libs/transmissionrpc/utils.py +++ b/libs/transmissionrpc/utils.py @@ -3,6 +3,7 @@ # Licensed under the MIT license. import socket, datetime, logging +from collections import namedtuple import transmissionrpc.constants as constants from transmissionrpc.constants import LOGGER @@ -186,3 +187,5 @@ def add_stdout_logger(level='debug'): trpc_logger.setLevel(loglevel) loghandler.setLevel(loglevel) trpc_logger.addHandler(loghandler) + +Field = namedtuple('Field', ['value', 'dirty']) \ No newline at end of file