Browse Source

Merge branch 'master' into develop

pull/1200/head
JackDandy 6 years ago
parent
commit
6fb9eb0cd7
  1. 6
      CHANGES.md
  2. 1
      HACKS.txt
  3. 2
      gui/slick/interfaces/default/config_search.tmpl
  4. 20
      gui/slick/js/configSearch.js
  5. 2
      lib/cfscrape/__init__.py
  6. 811
      lib/rtorrent/__init__.py
  7. 92
      lib/rtorrent/common.py
  8. 10
      lib/rtorrent/compat.py
  9. 8
      lib/rtorrent/err.py
  10. 79
      lib/rtorrent/file.py
  11. 55
      lib/rtorrent/group.py
  12. 78
      lib/rtorrent/lib/torrentparser.py
  13. 47
      lib/rtorrent/lib/xmlrpc/basic_auth.py
  14. 2
      lib/rtorrent/lib/xmlrpc/http.py
  15. 74
      lib/rtorrent/lib/xmlrpc/scgi.py
  16. 68
      lib/rtorrent/peer.py
  17. 226
      lib/rtorrent/rpc/__init__.py
  18. 517
      lib/rtorrent/torrent.py
  19. 77
      lib/rtorrent/tracker.py
  20. 2
      sickbeard/clients/generic.py
  21. 63
      sickbeard/clients/rtorrent.py

6
CHANGES.md

@ -33,6 +33,12 @@
### 0.19.6 (2019-06-24 00:15:00 UTC)
* Change add rTorrent 0.9.7 compatibility
* Change improve Cloudflare connectivity
### 0.19.5 (2019-06-13 18:25:00 UTC) ### 0.19.5 (2019-06-13 18:25:00 UTC)
* Update Js2Py 0.43 (da310bb) to 0.64 (efbfcca) * Update Js2Py 0.43 (da310bb) to 0.64 (efbfcca)

1
HACKS.txt

@ -10,6 +10,7 @@ Libs with customisations...
/lib/hachoir_parser/guess.py /lib/hachoir_parser/guess.py
/lib/hachoir_parser/misc/torrent.py /lib/hachoir_parser/misc/torrent.py
/lib/lockfile/mkdirlockfile.py /lib/lockfile/mkdirlockfile.py
/lib/rtorrent
/lib/scandir/scandir.py /lib/scandir/scandir.py
/lib/tmdb_api/tmdb_api.py /lib/tmdb_api/tmdb_api.py
/lib/tornado /lib/tornado

2
gui/slick/interfaces/default/config_search.tmpl

@ -516,7 +516,7 @@
</label> </label>
</div> </div>
<div class="field-pair"> <div class="field-pair" id="torrent-password-option">
<label> <label>
<span class="component-title" id="password-title">Client password</span> <span class="component-title" id="password-title">Client password</span>
<span class="component-desc"> <span class="component-desc">

20
gui/slick/js/configSearch.js

@ -1,7 +1,13 @@
/** @namespace $.SickGear.Root */
/** @namespace config.defaultHost */ /** @namespace config.defaultHost */
$(document).ready(function(){ $(document).ready(function(){
var loading = '<img src="' + sbRoot + '/images/loading16' + themeSpinner + '.gif" height="16" width="16" />'; var loading = '<img src="' + $.SickGear.Root + '/images/loading16' + themeSpinner + '.gif" height="16" width="16" />',
handleSCGI = function() {
var opts$ = $('#torrent-username-option,#torrent-password-option');
$('#torrent_username,#torrent_password').each(function() {$(this).val(
/^\s*scgi:\/\//.test($('#torrent_host').val()) ? opts$.hide() && $('#torrent_label').select() && ''
: opts$.show() && $('#torrent_username').select() && $(this).prop('defaultValue'));});};
function toggleTorrentTitle() { function toggleTorrentTitle() {
var noTorrent$ = $('#no_torrents'); var noTorrent$ = $('#no_torrents');
@ -65,7 +71,8 @@ $(document).ready(function(){
pathBlank = '#path-blank', pathBlank = '#path-blank',
seedTimeOption = '#torrent-seed-time-option', seedTimeOption = '#torrent-seed-time-option',
pausedOption = '#torrent-paused-option', pausedOption = '#torrent-paused-option',
highBandwidthOption = '#torrent-high-bandwidth-option'; highBandwidthOption = '#torrent-high-bandwidth-option',
torrentHost$ = $('#torrent_host');
$([labelWarningDeluge, hostDescDeluge, hostDescRtorrent, verifyCertOption, seedTimeOption, $([labelWarningDeluge, hostDescDeluge, hostDescRtorrent, verifyCertOption, seedTimeOption,
highBandwidthOption, qBitTorrent, synology, transmission].join(',')).hide(); highBandwidthOption, qBitTorrent, synology, transmission].join(',')).hide();
@ -73,6 +80,8 @@ $(document).ready(function(){
$([hostDesc, usernameOption, pathOption, labelOption, pathBlank, pausedOption].join(',')).show(); $([hostDesc, usernameOption, pathOption, labelOption, pathBlank, pausedOption].join(',')).show();
$(pathOption).find('.fileBrowser').show(); $(pathOption).find('.fileBrowser').show();
torrentHost$.off('blur');
switch (selectedProvider) { switch (selectedProvider) {
case 'utorrent': case 'utorrent':
client = 'uTorrent'; client = 'uTorrent';
@ -99,8 +108,9 @@ $(document).ready(function(){
$(synology).show(); $(synology).show();
break; break;
case 'rtorrent': case 'rtorrent':
client = 'rTorrent'; hideHostDesc = !0; hidePausedOption = !0; client = 'rTorrent'; hideHostDesc = !0;
$(hostDescRtorrent).show(); $(hostDescRtorrent).show();
torrentHost$.on('blur', handleSCGI);
break; break;
} }
hideHostDesc && $(hostDesc).hide(); hideHostDesc && $(hostDesc).hide();
@ -129,8 +139,10 @@ $(document).ready(function(){
$('#test_torrent').click(function() { $('#test_torrent').click(function() {
$('#test-torrent-result').html(loading); $('#test-torrent-result').html(loading);
var method = $('#torrent_method').find(':selected').val();
(('rtorrent' === method) && handleSCGI());
$.get(sbRoot + '/home/test_torrent', $.get(sbRoot + '/home/test_torrent',
{'torrent_method': $('#torrent_method').find(':selected').val(), 'host': $('#torrent_host').val(), {'torrent_method': method, 'host': $('#torrent_host').val(),
'username': $('#torrent_username').val(), 'password': $('#torrent_password').val()}, 'username': $('#torrent_username').val(), 'password': $('#torrent_password').val()},
function(data) { $(this).testResult(data, '#test-torrent-result'); }); function(data) { $(this).testResult(data, '#test-torrent-result'); });
}); });

2
lib/cfscrape/__init__.py

@ -199,7 +199,7 @@ class CloudflareScraper(Session):
([], ['GREASE_3A', 'GREASE_6A', 'AES128-GCM-SHA256', 'AES256-GCM-SHA256', 'AES256-GCM-SHA384', ([], ['GREASE_3A', 'GREASE_6A', 'AES128-GCM-SHA256', 'AES256-GCM-SHA256', 'AES256-GCM-SHA384',
'CHACHA20-POLY1305-SHA256'])[hasattr(ssl, 'PROTOCOL_TLSv1_3')] + 'CHACHA20-POLY1305-SHA256'])[hasattr(ssl, 'PROTOCOL_TLSv1_3')] +
['ECDHE-ECDSA-AES128-GCM-SHA256', 'ECDHE-RSA-AES128-GCM-SHA256', ['ECDHE-ECDSA-AES128-GCM-SHA256', 'ECDHE-RSA-AES128-GCM-SHA256',
'ECDHE-ECDSA-AES256-GCM-SHA384', 'ECDHE-RSA-AES256-GCM-SHA384', 'ECDHE-ECDSA-AES256-GCM-SHA384',
'ECDHE-ECDSA-CHACHA20-POLY1305-SHA256', 'ECDHE-RSA-CHACHA20-POLY1305-SHA256', 'ECDHE-ECDSA-CHACHA20-POLY1305-SHA256', 'ECDHE-RSA-CHACHA20-POLY1305-SHA256',
'ECDHE-RSA-AES128-CBC-SHA', 'ECDHE-RSA-AES256-CBC-SHA', 'RSA-AES128-GCM-SHA256', 'ECDHE-RSA-AES128-CBC-SHA', 'ECDHE-RSA-AES256-CBC-SHA', 'RSA-AES128-GCM-SHA256',
'RSA-AES256-GCM-SHA384', 'ECDHE-RSA-AES128-GCM-SHA256', 'RSA-AES256-SHA', '3DES-EDE-CBC']): 'RSA-AES256-GCM-SHA384', 'ECDHE-RSA-AES128-GCM-SHA256', 'RSA-AES256-SHA', '3DES-EDE-CBC']):

811
lib/rtorrent/__init__.py

File diff suppressed because it is too large

92
lib/rtorrent/common.py

@ -10,26 +10,25 @@
# The above copyright notice and this permission notice shall be # The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software. # included in all copies or substantial portions of the Software.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from compat import is_py3, urlparse, urlunparse, ParseResult
import os
from rtorrent.compat import is_py3
def bool_to_int(value): def bool_to_int(value):
"""Translates python booleans to RPC-safe integers""" """Translates python booleans to RPC-safe integers"""
if value is True: if value is True:
return("1") return '1'
elif value is False: elif value is False:
return("0") return '0'
else: return value
return(value)
def cmd_exists(cmds_list, cmd): def cmd_exists(cmds_list, cmd):
@ -44,7 +43,7 @@ def cmd_exists(cmds_list, cmd):
@return: bool @return: bool
""" """
return(cmd in cmds_list) return cmd in cmds_list
def find_torrent(info_hash, torrent_list): def find_torrent(info_hash, torrent_list):
@ -67,11 +66,11 @@ def find_torrent(info_hash, torrent_list):
def is_valid_port(port): def is_valid_port(port):
"""Check if given port is valid""" """Check if given port is valid"""
return(0 <= int(port) <= 65535) return 0 <= int(port) <= 65535
def convert_version_tuple_to_str(t): def convert_version_tuple_to_str(t):
return(".".join([str(n) for n in t])) return '.'.join([str(n) for n in t])
def safe_repr(fmt, *args, **kwargs): def safe_repr(fmt, *args, **kwargs):
@ -79,8 +78,73 @@ def safe_repr(fmt, *args, **kwargs):
if not is_py3(): if not is_py3():
# unicode fmt can take str args, str fmt cannot take unicode args # unicode fmt can take str args, str fmt cannot take unicode args
fmt = fmt.decode("utf-8") fmt = fmt.decode('utf-8')
out = fmt.format(*args, **kwargs) out = fmt.format(*args, **kwargs)
return out.encode("utf-8") return out.encode('utf-8')
else:
return fmt.format(*args, **kwargs) return fmt.format(*args, **kwargs)
def split_path(path):
fragments = path.split('/')
if 1 == len(fragments):
return fragments
if not fragments[-1]:
return fragments[:-1]
return fragments
def join_path(base, path):
# Return if we have a new absolute path
if os.path.isabs(path):
return path
# non-absolute base encountered
if base and not os.path.isabs(base):
raise NotImplementedError()
return '/'.join(split_path(base) + split_path(path))
def join_uri(base, uri, construct=True):
p_uri = urlparse(uri)
# Return if there is nothing to join
if not p_uri.path:
return base
scheme, netloc, path, params, query, fragment = urlparse(base)
# Switch to 'uri' parts
_, _, _, params, query, fragment = p_uri
path = join_path(path, p_uri.path)
result = ParseResult(scheme, netloc, path, params, query, fragment)
if not construct:
return result
# Construct from parts
return urlunparse(result)
def update_uri(uri, construct=True, **kwargs):
if isinstance(uri, ParseResult):
# noinspection PyProtectedMember
uri = dict(uri._asdict())
if type(uri) is not dict:
raise ValueError('Unknown URI type')
uri.update(kwargs)
result = ParseResult(**uri)
if not construct:
return result
return urlunparse(result)

10
lib/rtorrent/compat.py

@ -24,7 +24,17 @@ import sys
def is_py3(): def is_py3():
return sys.version_info[0] == 3 return sys.version_info[0] == 3
if is_py3(): if is_py3():
# noinspection PyCompatibility
from urllib.parse import parse_qs, ParseResult, urlparse, urlunparse
# noinspection PyCompatibility
from urllib.request import urlopen
import xmlrpc.client as xmlrpclib import xmlrpc.client as xmlrpclib
else: else:
from urllib2 import urlopen
# noinspection PyProtectedMember
from urlparse import parse_qs, ParseResult, urlparse, urlunparse
import xmlrpclib import xmlrpclib

8
lib/rtorrent/err.py

@ -18,18 +18,18 @@
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from rtorrent.common import convert_version_tuple_to_str from common import convert_version_tuple_to_str
class RTorrentVersionError(Exception): class RTorrentVersionError(Exception):
def __init__(self, min_version, cur_version): def __init__(self, min_version, cur_version):
self.min_version = min_version self.min_version = min_version
self.cur_version = cur_version self.cur_version = cur_version
self.msg = "Minimum version required: {0}".format( self.msg = 'Minimum version required: {0}'.format(
convert_version_tuple_to_str(min_version)) convert_version_tuple_to_str(min_version))
def __str__(self): def __str__(self):
return(self.msg) return self.msg
class MethodError(Exception): class MethodError(Exception):
@ -37,4 +37,4 @@ class MethodError(Exception):
self.msg = msg self.msg = msg
def __str__(self): def __str__(self):
return(self.msg) return self.msg

79
lib/rtorrent/file.py

@ -18,25 +18,22 @@
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# from rtorrent.rpc import Method from .common import safe_repr
import rtorrent.rpc from .rpc import Method
import rpc
from rtorrent.common import safe_repr
Method = rtorrent.rpc.Method class File(object):
class File:
"""Represents an individual file within a L{Torrent} instance.""" """Represents an individual file within a L{Torrent} instance."""
def __init__(self, _rt_obj, info_hash, index, **kwargs): def __init__(self, _rt_obj, info_hash, index, **kwargs):
self._rt_obj = _rt_obj self._rt_obj = _rt_obj
self.info_hash = info_hash # : info hash for the torrent the file is associated with self.info_hash = info_hash # : info hash for the torrent the file is associated with
self.index = index # : The position of the file within the file list self.index = index # : The position of the file within the file list
for k in kwargs.keys(): for k in kwargs:
setattr(self, k, kwargs.get(k, None)) setattr(self, k, kwargs.get(k, None))
self.rpc_id = "{0}:f{1}".format( self.rpc_id = '{0}:f{1}'.format(
self.info_hash, self.index) # : unique id to pass to rTorrent self.info_hash, self.index) # : unique id to pass to rTorrent
def update(self): def update(self):
@ -46,44 +43,58 @@ class File:
@return: None @return: None
""" """
multicall = rtorrent.rpc.Multicall(self) mc = rpc.Multicall(self)
retriever_methods = [m for m in methods
if m.is_retriever() and m.is_available(self._rt_obj)]
for method in retriever_methods:
multicall.add(method, self.rpc_id)
multicall.call() for method in filter(lambda m: m.is_retriever() and m.is_available(self._rt_obj), methods):
mc.add(method, self.rpc_id)
mc.call()
def __repr__(self): def __repr__(self):
return safe_repr("File(index={0} path=\"{1}\")", self.index, self.path) return safe_repr('File(index={0} path="{1}")', self.index, self.path)
methods = [ methods = [
# RETRIEVERS # RETRIEVERS
Method(File, 'get_last_touched', 'f.get_last_touched'), Method(File, 'get_last_touched', 'f.get_last_touched',
Method(File, 'get_range_second', 'f.get_range_second'), aliases=('f.last_touched',)),
Method(File, 'get_size_bytes', 'f.get_size_bytes'), Method(File, 'get_range_second', 'f.get_range_second',
Method(File, 'get_priority', 'f.get_priority'), aliases=('f.range_second',)),
Method(File, 'get_match_depth_next', 'f.get_match_depth_next'), Method(File, 'get_size_bytes', 'f.get_size_bytes',
aliases=('f.size_bytes',)),
Method(File, 'get_priority', 'f.get_priority',
aliases=('f.priority',)),
Method(File, 'get_match_depth_next', 'f.get_match_depth_next',
aliases=('f.match_depth_next',)),
Method(File, 'is_resize_queued', 'f.is_resize_queued', Method(File, 'is_resize_queued', 'f.is_resize_queued',
boolean=True, boolean=True
), ),
Method(File, 'get_range_first', 'f.get_range_first'), Method(File, 'get_range_first', 'f.get_range_first',
Method(File, 'get_match_depth_prev', 'f.get_match_depth_prev'), aliases=('f.range_first',)),
Method(File, 'get_path', 'f.get_path'), Method(File, 'get_match_depth_prev', 'f.get_match_depth_prev',
Method(File, 'get_completed_chunks', 'f.get_completed_chunks'), aliases=('f.match_depth_prev',)),
Method(File, 'get_path_components', 'f.get_path_components'), Method(File, 'get_path', 'f.get_path',
aliases=('f.path',)),
Method(File, 'get_completed_chunks', 'f.get_completed_chunks',
aliases=('f.completed_chunks',)),
Method(File, 'get_path_components', 'f.get_path_components',
aliases=('f.path_components',)),
Method(File, 'is_created', 'f.is_created', Method(File, 'is_created', 'f.is_created',
boolean=True, boolean=True
), ),
Method(File, 'is_open', 'f.is_open', Method(File, 'is_open', 'f.is_open',
boolean=True, boolean=True
), ),
Method(File, 'get_size_chunks', 'f.get_size_chunks'), Method(File, 'get_size_chunks', 'f.get_size_chunks',
Method(File, 'get_offset', 'f.get_offset'), aliases=('f.size_chunks',)),
Method(File, 'get_frozen_path', 'f.get_frozen_path'), Method(File, 'get_offset', 'f.get_offset',
Method(File, 'get_path_depth', 'f.get_path_depth'), aliases=('f.offset',)),
Method(File, 'get_frozen_path', 'f.get_frozen_path',
aliases=('f.frozen_path',)),
Method(File, 'get_path_depth', 'f.get_path_depth',
aliases=('f.path_depth',)),
Method(File, 'is_create_queued', 'f.is_create_queued', Method(File, 'is_create_queued', 'f.is_create_queued',
boolean=True, boolean=True
), ),

55
lib/rtorrent/group.py

@ -18,12 +18,11 @@
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import rtorrent.rpc from .rpc import Method
import rpc
Method = rtorrent.rpc.Method
class Group(object):
class Group:
__name__ = 'Group' __name__ = 'Group'
def __init__(self, _rt_obj, name): def __init__(self, _rt_obj, name):
@ -42,43 +41,49 @@ class Group:
Method(Group, 'set_upload', 'group.' + self.name + '.ratio.upload.set', varname='upload') Method(Group, 'set_upload', 'group.' + self.name + '.ratio.upload.set', varname='upload')
] ]
rtorrent.rpc._build_rpc_methods(self, self.methods) # noinspection PyProtectedMember
rpc._build_rpc_methods(self, self.methods)
# Setup multicall_add method # Setup multicall_add method
caller = lambda multicall, method, *args: \ caller = (lambda mc, method, *args: mc.add(method, *args))
multicall.add(method, *args) setattr(self, 'multicall_add', caller)
setattr(self, "multicall_add", caller)
def _get_prefix(self): def _get_prefix(self):
return 'group.' + self.name + '.ratio.' return 'group.' + self.name + '.ratio.'
def update(self): def update(self):
multicall = rtorrent.rpc.Multicall(self) mc = rpc.Multicall(self)
retriever_methods = [m for m in self.methods
if m.is_retriever() and m.is_available(self._rt_obj)]
for method in retriever_methods: for method in filter(lambda m: m.is_retriever() and m.is_available(self._rt_obj), self.methods):
multicall.add(method) mc.add(method)
multicall.call() mc.call()
def enable(self): def enable(self):
p = self._rt_obj._get_conn() p = self._rt_obj.get_connection()
return getattr(p, self._get_prefix() + 'enable')() return getattr(p, self._get_prefix() + 'enable')()
def disable(self): def disable(self):
p = self._rt_obj._get_conn() p = self._rt_obj.get_connection()
return getattr(p, self._get_prefix() + 'disable')() return getattr(p, self._get_prefix() + 'disable')()
def _get_method(self, *choices):
try:
return filter(lambda method: self._rt_obj.method_exists(method), choices)[0]
except(Exception, BaseException):
pass
def set_command(self, *methods): def set_command(self, *methods):
methods = [m + '=' for m in methods] method = self._get_method(*('system.method.set', 'method.set'))
if method:
methods = [m + '=' for m in methods]
mc = rpc.Multicall(self)
m = rtorrent.rpc.Multicall(self) self.multicall_add(
self.multicall_add( mc, method,
m, 'system.method.set', self._get_prefix() + 'command',
self._get_prefix() + 'command', *methods
*methods )
)
return(m.call()[-1]) return mc.call()[-1]

78
lib/rtorrent/lib/torrentparser.py

@ -18,19 +18,15 @@
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from rtorrent.compat import is_py3 from ..compat import parse_qs, urlopen, urlparse
import bencode
import hashlib
import os.path import os.path
import re import re
import rtorrent.lib.bencode as bencode
import hashlib
if is_py3():
from urllib.request import urlopen # @UnresolvedImport @UnusedImport
else:
from urllib2 import urlopen # @UnresolvedImport @Reimport
class TorrentParser(object):
class TorrentParser():
def __init__(self, torrent): def __init__(self, torrent):
"""Decode and parse given torrent """Decode and parse given torrent
@ -48,12 +44,16 @@ class TorrentParser():
self.file_type = None self.file_type = None
self._get_raw_torrent() self._get_raw_torrent()
assert self._raw_torrent is not None, "Couldn't get raw_torrent." assert self._raw_torrent is not None, 'Couldn\'t get raw_torrent.'
if self._torrent_decoded is None: if self._torrent_decoded is None:
self._decode_torrent() self._decode_torrent()
assert isinstance(self._torrent_decoded, dict), "Invalid torrent file." assert isinstance(self._torrent_decoded, dict), 'Invalid torrent file.'
self._parse_torrent() self._parse_torrent()
@property
def raw_torrent(self):
return self._raw_torrent
def _is_raw(self): def _is_raw(self):
raw = False raw = False
if isinstance(self.torrent, (str, bytes)): if isinstance(self.torrent, (str, bytes)):
@ -63,43 +63,57 @@ class TorrentParser():
# reset self._torrent_decoded (currently equals False) # reset self._torrent_decoded (currently equals False)
self._torrent_decoded = None self._torrent_decoded = None
return(raw) return raw
def _get_raw_torrent(self): def _get_raw_torrent(self):
"""Get raw torrent data by determining what self.torrent is""" """Get raw torrent data by determining what self.torrent is"""
# already raw? # already raw?
if self._is_raw(): if self._is_raw():
self.file_type = "raw" self.file_type = 'raw'
self._raw_torrent = self.torrent self._raw_torrent = self.torrent
return return
# local file? # local file?
if os.path.isfile(self.torrent): if os.path.isfile(self.torrent):
self.file_type = "file" self.file_type = 'file'
self._raw_torrent = open(self.torrent, "rb").read() self._raw_torrent = open(self.torrent, 'rb').read()
# url? # url?
elif re.search("^(http|ftp)s?:\/\/", self.torrent, re.I): elif re.search(r'^(http|ftp)s?://', self.torrent, re.I):
self.file_type = "url" self.file_type = 'url'
self._raw_torrent = urlopen(self.torrent).read() self._raw_torrent = urlopen(self.torrent).read()
# magnet link?
elif re.match(r'magnet:', self.torrent, re.I):
self.file_type = 'url'
query = {}
try:
parsed = urlparse(self.torrent)
query = parse_qs(parsed.query)
except(Exception, BaseException):
pass
self.title = query.get('dn', [''])[0]
self.info_hash = query.get('xt', [''])[0].split(':')[-1]
self.trackers = query.get('tr', [])
self._torrent_decoded = query
self._raw_torrent = self.torrent
def _decode_torrent(self, raw_torrent=None): def _decode_torrent(self, raw_torrent=None):
if raw_torrent is None: if raw_torrent is None:
raw_torrent = self._raw_torrent raw_torrent = self._raw_torrent
self._torrent_decoded = bencode.decode(raw_torrent) self._torrent_decoded = bencode.decode(raw_torrent)
return(self._torrent_decoded) return self._torrent_decoded
def _calc_info_hash(self): def _calc_info_hash(self):
self.info_hash = None self.info_hash = None
if "info" in self._torrent_decoded.keys(): if 'info' in self._torrent_decoded:
info_encoded = bencode.encode(self._torrent_decoded["info"]) info_encoded = bencode.encode(self._torrent_decoded['info'])
if info_encoded: if info_encoded:
self.info_hash = hashlib.sha1(info_encoded).hexdigest().upper() self.info_hash = hashlib.sha1(info_encoded).hexdigest().upper()
return(self.info_hash) return self.info_hash
def _parse_torrent(self): def _parse_torrent(self):
for k in self._torrent_decoded: for k in self._torrent_decoded:
key = k.replace(" ", "_").lower() key = k.replace(' ', '_').lower()
setattr(self, key, self._torrent_decoded[k]) setattr(self, key, self._torrent_decoded[k])
self._calc_info_hash() self._calc_info_hash()
@ -119,8 +133,8 @@ class NewTorrentParser(object):
def _decode_torrent(data): def _decode_torrent(data):
return bencode.decode(data) return bencode.decode(data)
def __init__(self, input): def __init__(self, infile):
self.input = input self.input = infile
self._raw_torrent = None self._raw_torrent = None
self._decoded_torrent = None self._decoded_torrent = None
self._hash_outdated = False self._hash_outdated = False
@ -128,33 +142,33 @@ class NewTorrentParser(object):
if isinstance(self.input, (str, bytes)): if isinstance(self.input, (str, bytes)):
# path to file? # path to file?
if os.path.isfile(self.input): if os.path.isfile(self.input):
self._raw_torrent = self._read_file(open(self.input, "rb")) self._raw_torrent = self._read_file(open(self.input, 'rb'))
else: else:
# assume input was the raw torrent data (do we really want # assume input was the raw torrent data (do we really want
# this?) # this?)
self._raw_torrent = self.input self._raw_torrent = self.input
# file-like object? # file-like object?
elif self.input.hasattr("read"): elif self.input.hasattr('read'):
self._raw_torrent = self._read_file(self.input) self._raw_torrent = self._read_file(self.input)
assert self._raw_torrent is not None, "Invalid input: input must be a path or a file-like object" assert self._raw_torrent is not None, 'Invalid input: input must be a path or a file-like object'
self._decoded_torrent = self._decode_torrent(self._raw_torrent) self._decoded_torrent = self._decode_torrent(self._raw_torrent)
assert isinstance( assert isinstance(
self._decoded_torrent, dict), "File could not be decoded" self._decoded_torrent, dict), 'File could not be decoded'
def _calc_info_hash(self): def _calc_info_hash(self):
self.info_hash = None self.info_hash = None
info_dict = self._torrent_decoded["info"] info_dict = self._torrent_decoded['info']
self.info_hash = hashlib.sha1(bencode.encode( self.info_hash = hashlib.sha1(bencode.encode(
info_dict)).hexdigest().upper() info_dict)).hexdigest().upper()
return(self.info_hash) return self.info_hash
def set_tracker(self, tracker): def set_tracker(self, tracker):
self._decoded_torrent["announce"] = tracker self._decoded_torrent['announce'] = tracker
def get_tracker(self): def get_tracker(self):
return self._decoded_torrent.get("announce") return self._decoded_torrent.get('announce')

47
lib/rtorrent/lib/xmlrpc/basic_auth.py

@ -20,27 +20,44 @@
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from base64 import encodestring from ...compat import xmlrpclib
import string
try: from base64 import b64encode
import xmlrpc.client as xmlrpclib import httplib
except:
import xmlrpclib
class BasicAuthTransport(xmlrpclib.Transport): class BasicAuthTransport(xmlrpclib.Transport):
def __init__(self, username=None, password=None): def __init__(self, secure=False, username=None, password=None):
xmlrpclib.Transport.__init__(self) xmlrpclib.Transport.__init__(self)
self.secure = secure
self.username = username self.username = username
self.password = password self.password = password
self.verbose = None
def send_auth(self, h): def send_auth(self, h):
if self.username is not None and self.password is not None: if self.username and self.password:
h.putheader('AUTHORIZATION', "Basic %s" % string.replace( h.putheader('Authorization', 'Basic %s' % b64encode('%s:%s' % (self.username, self.password)))
encodestring("%s:%s" % (self.username, self.password)),
"\012", "" def make_connection(self, host):
)) if self._connection and host == self._connection[0]:
return self._connection[1]
chost, self._extra_headers, x509 = self.get_host_info(host)
if self.secure:
try:
self._connection = host, httplib.HTTPSConnection(chost, None, **(x509 or {}))
except AttributeError:
raise NotImplementedError(
'In use version of httplib doesn\'t support HTTPS'
)
else:
self._connection = host, httplib.HTTPConnection(chost)
return self._connection[1]
def single_request(self, host, handler, request_body, verbose=0): def single_request(self, host, handler, request_body, verbose=0):
# issue XML-RPC request # issue XML-RPC request
@ -57,7 +74,7 @@ class BasicAuthTransport(xmlrpclib.Transport):
self.send_content(h, request_body) self.send_content(h, request_body)
response = h.getresponse(buffering=True) response = h.getresponse(buffering=True)
if response.status == 200: if 200 == response.status:
self.verbose = verbose self.verbose = verbose
return self.parse_response(response) return self.parse_response(response)
except xmlrpclib.Fault: except xmlrpclib.Fault:
@ -66,8 +83,8 @@ class BasicAuthTransport(xmlrpclib.Transport):
self.close() self.close()
raise raise
#discard any response data and raise exception # discard any response data and raise exception
if response.getheader("content-length", 0): if response.getheader('content-length', 0):
response.read() response.read()
raise xmlrpclib.ProtocolError( raise xmlrpclib.ProtocolError(
host + handler, host + handler,

2
lib/rtorrent/lib/xmlrpc/http.py

@ -18,6 +18,6 @@
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from rtorrent.compat import xmlrpclib from ...compat import xmlrpclib
HTTPServerProxy = xmlrpclib.ServerProxy HTTPServerProxy = xmlrpclib.ServerProxy

74
lib/rtorrent/lib/xmlrpc/scgi.py

@ -82,39 +82,14 @@
# OF THIS SOFTWARE. # OF THIS SOFTWARE.
from __future__ import print_function from __future__ import print_function
try:
import http.client as httplib
except ImportError:
import httplib
import re import re
import socket import socket
import sys import sys
try:
import urllib.parse as urlparser
except ImportError:
import urllib as urlparser
try: from ...compat import urlparse, xmlrpclib
import xmlrpc.client as xmlrpclib
except:
import xmlrpclib
import errno
class SCGITransport(xmlrpclib.Transport): class SCGITransport(xmlrpclib.Transport):
# Added request() from Python 2.7 xmlrpclib here to backport to Python 2.6
def request(self, host, handler, request_body, verbose=0):
#retry request once if cached connection has gone cold
for i in (0, 1):
try:
return self.single_request(host, handler, request_body, verbose)
except socket.error as e:
if i or e.errno not in (errno.ECONNRESET, errno.ECONNABORTED, errno.EPIPE):
raise
except httplib.BadStatusLine: #close after we sent request
if i:
raise
def single_request(self, host, handler, request_body, verbose=0): def single_request(self, host, handler, request_body, verbose=0):
# Add SCGI headers to the request. # Add SCGI headers to the request.
@ -127,7 +102,9 @@ class SCGITransport(xmlrpclib.Transport):
try: try:
if host: if host:
host, port = urlparser.splitport(host) parsed = urlparse('//' + host)
host, port = parsed.hostname, parsed.port
addrinfo = socket.getaddrinfo(host, int(port), socket.AF_INET, addrinfo = socket.getaddrinfo(host, int(port), socket.AF_INET,
socket.SOCK_STREAM) socket.SOCK_STREAM)
sock = socket.socket(*addrinfo[0][:3]) sock = socket.socket(*addrinfo[0][:3])
@ -136,10 +113,12 @@ class SCGITransport(xmlrpclib.Transport):
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(handler) sock.connect(handler)
# noinspection PyAttributeOutsideInit
self.verbose = verbose self.verbose = verbose
if sys.version_info[0] > 2: if sys.version_info[0] > 2:
sock.send(bytes(request_body, "utf-8")) # noinspection PyArgumentList
sock.send(bytes(request_body, 'utf-8')) # py3
else: else:
sock.send(request_body) sock.send(request_body)
return self.parse_response(sock.makefile()) return self.parse_response(sock.makefile())
@ -163,10 +142,9 @@ class SCGITransport(xmlrpclib.Transport):
print('body:', repr(response_body)) print('body:', repr(response_body))
try: try:
response_header, response_body = re.split(r'\n\s*?\n', response_body, response_header, response_body = re.split(r'\n\s*?\n', response_body, maxsplit=1)
maxsplit=1)
except ValueError: except ValueError:
print("error in response: %s", response_body) print('error in response: %s', response_body)
p.close() p.close()
u.close() u.close()
@ -177,12 +155,14 @@ class SCGITransport(xmlrpclib.Transport):
class SCGIServerProxy(xmlrpclib.ServerProxy): class SCGIServerProxy(xmlrpclib.ServerProxy):
def __init__(self, uri, transport=None, encoding=None, verbose=False,
allow_none=False, use_datetime=False): # noinspection PyMissingConstructor
type, uri = urlparser.splittype(uri) def __init__(self, uri, transport=None, encoding=None, verbose=False, allow_none=False, use_datetime=False):
if type not in ('scgi'):
parsed = urlparse(uri)
if 'scgi' != parsed.scheme:
raise IOError('unsupported XML-RPC protocol') raise IOError('unsupported XML-RPC protocol')
self.__host, self.__handler = urlparser.splithost(uri) self.__host, self.__handler = parsed.netloc, parsed.path
if not self.__handler: if not self.__handler:
self.__handler = '/' self.__handler = '/'
@ -194,14 +174,10 @@ class SCGIServerProxy(xmlrpclib.ServerProxy):
self.__verbose = verbose self.__verbose = verbose
self.__allow_none = allow_none self.__allow_none = allow_none
def __close(self):
self.__transport.close()
def __request(self, methodname, params): def __request(self, methodname, params):
# call a method on the remote server # call a method on the remote server
request = xmlrpclib.dumps(params, methodname, encoding=self.__encoding, request = xmlrpclib.dumps(params, methodname, encoding=self.__encoding, allow_none=self.__allow_none)
allow_none=self.__allow_none)
response = self.__transport.request( response = self.__transport.request(
self.__host, self.__host,
@ -210,22 +186,22 @@ class SCGIServerProxy(xmlrpclib.ServerProxy):
verbose=self.__verbose verbose=self.__verbose
) )
if len(response) == 1: if 1 == len(response):
response = response[0] response = response[0]
return response return response
def __repr__(self): def __repr__(self):
return ( return ('<SCGIServerProxy for %s%s>' %
"<SCGIServerProxy for %s%s>" % (self.__host, self.__handler))
(self.__host, self.__handler)
)
__str__ = __repr__ __str__ = __repr__
def __getattr__(self, name): def __getattr__(self, name):
# magic method dispatcher # magic method dispatcher
# noinspection PyProtectedMember
return xmlrpclib._Method(self.__request, name) return xmlrpclib._Method(self.__request, name)
# return _Method(self.__request, name)
# note: to call a remote object with an non-standard name, use # note: to call a remote object with an non-standard name, use
# result getattr(server, "strange-python-name")(args) # result getattr(server, "strange-python-name")(args)
@ -234,8 +210,8 @@ class SCGIServerProxy(xmlrpclib.ServerProxy):
"""A workaround to get special attributes on the ServerProxy """A workaround to get special attributes on the ServerProxy
without interfering with the magic __getattr__ without interfering with the magic __getattr__
""" """
if attr == "close": if 'close' == attr:
return self.__close return self.__close
elif attr == "transport": elif 'transport' == attr:
return self.__transport return self.__transport
raise AttributeError("Attribute %r not found" % (attr,)) raise AttributeError('Attribute %r not found' % (attr,))

68
lib/rtorrent/peer.py

@ -18,27 +18,24 @@
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# from rtorrent.rpc import Method from .common import safe_repr
import rtorrent.rpc from .rpc import Method
import rpc
from rtorrent.common import safe_repr
Method = rtorrent.rpc.Method class Peer(object):
class Peer:
"""Represents an individual peer within a L{Torrent} instance.""" """Represents an individual peer within a L{Torrent} instance."""
def __init__(self, _rt_obj, info_hash, **kwargs): def __init__(self, _rt_obj, info_hash, **kwargs):
self._rt_obj = _rt_obj self._rt_obj = _rt_obj
self.info_hash = info_hash # : info hash for the torrent the peer is associated with self.info_hash = info_hash # : info hash for the torrent the peer is associated with
for k in kwargs.keys(): for k in kwargs:
setattr(self, k, kwargs.get(k, None)) setattr(self, k, kwargs.get(k, None))
self.rpc_id = "{0}:p{1}".format( self.rpc_id = '{0}:p{1}'.format(
self.info_hash, self.id) # : unique id to pass to rTorrent self.info_hash, self.id) # : unique id to pass to rTorrent
def __repr__(self): def __repr__(self):
return safe_repr("Peer(id={0})", self.id) return safe_repr('Peer(id={0})', self.id)
def update(self): def update(self):
"""Refresh peer data """Refresh peer data
@ -47,52 +44,65 @@ class Peer:
@return: None @return: None
""" """
multicall = rtorrent.rpc.Multicall(self) mc = rpc.Multicall(self)
retriever_methods = [m for m in methods
if m.is_retriever() and m.is_available(self._rt_obj)] for method in filter(lambda fx: fx.is_retriever() and fx.is_available(self._rt_obj), methods):
for method in retriever_methods: mc.add(method, self.rpc_id)
multicall.add(method, self.rpc_id)
mc.call()
multicall.call()
methods = [ methods = [
# RETRIEVERS # RETRIEVERS
Method(Peer, 'is_preferred', 'p.is_preferred', Method(Peer, 'is_preferred', 'p.is_preferred',
boolean=True, boolean=True,
), ),
Method(Peer, 'get_down_rate', 'p.get_down_rate'), Method(Peer, 'get_down_rate', 'p.get_down_rate',
aliases=('p.down_rate',)),
Method(Peer, 'is_unwanted', 'p.is_unwanted', Method(Peer, 'is_unwanted', 'p.is_unwanted',
boolean=True, boolean=True,
), ),
Method(Peer, 'get_peer_total', 'p.get_peer_total'), Method(Peer, 'get_peer_total', 'p.get_peer_total',
Method(Peer, 'get_peer_rate', 'p.get_peer_rate'), aliases=('p.peer_total',)),
Method(Peer, 'get_port', 'p.get_port'), Method(Peer, 'get_peer_rate', 'p.get_peer_rate',
aliases=('p.peer_rate',)),
Method(Peer, 'get_port', 'p.get_port',
aliases=('p.port',)),
Method(Peer, 'is_snubbed', 'p.is_snubbed', Method(Peer, 'is_snubbed', 'p.is_snubbed',
boolean=True, boolean=True,
), ),
Method(Peer, 'get_id_html', 'p.get_id_html'), Method(Peer, 'get_id_html', 'p.get_id_html',
Method(Peer, 'get_up_rate', 'p.get_up_rate'), aliases=('p.id_html',)),
Method(Peer, 'get_up_rate', 'p.get_up_rate',
aliases=('p.up_rate',)),
Method(Peer, 'is_banned', 'p.banned', Method(Peer, 'is_banned', 'p.banned',
boolean=True, boolean=True,
), ),
Method(Peer, 'get_completed_percent', 'p.get_completed_percent'), Method(Peer, 'get_completed_percent', 'p.get_completed_percent',
aliases=('p.completed_percent',)),
Method(Peer, 'completed_percent', 'p.completed_percent'), Method(Peer, 'completed_percent', 'p.completed_percent'),
Method(Peer, 'get_id', 'p.get_id'), Method(Peer, 'get_id', 'p.get_id',
aliases=('p.id',)),
Method(Peer, 'is_obfuscated', 'p.is_obfuscated', Method(Peer, 'is_obfuscated', 'p.is_obfuscated',
boolean=True, boolean=True,
), ),
Method(Peer, 'get_down_total', 'p.get_down_total'), Method(Peer, 'get_down_total', 'p.get_down_total',
Method(Peer, 'get_client_version', 'p.get_client_version'), aliases=('p.down_total',)),
Method(Peer, 'get_address', 'p.get_address'), Method(Peer, 'get_client_version', 'p.get_client_version',
aliases=('p.client_version',)),
Method(Peer, 'get_address', 'p.get_address',
aliases=('p.address',)),
Method(Peer, 'is_incoming', 'p.is_incoming', Method(Peer, 'is_incoming', 'p.is_incoming',
boolean=True, boolean=True,
), ),
Method(Peer, 'is_encrypted', 'p.is_encrypted', Method(Peer, 'is_encrypted', 'p.is_encrypted',
boolean=True, boolean=True,
), ),
Method(Peer, 'get_options_str', 'p.get_options_str'), Method(Peer, 'get_options_str', 'p.get_options_str',
aliases=('p.options_str',)),
Method(Peer, 'get_client_version', 'p.client_version'), Method(Peer, 'get_client_version', 'p.client_version'),
Method(Peer, 'get_up_total', 'p.get_up_total'), Method(Peer, 'get_up_total', 'p.get_up_total',
aliases=('p.up_total',)),
# MODIFIERS # MODIFIERS
] ]

226
lib/rtorrent/rpc/__init__.py

@ -10,7 +10,7 @@
# The above copyright notice and this permission notice shall be # The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software. # included in all copies or substantial portions of the Software.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
@ -18,13 +18,14 @@
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from ..common import bool_to_int, convert_version_tuple_to_str, safe_repr
from ..compat import xmlrpclib
from ..err import MethodError
import inspect import inspect
import rtorrent
import re import re
from rtorrent.common import bool_to_int, convert_version_tuple_to_str,\
safe_repr import rtorrent
from rtorrent.err import MethodError
from rtorrent.compat import xmlrpclib
def get_varname(rpc_call): def get_varname(rpc_call):
@ -36,91 +37,78 @@ def get_varname(rpc_call):
""" """
# extract variable name from xmlrpc func name # extract variable name from xmlrpc func name
r = re.search( r = re.search(
"([ptdf]\.|system\.|get\_|is\_|set\_)+([^=]*)", rpc_call, re.I) r'(^([dtfp]|system)\.|(is|[sg]et)_)+([^=]*?)(?:=|.[sg]et)?$', rpc_call, re.I)
if r: if r:
return(r.groups()[-1]) return r.groups()[-1]
else:
return(None)
def _handle_unavailable_rpc_method(method, rt_obj): def _handle_unavailable_rpc_method(method, rt_obj):
msg = "Method isn't available." msg = 'Method isn\'t available.'
if rt_obj._get_client_version_tuple() < method.min_version: if rt_obj.get_client_version_tuple() < method.min_version:
msg = "This method is only available in " \ msg = 'This method is only available in ' \
"RTorrent version v{0} or later".format( 'RTorrent version v{0} or later'.format(convert_version_tuple_to_str(method.min_version))
convert_version_tuple_to_str(method.min_version))
raise MethodError(msg) raise MethodError(msg)
class DummyClass: class DummyClass(object):
def __init__(self): def __init__(self):
pass pass
class Method: class Method(object):
"""Represents an individual RPC method""" """Represents an individual RPC method"""
def __init__(self, _class, method_name, def __init__(self, _class, method_name, rpc_call, docstring=None, **kwargs):
rpc_call, docstring=None, varname=None, **kwargs):
self._class = _class # : Class this method is associated with self._class = _class # : Class this method is associated with
self.class_name = _class.__name__ self.class_name = _class.__name__
self.method_name = method_name # : name of public-facing method self.method_name = method_name # : name of public-facing method
self.rpc_call = rpc_call # : name of rpc method self.rpc_call = rpc_call # : name of rpc method
self.docstring = docstring # : docstring for rpc method (optional) self.docstring = docstring # : docstring for rpc method (optional)
self.varname = varname # : variable for the result of the method call, usually set to self.varname self.min_version = kwargs.get('min_version', (0, 0, 0)) # : Minimum version of rTorrent required
self.min_version = kwargs.get("min_version", ( self.boolean = kwargs.get('boolean', False) # : returns boolean value?
0, 0, 0)) # : Minimum version of rTorrent required self.post_process_func = kwargs.get('post_process_func', None) # : custom post process function
self.boolean = kwargs.get("boolean", False) # : returns boolean value? self.aliases = kwargs.get('aliases', []) # : aliases for method (optional)
self.post_process_func = kwargs.get( self.required_args = [] # : Arguments required when calling the method (not utilized)
"post_process_func", None) # : custom post process function
self.aliases = kwargs.get(
"aliases", []) # : aliases for method (optional)
self.required_args = []
#: Arguments required when calling the method (not utilized)
self.method_type = self._get_method_type() self.method_type = self._get_method_type()
self.varname = None # : variable for the result of the method call, usually set to self.varname
if self.varname is None:
self.varname = get_varname(self.rpc_call)
assert self.varname is not None, "Couldn't get variable name."
def __repr__(self): def __repr__(self):
return safe_repr("Method(method_name='{0}', rpc_call='{1}')", return safe_repr('Method(method_name="{0}", rpc_call="{1}")', self.method_name, self.rpc_call)
self.method_name, self.rpc_call)
def _get_method_type(self): def _get_method_type(self):
"""Determine whether method is a modifier or a retriever""" """Determine whether method is a modifier or a retriever"""
if self.method_name[:4] == "set_": return('m') # modifier if 'set_' == self.method_name[:4]:
else: return 'm' # modifier
return('r') # retriever return 'r' # retriever
def is_modifier(self): def is_modifier(self):
if self.method_type == 'm': return 'm' == self.method_type
return(True)
else:
return(False)
def is_retriever(self): def is_retriever(self):
if self.method_type == 'r': return 'r' == self.method_type
return(True)
else:
return(False)
def is_available(self, rt_obj): def is_available(self, rt_obj):
if rt_obj._get_client_version_tuple() < self.min_version or \
self.rpc_call not in rt_obj._get_rpc_methods(): if rt_obj.get_client_version_tuple() >= self.min_version:
return(False) try:
else: self.varname = get_varname(filter(lambda f: rt_obj.method_exists(f),
return(True) (self.rpc_call,) + tuple(getattr(self, 'aliases', '')))[0])
return True
except IndexError:
pass
return False
class Multicall: class Multicall(object):
# noinspection PyUnusedLocal
def __init__(self, class_obj, **kwargs): def __init__(self, class_obj, **kwargs):
self.class_obj = class_obj self.class_obj = class_obj
if class_obj.__class__.__name__ == "RTorrent": if 'RTorrent' == class_obj.__class__.__name__:
self.rt_obj = class_obj self.rt_obj = class_obj
else: else:
# noinspection PyProtectedMember
self.rt_obj = class_obj._rt_obj self.rt_obj = class_obj._rt_obj
self.calls = [] self.calls = []
@ -138,7 +126,8 @@ class Multicall:
if isinstance(method, str): if isinstance(method, str):
result = find_method(method) result = find_method(method)
# if result not found # if result not found
if result == -1: if -1 == result:
# noinspection PyTypeChecker
method = Method(DummyClass, method, method) method = Method(DummyClass, method, method)
else: else:
method = result method = result
@ -159,17 +148,25 @@ class Multicall:
@return: the results (post-processed), in the order they were added @return: the results (post-processed), in the order they were added
@rtype: tuple @rtype: tuple
""" """
m = xmlrpclib.MultiCall(self.rt_obj._get_conn()) xmc = xmlrpclib.MultiCall(self.rt_obj.get_connection())
for call in self.calls: for call in self.calls:
method, args = call method, args = call
rpc_call = getattr(method, "rpc_call") rpc_call = method.rpc_call
getattr(m, rpc_call)(*args) if not self.rt_obj.method_exists(rpc_call):
for alias in getattr(method, 'aliases', None) or []:
if self.rt_obj.method_exists(alias):
rpc_call = alias
break
getattr(xmc, rpc_call)(*args)
try:
results = tuple(filter(lambda x: isinstance(x, list), xmc().results)[0])
except IndexError:
return [[]]
results = m()
results = tuple(results)
results_processed = [] results_processed = []
for r, c in zip(results, self.calls): for r, c in list(zip(results, self.calls)):
method = c[0] # Method instance method = c[0] # Method instance
result = process_result(method, r) result = process_result(method, r)
results_processed.append(result) results_processed.append(result)
@ -178,7 +175,7 @@ class Multicall:
if not exists or not inspect.ismethod(getattr(self.class_obj, method.varname)): if not exists or not inspect.ismethod(getattr(self.class_obj, method.varname)):
setattr(self.class_obj, method.varname, result) setattr(self.class_obj, method.varname, result)
return(tuple(results_processed)) return tuple(results_processed)
def call_method(class_obj, method, *args): def call_method(class_obj, method, *args):
@ -193,51 +190,35 @@ def call_method(class_obj, method, *args):
if method.is_retriever(): if method.is_retriever():
args = args[:-1] args = args[:-1]
else: else:
assert args[-1] is not None, "No argument given." assert args[-1] is not None, 'No argument given.'
if class_obj.__class__.__name__ == "RTorrent": if 'RTorrent' == class_obj.__class__.__name__:
rt_obj = class_obj rt_obj = class_obj
else: else:
# noinspection PyProtectedMember
rt_obj = class_obj._rt_obj rt_obj = class_obj._rt_obj
# check if rpc method is even available # check if rpc method is even available
if not method.is_available(rt_obj): if not method.is_available(rt_obj):
_handle_unavailable_rpc_method(method, rt_obj) _handle_unavailable_rpc_method(method, rt_obj)
m = Multicall(class_obj) mc = Multicall(class_obj)
m.add(method, *args) mc.add(method, *args)
# only added one method, only getting one result back # only added one method, only getting one result back
ret_value = m.call()[0] ret_value = mc.call()[0]
####### OBSOLETE ########################################################## return ret_value
# if method.is_retriever():
# #value = process_result(method, ret_value)
# value = ret_value #MultiCall already processed the result
# else:
# # we're setting the user's input to method.varname
# # but we'll return the value that xmlrpc gives us
# value = process_result(method, args[-1])
##########################################################################
return(ret_value)
def find_method(rpc_call): def find_method(rpc_call):
"""Return L{Method} instance associated with given RPC call""" """Return L{Method} instance associated with given RPC call"""
method_lists = [ try:
rtorrent.methods, rpc_call = rpc_call.lower()
rtorrent.file.methods, return filter(lambda m: rpc_call in map(lambda n: n.lower(), [m.rpc_call] + list(getattr(m, 'aliases', []))),
rtorrent.tracker.methods, rtorrent.methods + rtorrent.torrent.methods +
rtorrent.peer.methods, rtorrent.file.methods + rtorrent.tracker.methods + rtorrent.peer.methods)[0]
rtorrent.torrent.methods, except IndexError:
] return -1
for l in method_lists:
for m in l:
if m.rpc_call.lower() == rpc_call.lower():
return(m)
return(-1)
def process_result(method, result): def process_result(method, result):
@ -249,7 +230,7 @@ def process_result(method, result):
@param result: result to be processed (the result of given L{Method} instance) @param result: result to be processed (the result of given L{Method} instance)
@note: Supported Processing: @note: Supported Processing:
- boolean - convert ones and zeros returned by rTorrent and - bololean - convert ones and zeros returned by rTorrent and
convert to python boolean values convert to python boolean values
""" """
# handle custom post processing function # handle custom post processing function
@ -263,7 +244,7 @@ def process_result(method, result):
elif result in [0, '0']: elif result in [0, '0']:
result = False result = False
return(result) return result
def _build_rpc_methods(class_, method_list): def _build_rpc_methods(class_, method_list):
@ -273,47 +254,26 @@ def _build_rpc_methods(class_, method_list):
instance = class_ instance = class_
class_ = instance.__class__ class_ = instance.__class__
method_store = (instance, class_)[None is instance]
for m in method_list: for m in method_list:
class_name = m.class_name class_name = m.class_name
if class_name != class_.__name__: if class_name != class_.__name__:
continue continue
if class_name == "RTorrent": caller = None
caller = lambda self, arg = None, method = m:\ if 'RTorrent' == class_name:
call_method(self, method, bool_to_int(arg)) caller = (lambda self, arg=None, method=m: call_method(self, method, bool_to_int(arg)))
elif class_name == "Torrent": elif class_name in ['Torrent', 'Tracker', 'File', 'Peer']:
caller = lambda self, arg = None, method = m:\ caller = (lambda self, arg=None, method=m: call_method(self, method, self.rpc_id, bool_to_int(arg)))
call_method(self, method, self.rpc_id, elif 'Group' == class_name:
bool_to_int(arg)) caller = (lambda arg=None, method=m: call_method(instance, method, bool_to_int(arg)))
elif class_name in ["Tracker", "File"]:
caller = lambda self, arg = None, method = m:\ if None is not caller:
call_method(self, method, self.rpc_id, for method_name in [m.method_name] + list(getattr(m, 'aliases', [])):
bool_to_int(arg)) setattr(method_store, method_name, caller)
elif class_name == "Peer": m.docstring = m.docstring or ''
caller = lambda self, arg = None, method = m:\ caller.__doc__ = """{0}
call_method(self, method, self.rpc_id,
bool_to_int(arg)) @note: Variable where the result for this method is stored: {1}.{2}""".format(
m.docstring, class_name, m.varname) # print(m)
elif class_name == "Group":
caller = lambda arg = None, method = m: \
call_method(instance, method, bool_to_int(arg))
if m.docstring is None:
m.docstring = ""
# print(m)
docstring = """{0}
@note: Variable where the result for this method is stored: {1}.{2}""".format(
m.docstring,
class_name,
m.varname)
caller.__doc__ = docstring
for method_name in [m.method_name] + list(m.aliases):
if instance is None:
setattr(class_, method_name, caller)
else:
setattr(instance, method_name, caller)

517
lib/rtorrent/torrent.py

@ -18,40 +18,38 @@
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import rtorrent.rpc from .common import safe_repr
# from rtorrent.rpc import Method
import rtorrent.peer
import rtorrent.tracker
import rtorrent.file
import rtorrent.compat
from rtorrent.common import safe_repr from .file import File, methods as file_methods
from .peer import Peer, methods as peer_methods
from .tracker import Tracker, methods as tracker_methods
from .rpc import Method
import rpc
Peer = rtorrent.peer.Peer
Tracker = rtorrent.tracker.Tracker
File = rtorrent.file.File
Method = rtorrent.rpc.Method
class Torrent(object):
class Torrent:
"""Represents an individual torrent within a L{RTorrent} instance.""" """Represents an individual torrent within a L{RTorrent} instance."""
def __init__(self, _rt_obj, info_hash, **kwargs): def __init__(self, _rt_obj, info_hash, **kwargs):
self._rt_obj = _rt_obj self._rt_obj = _rt_obj
self.info_hash = info_hash # : info hash for the torrent self.info_hash = info_hash # : info hash for the torrent
self.rpc_id = self.info_hash # : unique id to pass to rTorrent self.rpc_id = self.info_hash # : unique id to pass to rTorrent
for k in kwargs.keys(): for k in kwargs:
setattr(self, k, kwargs.get(k, None)) setattr(self, k, kwargs.get(k, None))
self.peers = [] self.peers = []
self.trackers = [] self.trackers = []
self.files = [] self.files = []
self.hashing = None
self.state = None
self.directory = None
self.active = None
self._call_custom_methods() self._call_custom_methods()
def __repr__(self): def __repr__(self):
return safe_repr("Torrent(info_hash=\"{0}\" name=\"{1}\")", return safe_repr('Torrent(info_hash="{0}" name="{1}")',
self.info_hash, self.name) self.info_hash, self.name)
def _call_custom_methods(self): def _call_custom_methods(self):
"""only calls methods that check instance variables.""" """only calls methods that check instance variables."""
@ -68,25 +66,23 @@ class Torrent:
@note: also assigns return value to self.peers @note: also assigns return value to self.peers
""" """
self.peers = [] self.peers = []
retriever_methods = [m for m in rtorrent.peer.methods retriever_methods = filter(lambda m: m.is_retriever() and m.is_available(self._rt_obj), peer_methods)
if m.is_retriever() and m.is_available(self._rt_obj)] mc = rpc.Multicall(self)
# need to leave 2nd arg empty (dunno why) # need to leave 2nd arg empty (dunno why)
m = rtorrent.rpc.Multicall(self) mc.add('p.multicall', self.info_hash, '', *[method.rpc_call + '=' for method in retriever_methods])
m.add("p.multicall", self.info_hash, "",
*[method.rpc_call + "=" for method in retriever_methods])
results = m.call()[0] # only sent one call, only need first result results = mc.call()[0] # only sent one call, only need first result
for result in results: for result in results:
results_dict = {} results_dict = {}
# build results_dict # build results_dict
for m, r in zip(retriever_methods, result): for mc, r in zip(retriever_methods, result):
results_dict[m.varname] = rtorrent.rpc.process_result(m, r) results_dict[mc.varname] = rpc.process_result(mc, r)
self.peers.append(Peer( self.peers.append(Peer(self._rt_obj, self.info_hash, **results_dict))
self._rt_obj, self.info_hash, **results_dict))
return(self.peers) return self.peers
def get_trackers(self): def get_trackers(self):
"""Get list of Tracker instances for given torrent. """Get list of Tracker instances for given torrent.
@ -97,26 +93,23 @@ class Torrent:
@note: also assigns return value to self.trackers @note: also assigns return value to self.trackers
""" """
self.trackers = [] self.trackers = []
retriever_methods = [m for m in rtorrent.tracker.methods retriever_methods = filter(lambda m: m.is_retriever() and m.is_available(self._rt_obj), tracker_methods)
if m.is_retriever() and m.is_available(self._rt_obj)] mc = rpc.Multicall(self)
# need to leave 2nd arg empty (dunno why) # need to leave 2nd arg empty (dunno why)
m = rtorrent.rpc.Multicall(self) mc.add('t.multicall', self.info_hash, '', *[method.rpc_call + '=' for method in retriever_methods])
m.add("t.multicall", self.info_hash, "",
*[method.rpc_call + "=" for method in retriever_methods])
results = m.call()[0] # only sent one call, only need first result results = mc.call()[0] # only sent one call, only need first result
for result in results: for result in results:
results_dict = {} results_dict = {}
# build results_dict # build results_dict
for m, r in zip(retriever_methods, result): for mc, r in zip(retriever_methods, result):
results_dict[m.varname] = rtorrent.rpc.process_result(m, r) results_dict[mc.varname] = rpc.process_result(mc, r)
self.trackers.append(Tracker( self.trackers.append(Tracker(self._rt_obj, self.info_hash, **results_dict))
self._rt_obj, self.info_hash, **results_dict))
return(self.trackers) return self.trackers
def get_files(self): def get_files(self):
"""Get list of File instances for given torrent. """Get list of File instances for given torrent.
@ -128,18 +121,15 @@ class Torrent:
""" """
self.files = [] self.files = []
retriever_methods = [m for m in rtorrent.file.methods retriever_methods = filter(lambda m: m.is_retriever() and m.is_available(self._rt_obj), file_methods)
if m.is_retriever() and m.is_available(self._rt_obj)] mc = rpc.Multicall(self)
# 2nd arg can be anything, but it'll return all files in torrent # 2nd arg can be anything, but it'll return all files in torrent
# regardless mc.add('f.multicall', self.info_hash, '', *[method.rpc_call + '=' for method in retriever_methods])
m = rtorrent.rpc.Multicall(self)
m.add("f.multicall", self.info_hash, "",
*[method.rpc_call + "=" for method in retriever_methods])
results = m.call()[0] # only sent one call, only need first result results = mc.call()[0] # only sent one call, only need first result
offset_method_index = retriever_methods.index( offset_method_index = retriever_methods.index(rpc.find_method('f.get_offset'))
rtorrent.rpc.find_method("f.get_offset"))
# make a list of the offsets of all the files, sort appropriately # make a list of the offsets of all the files, sort appropriately
offset_list = sorted([r[offset_method_index] for r in results]) offset_list = sorted([r[offset_method_index] for r in results])
@ -147,17 +137,33 @@ class Torrent:
for result in results: for result in results:
results_dict = {} results_dict = {}
# build results_dict # build results_dict
for m, r in zip(retriever_methods, result): for mc, r in zip(retriever_methods, result):
results_dict[m.varname] = rtorrent.rpc.process_result(m, r) results_dict[mc.varname] = rpc.process_result(mc, r)
# get proper index positions for each file (based on the file # get proper index positions for each file (based on the file
# offset) # offset)
f_index = offset_list.index(results_dict["offset"]) f_index = offset_list.index(results_dict['offset'])
self.files.append(File(self._rt_obj, self.info_hash, f_index, **results_dict))
return self.files
def _get_method(self, *choices):
try:
return filter(lambda method: self._rt_obj.method_exists(method), choices)[0]
except(Exception, BaseException):
pass
self.files.append(File(self._rt_obj, self.info_hash, def get_state(self):
f_index, **results_dict))
return(self.files) method = self._get_method(*('d.get_state', 'd.state'))
if method:
mc = rpc.Multicall(self)
self.multicall_add(mc, method)
return mc.call()[-1]
def set_directory(self, d): def set_directory(self, d):
"""Modify download directory """Modify download directory
@ -166,11 +172,14 @@ class Torrent:
Also doesn't restart after directory is set, that must be called Also doesn't restart after directory is set, that must be called
separately. separately.
""" """
m = rtorrent.rpc.Multicall(self) method = self._get_method(*('d.set_directory', 'd.directory.set'))
self.multicall_add(m, "d.try_stop") if method:
self.multicall_add(m, "d.set_directory", d) mc = rpc.Multicall(self)
self.multicall_add(mc, 'd.try_stop')
self.multicall_add(mc, method, d)
self.directory = m.call()[-1] self.directory = mc.call()[-1]
def set_directory_base(self, d): def set_directory_base(self, d):
"""Modify base download directory """Modify base download directory
@ -179,64 +188,74 @@ class Torrent:
Also doesn't restart after directory is set, that must be called Also doesn't restart after directory is set, that must be called
separately. separately.
""" """
m = rtorrent.rpc.Multicall(self) method = self._get_method(*('d.set_directory_base', 'd.directory_base.set'))
self.multicall_add(m, "d.try_stop") if method:
self.multicall_add(m, "d.set_directory_base", d) mc = rpc.Multicall(self)
self.multicall_add(mc, 'd.try_stop')
self.multicall_add(mc, method, d)
def start(self): def start(self):
"""Start the torrent""" """Start the torrent"""
m = rtorrent.rpc.Multicall(self) mc = rpc.Multicall(self)
self.multicall_add(m, "d.try_start")
self.multicall_add(m, "d.is_active") self.multicall_add(mc, 'd.try_start')
self.multicall_add(mc, 'd.is_active')
self.active = m.call()[-1] self.active = mc.call()[-1]
return(self.active) return self.active
def stop(self): def stop(self):
""""Stop the torrent""" """"Stop the torrent"""
m = rtorrent.rpc.Multicall(self) mc = rpc.Multicall(self)
self.multicall_add(m, "d.try_stop")
self.multicall_add(m, "d.is_active") self.multicall_add(mc, 'd.try_stop')
self.multicall_add(mc, 'd.is_active')
self.active = m.call()[-1] self.active = mc.call()[-1]
return(self.active) return self.active
def pause(self): def pause(self):
"""Pause the torrent""" """Pause the torrent"""
m = rtorrent.rpc.Multicall(self) mc = rpc.Multicall(self)
self.multicall_add(m, "d.pause")
return(m.call()[-1]) self.multicall_add(mc, 'd.pause')
return mc.call()[-1]
def resume(self): def resume(self):
"""Resume the torrent""" """Resume the torrent"""
m = rtorrent.rpc.Multicall(self) mc = rpc.Multicall(self)
self.multicall_add(m, "d.resume")
self.multicall_add(mc, 'd.resume')
return(m.call()[-1]) return mc.call()[-1]
def close(self): def close(self):
"""Close the torrent and it's files""" """Close the torrent and it's files"""
m = rtorrent.rpc.Multicall(self) mc = rpc.Multicall(self)
self.multicall_add(m, "d.close")
self.multicall_add(mc, 'd.close')
return(m.call()[-1]) return mc.call()[-1]
def erase(self): def erase(self):
"""Delete the torrent """Delete the torrent
@note: doesn't delete the downloaded files""" @note: doesn't delete the downloaded files"""
m = rtorrent.rpc.Multicall(self) mc = rpc.Multicall(self)
self.multicall_add(m, "d.erase")
return(m.call()[-1]) self.multicall_add(mc, 'd.erase')
return mc.call()[-1]
def check_hash(self): def check_hash(self):
"""(Re)hash check the torrent""" """(Re)hash check the torrent"""
m = rtorrent.rpc.Multicall(self) mc = rpc.Multicall(self)
self.multicall_add(m, "d.check_hash")
self.multicall_add(mc, 'd.check_hash')
return(m.call()[-1]) return mc.call()[-1]
def poll(self): def poll(self):
"""poll rTorrent to get latest peer/tracker/file information""" """poll rTorrent to get latest peer/tracker/file information"""
@ -251,13 +270,12 @@ class Torrent:
@return: None @return: None
""" """
multicall = rtorrent.rpc.Multicall(self) mc = rpc.Multicall(self)
retriever_methods = [m for m in methods
if m.is_retriever() and m.is_available(self._rt_obj)] for method in filter(lambda m: m.is_retriever() and m.is_available(self._rt_obj), methods):
for method in retriever_methods: mc.add(method, self.rpc_id)
multicall.add(method, self.rpc_id)
multicall.call() mc.call()
# custom functions (only call private methods, since they only check # custom functions (only call private methods, since they only check
# local variables and are therefore faster) # local variables and are therefore faster)
@ -268,27 +286,23 @@ class Torrent:
@param accept_seeds: enable/disable accepting seeders @param accept_seeds: enable/disable accepting seeders
@type accept_seeds: bool""" @type accept_seeds: bool"""
if accept_seeds: mc = rpc.Multicall(self)
call = "d.accepting_seeders.enable"
else:
call = "d.accepting_seeders.disable"
m = rtorrent.rpc.Multicall(self) self.multicall_add(mc, ('d.accepting_seeders.disable', 'd.accepting_seeders.enable')[accept_seeds])
self.multicall_add(m, call)
return(m.call()[-1]) return mc.call()[-1]
def announce(self): def announce(self):
"""Announce torrent info to tracker(s)""" """Announce torrent info to tracker(s)"""
m = rtorrent.rpc.Multicall(self) mc = rpc.Multicall(self)
self.multicall_add(m, "d.tracker_announce")
return(m.call()[-1]) self.multicall_add(mc, 'd.tracker_announce')
return mc.call()[-1]
@staticmethod @staticmethod
def _assert_custom_key_valid(key): def _assert_custom_key_valid(key):
assert type(key) == int and key > 0 and key < 6, \ assert type(key) == int and 0 < key < 6, 'key must be an integer between 1-5'
"key must be an integer between 1-5"
def get_custom(self, key): def get_custom(self, key):
""" """
@ -301,13 +315,16 @@ class Torrent:
""" """
self._assert_custom_key_valid(key) self._assert_custom_key_valid(key)
m = rtorrent.rpc.Multicall(self)
field = "custom{0}".format(key) field = 'custom%s' % key
self.multicall_add(m, "d.get_{0}".format(field)) method = self._get_method(*('d.get_%s' % field, 'd.%s' % field))
setattr(self, field, m.call()[-1]) if method:
mc = rpc.Multicall(self)
self.multicall_add(mc, method)
return (getattr(self, field)) setattr(self, field, mc.call()[-1])
return getattr(self, field)
def set_custom(self, key, value): def set_custom(self, key, value):
""" """
@ -324,19 +341,23 @@ class Torrent:
""" """
self._assert_custom_key_valid(key) self._assert_custom_key_valid(key)
m = rtorrent.rpc.Multicall(self)
self.multicall_add(m, "d.set_custom{0}".format(key), value) field = 'custom%s' % key
method = self._get_method(*('d.set_%s' % field, 'd.%s.set' % field))
if method:
mc = rpc.Multicall(self)
self.multicall_add(mc, method, value)
return(m.call()[-1]) return mc.call()[-1]
def set_visible(self, view, visible=True): def set_visible(self, view, visible=True):
p = self._rt_obj._get_conn() p = self._rt_obj.get_connection()
if visible: if visible:
return p.view.set_visible(self.info_hash, view) return p.view.set_visible(self.info_hash, view)
else:
return p.view.set_not_visible(self.info_hash, view) return p.view.set_not_visible(self.info_hash, view)
def add_tracker(self, group, tracker): def add_tracker(self, group, tracker):
""" """
@ -351,10 +372,11 @@ class Torrent:
@return: if successful, 0 @return: if successful, 0
@rtype: int @rtype: int
""" """
m = rtorrent.rpc.Multicall(self) mc = rpc.Multicall(self)
self.multicall_add(m, "d.tracker.insert", group, tracker)
self.multicall_add(mc, 'd.tracker.insert', group, tracker)
return (m.call()[-1]) return mc.call()[-1]
############################################################################ ############################################################################
# CUSTOM METHODS (Not part of the official rTorrent API) # CUSTOM METHODS (Not part of the official rTorrent API)
@ -366,45 +388,48 @@ class Torrent:
self.hash_checking_queued = (self.hashing == 3 and self.hash_checking_queued = (self.hashing == 3 and
self.hash_checking is False) self.hash_checking is False)
return(self.hash_checking_queued) return self.hash_checking_queued
def is_hash_checking_queued(self): def is_hash_checking_queued(self):
"""Check if torrent is waiting to be hash checked """Check if torrent is waiting to be hash checked
@note: Variable where the result for this method is stored Torrent.hash_checking_queued""" @note: Variable where the result for this method is stored Torrent.hash_checking_queued"""
m = rtorrent.rpc.Multicall(self)
self.multicall_add(m, "d.get_hashing")
self.multicall_add(m, "d.is_hash_checking")
results = m.call()
setattr(self, "hashing", results[0]) method = self._get_method(*('d.get_hashing', 'd.hashing'))
setattr(self, "hash_checking", results[1]) if method:
mc = rpc.Multicall(self)
self.multicall_add(mc, method)
self.multicall_add(mc, 'd.is_hash_checking')
results = mc.call()
return(self._is_hash_checking_queued()) setattr(self, 'hashing', results[0])
setattr(self, 'hash_checking', results[1])
return self._is_hash_checking_queued()
def _is_paused(self): def _is_paused(self):
"""Only checks instance variables, shouldn't be called directly""" """Only checks instance variables, shouldn't be called directly"""
self.paused = (self.state == 0) self.paused = (self.state == 0)
return(self.paused) return self.paused
def is_paused(self): def is_paused(self):
"""Check if torrent is paused """Check if torrent is paused
@note: Variable where the result for this method is stored: Torrent.paused""" @note: Variable where the result for this method is stored: Torrent.paused"""
self.get_state() self.get_state()
return(self._is_paused()) return self._is_paused()
def _is_started(self): def _is_started(self):
"""Only checks instance variables, shouldn't be called directly""" """Only checks instance variables, shouldn't be called directly"""
self.started = (self.state == 1) self.started = (self.state == 1)
return(self.started) return self.started
def is_started(self): def is_started(self):
"""Check if torrent is started """Check if torrent is started
@note: Variable where the result for this method is stored: Torrent.started""" @note: Variable where the result for this method is stored: Torrent.started"""
self.get_state() self.get_state()
return(self._is_started()) return self._is_started()
methods = [ methods = [
@ -415,121 +440,197 @@ methods = [
Method(Torrent, 'is_hash_checking', 'd.is_hash_checking', Method(Torrent, 'is_hash_checking', 'd.is_hash_checking',
boolean=True, boolean=True,
), ),
Method(Torrent, 'get_peers_max', 'd.get_peers_max'), Method(Torrent, 'get_peers_max', 'd.get_peers_max',
Method(Torrent, 'get_tracker_focus', 'd.get_tracker_focus'), aliases=('d.peers_max',)),
Method(Torrent, 'get_skip_total', 'd.get_skip_total'), Method(Torrent, 'get_tracker_focus', 'd.get_tracker_focus',
Method(Torrent, 'get_state', 'd.get_state'), aliases=('d.tracker_focus',)),
Method(Torrent, 'get_peer_exchange', 'd.get_peer_exchange'), Method(Torrent, 'get_skip_total', 'd.get_skip_total',
Method(Torrent, 'get_down_rate', 'd.get_down_rate'), aliases=('d.skip.total',)),
Method(Torrent, 'get_connection_seed', 'd.get_connection_seed'), Method(Torrent, 'get_state', 'd.get_state',
Method(Torrent, 'get_uploads_max', 'd.get_uploads_max'), aliases=('d.state',)),
Method(Torrent, 'get_priority_str', 'd.get_priority_str'), Method(Torrent, 'get_peer_exchange', 'd.get_peer_exchange',
aliases=('d.peer_exchange',)),
Method(Torrent, 'get_down_rate', 'd.get_down_rate',
aliases=('d.down.rate',)),
Method(Torrent, 'get_connection_seed', 'd.get_connection_seed',
aliases=('d.connection_seed',)),
Method(Torrent, 'get_uploads_max', 'd.get_uploads_max',
aliases=('d.uploads_max',)),
Method(Torrent, 'get_priority_str', 'd.get_priority_str',
aliases=('d.priority_str',)),
Method(Torrent, 'is_open', 'd.is_open', Method(Torrent, 'is_open', 'd.is_open',
boolean=True, boolean=True,
), ),
Method(Torrent, 'get_peers_min', 'd.get_peers_min'), Method(Torrent, 'get_peers_min', 'd.get_peers_min',
Method(Torrent, 'get_peers_complete', 'd.get_peers_complete'), aliases=('d.peers_min',)),
Method(Torrent, 'get_tracker_numwant', 'd.get_tracker_numwant'), Method(Torrent, 'get_peers_complete', 'd.get_peers_complete',
Method(Torrent, 'get_connection_current', 'd.get_connection_current'), aliases=('d.peers_complete',)),
Method(Torrent, 'get_tracker_numwant', 'd.get_tracker_numwant',
aliases=('d.tracker_numwant',)),
Method(Torrent, 'get_connection_current', 'd.get_connection_current',
aliases=('d.connection_current',)),
Method(Torrent, 'is_complete', 'd.get_complete', Method(Torrent, 'is_complete', 'd.get_complete',
boolean=True, boolean=True,
aliases=('d.complete',)
), ),
Method(Torrent, 'get_peers_connected', 'd.get_peers_connected'), Method(Torrent, 'get_peers_connected', 'd.get_peers_connected',
Method(Torrent, 'get_chunk_size', 'd.get_chunk_size'), aliases=('d.peers_connected',)),
Method(Torrent, 'get_state_counter', 'd.get_state_counter'), Method(Torrent, 'get_chunk_size', 'd.get_chunk_size',
Method(Torrent, 'get_base_filename', 'd.get_base_filename'), aliases=('d.chunk_size',)),
Method(Torrent, 'get_state_changed', 'd.get_state_changed'), Method(Torrent, 'get_state_counter', 'd.get_state_counter',
Method(Torrent, 'get_peers_not_connected', 'd.get_peers_not_connected'), aliases=('d.state_counter',)),
Method(Torrent, 'get_directory', 'd.get_directory'), Method(Torrent, 'get_base_filename', 'd.get_base_filename',
aliases=('d.base_filename',)),
Method(Torrent, 'get_state_changed', 'd.get_state_changed',
aliases=('d.state_changed',)),
Method(Torrent, 'get_peers_not_connected', 'd.get_peers_not_connected',
aliases=('d.peers_not_connected',)),
Method(Torrent, 'get_directory', 'd.get_directory',
aliases=('d.directory',)),
Method(Torrent, 'is_incomplete', 'd.incomplete', Method(Torrent, 'is_incomplete', 'd.incomplete',
boolean=True, boolean=True,
), ),
Method(Torrent, 'get_tracker_size', 'd.get_tracker_size'), Method(Torrent, 'get_tracker_size', 'd.get_tracker_size',
aliases=('d.tracker_size',)),
Method(Torrent, 'is_multi_file', 'd.is_multi_file', Method(Torrent, 'is_multi_file', 'd.is_multi_file',
boolean=True, boolean=True,
), ),
Method(Torrent, 'get_local_id', 'd.get_local_id'), Method(Torrent, 'get_local_id', 'd.get_local_id',
aliases=('d.local_id',)),
Method(Torrent, 'get_ratio', 'd.get_ratio', Method(Torrent, 'get_ratio', 'd.get_ratio',
post_process_func=lambda x: x / 1000.0, post_process_func=lambda x: x / 1000.0,
aliases=('d.ratio',)
), ),
Method(Torrent, 'get_loaded_file', 'd.get_loaded_file'), Method(Torrent, 'get_loaded_file', 'd.get_loaded_file',
Method(Torrent, 'get_max_file_size', 'd.get_max_file_size'), aliases=('d.loaded_file',)),
Method(Torrent, 'get_size_chunks', 'd.get_size_chunks'), Method(Torrent, 'get_max_file_size', 'd.get_max_file_size',
aliases=('d.max_file_size',)),
Method(Torrent, 'get_size_chunks', 'd.get_size_chunks',
aliases=('d.size_chunks',)),
Method(Torrent, 'is_pex_active', 'd.is_pex_active', Method(Torrent, 'is_pex_active', 'd.is_pex_active',
boolean=True, boolean=True,
), ),
Method(Torrent, 'get_hashing', 'd.get_hashing'), Method(Torrent, 'get_hashing', 'd.get_hashing',
Method(Torrent, 'get_bitfield', 'd.get_bitfield'), aliases=('d.hashing',)),
Method(Torrent, 'get_local_id_html', 'd.get_local_id_html'), Method(Torrent, 'get_bitfield', 'd.get_bitfield',
Method(Torrent, 'get_connection_leech', 'd.get_connection_leech'), aliases=('d.bitfield',)),
Method(Torrent, 'get_peers_accounted', 'd.get_peers_accounted'), Method(Torrent, 'get_local_id_html', 'd.get_local_id_html',
Method(Torrent, 'get_message', 'd.get_message'), aliases=('d.bitfield',)),
Method(Torrent, 'get_connection_leech', 'd.get_connection_leech',
aliases=('d.connection_leech',)),
Method(Torrent, 'get_peers_accounted', 'd.get_peers_accounted',
aliases=('d.peers_accounted',)),
Method(Torrent, 'get_message', 'd.get_message',
aliases=('d.message',)),
Method(Torrent, 'is_active', 'd.is_active', Method(Torrent, 'is_active', 'd.is_active',
boolean=True, boolean=True,
), ),
Method(Torrent, 'get_size_bytes', 'd.get_size_bytes'), Method(Torrent, 'get_size_bytes', 'd.get_size_bytes',
Method(Torrent, 'get_ignore_commands', 'd.get_ignore_commands'), aliases=('d.size_bytes',)),
Method(Torrent, 'get_creation_date', 'd.get_creation_date'), Method(Torrent, 'get_ignore_commands', 'd.get_ignore_commands',
Method(Torrent, 'get_base_path', 'd.get_base_path'), aliases=('d.ignore_commands',)),
Method(Torrent, 'get_left_bytes', 'd.get_left_bytes'), Method(Torrent, 'get_creation_date', 'd.get_creation_date',
Method(Torrent, 'get_size_files', 'd.get_size_files'), aliases=('d.creation_date',)),
Method(Torrent, 'get_size_pex', 'd.get_size_pex'), Method(Torrent, 'get_base_path', 'd.get_base_path',
aliases=('d.base_path',)),
Method(Torrent, 'get_left_bytes', 'd.get_left_bytes',
aliases=('d.left_bytes',)),
Method(Torrent, 'get_size_files', 'd.get_size_files',
aliases=('d.size_files',)),
Method(Torrent, 'get_size_pex', 'd.get_size_pex',
aliases=('d.size_pex',)),
Method(Torrent, 'is_private', 'd.is_private', Method(Torrent, 'is_private', 'd.is_private',
boolean=True, boolean=True,
), ),
Method(Torrent, 'get_max_size_pex', 'd.get_max_size_pex'), Method(Torrent, 'get_max_size_pex', 'd.get_max_size_pex'),
Method(Torrent, 'get_num_chunks_hashed', 'd.get_chunks_hashed', Method(Torrent, 'get_num_chunks_hashed', 'd.get_chunks_hashed',
aliases=("get_chunks_hashed",)), aliases=('get_chunks_hashed', 'd.chunks_hashed')),
Method(Torrent, 'get_num_chunks_wanted', 'd.wanted_chunks'), Method(Torrent, 'get_num_chunks_wanted', 'd.wanted_chunks'),
Method(Torrent, 'get_priority', 'd.get_priority'), Method(Torrent, 'get_priority', 'd.get_priority',
Method(Torrent, 'get_skip_rate', 'd.get_skip_rate'), aliases=('d.priority',)),
Method(Torrent, 'get_completed_bytes', 'd.get_completed_bytes'), Method(Torrent, 'get_skip_rate', 'd.get_skip_rate',
Method(Torrent, 'get_name', 'd.get_name'), aliases=('d.skip.rate',)),
Method(Torrent, 'get_completed_chunks', 'd.get_completed_chunks'), Method(Torrent, 'get_completed_bytes', 'd.get_completed_bytes',
Method(Torrent, 'get_throttle_name', 'd.get_throttle_name'), aliases=('d.completed_bytes',)),
Method(Torrent, 'get_free_diskspace', 'd.get_free_diskspace'), Method(Torrent, 'get_name', 'd.get_name',
Method(Torrent, 'get_directory_base', 'd.get_directory_base'), aliases=('d.name',)),
Method(Torrent, 'get_hashing_failed', 'd.get_hashing_failed'), Method(Torrent, 'get_completed_chunks', 'd.get_completed_chunks',
Method(Torrent, 'get_tied_to_file', 'd.get_tied_to_file'), aliases=('d.completed_chunks',)),
Method(Torrent, 'get_down_total', 'd.get_down_total'), Method(Torrent, 'get_throttle_name', 'd.get_throttle_name',
Method(Torrent, 'get_bytes_done', 'd.get_bytes_done'), aliases=('d.throttle_name',)),
Method(Torrent, 'get_up_rate', 'd.get_up_rate'), Method(Torrent, 'get_free_diskspace', 'd.get_free_diskspace',
Method(Torrent, 'get_up_total', 'd.get_up_total'), aliases=('d.free_diskspace',)),
Method(Torrent, 'get_directory_base', 'd.get_directory_base',
aliases=('d.directory_base',)),
Method(Torrent, 'get_hashing_failed', 'd.get_hashing_failed',
aliases=('d.hashing_failed',)),
Method(Torrent, 'get_tied_to_file', 'd.get_tied_to_file',
aliases=('d.tied_to_file',)),
Method(Torrent, 'get_down_total', 'd.get_down_total',
aliases=('d.down.total',)),
Method(Torrent, 'get_bytes_done', 'd.get_bytes_done',
aliases=('d.bytes_done',)),
Method(Torrent, 'get_up_rate', 'd.get_up_rate',
aliases=('d.up.rate',)),
Method(Torrent, 'get_up_total', 'd.get_up_total',
aliases=('d.up.total',)),
Method(Torrent, 'is_accepting_seeders', 'd.accepting_seeders', Method(Torrent, 'is_accepting_seeders', 'd.accepting_seeders',
boolean=True, boolean=True,
), ),
Method(Torrent, "get_chunks_seen", "d.chunks_seen", Method(Torrent, 'get_chunks_seen', 'd.chunks_seen',
min_version=(0, 9, 1), min_version=(0, 9, 1),
), ),
Method(Torrent, "is_partially_done", "d.is_partially_done", Method(Torrent, 'is_partially_done', 'd.is_partially_done',
boolean=True, boolean=True,
), ),
Method(Torrent, "is_not_partially_done", "d.is_not_partially_done", Method(Torrent, 'is_not_partially_done', 'd.is_not_partially_done',
boolean=True, boolean=True,
), ),
Method(Torrent, "get_time_started", "d.timestamp.started"), Method(Torrent, 'get_time_started', 'd.timestamp.started'),
Method(Torrent, "get_custom1", "d.get_custom1"), Method(Torrent, 'get_custom1', 'd.get_custom1',
Method(Torrent, "get_custom2", "d.get_custom2"), aliases=('d.custom1',)),
Method(Torrent, "get_custom3", "d.get_custom3"), Method(Torrent, 'get_custom2', 'd.get_custom2',
Method(Torrent, "get_custom4", "d.get_custom4"), aliases=('d.custom2',)),
Method(Torrent, "get_custom5", "d.get_custom5"), Method(Torrent, 'get_custom3', 'd.get_custom3',
aliases=('d.custom3',)),
Method(Torrent, 'get_custom4', 'd.get_custom4',
aliases=('d.custom4',)),
Method(Torrent, 'get_custom5', 'd.get_custom5',
aliases=('d.custom5',)),
# MODIFIERS # MODIFIERS
Method(Torrent, 'set_uploads_max', 'd.set_uploads_max'), Method(Torrent, 'set_uploads_max', 'd.set_uploads_max',
Method(Torrent, 'set_tied_to_file', 'd.set_tied_to_file'), aliases=('d.uploads_max.set',)),
Method(Torrent, 'set_tracker_numwant', 'd.set_tracker_numwant'), Method(Torrent, 'set_tied_to_file', 'd.set_tied_to_file',
Method(Torrent, 'set_priority', 'd.set_priority'), aliases=('d.tied_to_file.set',)),
Method(Torrent, 'set_peers_max', 'd.set_peers_max'), Method(Torrent, 'set_tracker_numwant', 'd.set_tracker_numwant',
Method(Torrent, 'set_hashing_failed', 'd.set_hashing_failed'), aliases=('d.tracker_numwant.set',)),
Method(Torrent, 'set_message', 'd.set_message'), Method(Torrent, 'set_priority', 'd.set_priority',
Method(Torrent, 'set_throttle_name', 'd.set_throttle_name'), aliases=('d.priority.set',)),
Method(Torrent, 'set_peers_min', 'd.set_peers_min'), Method(Torrent, 'set_peers_max', 'd.set_peers_max',
Method(Torrent, 'set_ignore_commands', 'd.set_ignore_commands'), aliases=('d.peers_max.set',)),
Method(Torrent, 'set_max_file_size', 'd.set_max_file_size'), Method(Torrent, 'set_hashing_failed', 'd.set_hashing_failed',
Method(Torrent, 'set_custom5', 'd.set_custom5'), aliases=('d.hashing_failed.set',)),
Method(Torrent, 'set_custom4', 'd.set_custom4'), Method(Torrent, 'set_message', 'd.set_message',
Method(Torrent, 'set_custom2', 'd.set_custom2'), aliases=('d.message.set',)),
Method(Torrent, 'set_custom1', 'd.set_custom1'), Method(Torrent, 'set_throttle_name', 'd.set_throttle_name',
Method(Torrent, 'set_custom3', 'd.set_custom3'), aliases=('d.throttle_name.set',)),
Method(Torrent, 'set_connection_current', 'd.set_connection_current'), Method(Torrent, 'set_peers_min', 'd.set_peers_min',
aliases=('d.peers_min.set',)),
Method(Torrent, 'set_ignore_commands', 'd.set_ignore_commands',
aliases=('d.ignore_commands.set',)),
Method(Torrent, 'set_max_file_size', 'd.set_max_file_size',
aliases=('d.max_file_size.set',)),
Method(Torrent, 'set_custom5', 'd.set_custom5',
aliases=('d.custom5.set',)),
Method(Torrent, 'set_custom4', 'd.set_custom4',
aliases=('d.custom4.set',)),
Method(Torrent, 'set_custom2', 'd.set_custom2',
aliases=('d.custom2.set',)),
Method(Torrent, 'set_custom1', 'd.set_custom1',
aliases=('d.custom1.set',)),
Method(Torrent, 'set_custom3', 'd.set_custom3',
aliases=('d.custom3.set',)),
Method(Torrent, 'set_connection_current', 'd.set_connection_current',
aliases=('d.connection_current.set',)),
] ]

77
lib/rtorrent/tracker.py

@ -18,39 +18,36 @@
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# from rtorrent.rpc import Method from .common import safe_repr
import rtorrent.rpc from .rpc import Method
import rpc
from rtorrent.common import safe_repr
Method = rtorrent.rpc.Method class Tracker(object):
class Tracker:
"""Represents an individual tracker within a L{Torrent} instance.""" """Represents an individual tracker within a L{Torrent} instance."""
def __init__(self, _rt_obj, info_hash, **kwargs): def __init__(self, _rt_obj, info_hash, **kwargs):
self._rt_obj = _rt_obj self._rt_obj = _rt_obj
self.info_hash = info_hash # : info hash for the torrent using this tracker self.info_hash = info_hash # : info hash for the torrent using this tracker
for k in kwargs.keys(): for k in kwargs:
setattr(self, k, kwargs.get(k, None)) setattr(self, k, kwargs.get(k, None))
# for clarity's sake... # for clarity's sake...
self.index = self.group # : position of tracker within the torrent's tracker list self.index = self.group # : position of tracker within the torrent's tracker list
self.rpc_id = "{0}:t{1}".format( self.rpc_id = '{0}:t{1}'.format(
self.info_hash, self.index) # : unique id to pass to rTorrent self.info_hash, self.index) # : unique id to pass to rTorrent
def __repr__(self): def __repr__(self):
return safe_repr("Tracker(index={0}, url=\"{1}\")", return safe_repr('Tracker(index={0}, url="{1}")',
self.index, self.url) self.index, self.url)
def enable(self): def enable(self):
"""Alias for set_enabled("yes")""" """Alias for set_enabled("yes")"""
self.set_enabled("yes") self.set_enabled('yes')
def disable(self): def disable(self):
"""Alias for set_enabled("no")""" """Alias for set_enabled("no")"""
self.set_enabled("no") self.set_enabled('no')
def update(self): def update(self):
"""Refresh tracker data """Refresh tracker data
@ -59,13 +56,12 @@ class Tracker:
@return: None @return: None
""" """
multicall = rtorrent.rpc.Multicall(self) mc = rpc.Multicall(self)
retriever_methods = [m for m in methods
if m.is_retriever() and m.is_available(self._rt_obj)] for method in filter(lambda fx: fx.is_retriever() and fx.is_available(self._rt_obj), methods):
for method in retriever_methods: mc.add(method, self.rpc_id)
multicall.add(method, self.rpc_id)
multicall.call() mc.call()
def append_tracker(self, tracker): def append_tracker(self, tracker):
""" """
@ -77,26 +73,38 @@ class Tracker:
@return: if successful, 0 @return: if successful, 0
@rtype: int @rtype: int
""" """
m = rtorrent.rpc.Multicall(self) mc = rpc.Multicall(self)
self.multicall_add(m, "d.tracker.insert", self.index, tracker)
self.multicall_add(mc, 'd.tracker.insert', self.index, tracker)
return mc.call()[-1]
return (m.call()[-1])
methods = [ methods = [
# RETRIEVERS # RETRIEVERS
Method(Tracker, 'is_enabled', 't.is_enabled', boolean=True), Method(Tracker, 'is_enabled', 't.is_enabled', boolean=True),
Method(Tracker, 'get_id', 't.get_id'), Method(Tracker, 'get_id', 't.get_id',
Method(Tracker, 'get_scrape_incomplete', 't.get_scrape_incomplete'), aliases=('t.id',)),
Method(Tracker, 'get_scrape_incomplete', 't.get_scrape_incomplete',
aliases=('t.scrape_incomplete',)),
Method(Tracker, 'is_open', 't.is_open', boolean=True), Method(Tracker, 'is_open', 't.is_open', boolean=True),
Method(Tracker, 'get_min_interval', 't.get_min_interval'), Method(Tracker, 'get_min_interval', 't.get_min_interval',
Method(Tracker, 'get_scrape_downloaded', 't.get_scrape_downloaded'), aliases=('t.min_interval',)),
Method(Tracker, 'get_group', 't.get_group'), Method(Tracker, 'get_scrape_downloaded', 't.get_scrape_downloaded',
Method(Tracker, 'get_scrape_time_last', 't.get_scrape_time_last'), aliases=('t.scrape_downloaded',)),
Method(Tracker, 'get_type', 't.get_type'), Method(Tracker, 'get_group', 't.get_group',
Method(Tracker, 'get_normal_interval', 't.get_normal_interval'), aliases=('t.group',)),
Method(Tracker, 'get_url', 't.get_url'), Method(Tracker, 'get_scrape_time_last', 't.get_scrape_time_last',
aliases=('t.scrape_time_last',)),
Method(Tracker, 'get_type', 't.get_type',
aliases=('t.type',)),
Method(Tracker, 'get_normal_interval', 't.get_normal_interval',
aliases=('t.normal_interval',)),
Method(Tracker, 'get_url', 't.get_url',
aliases=('t.url',)),
Method(Tracker, 'get_scrape_complete', 't.get_scrape_complete', Method(Tracker, 'get_scrape_complete', 't.get_scrape_complete',
min_version=(0, 8, 9), min_version=(0, 8, 9),
aliases=('t.scrape_complete',)
), ),
Method(Tracker, 'get_activity_time_last', 't.activity_time_last', Method(Tracker, 'get_activity_time_last', 't.activity_time_last',
min_version=(0, 8, 9), min_version=(0, 8, 9),
@ -141,13 +149,14 @@ methods = [
min_version=(0, 9, 1), min_version=(0, 9, 1),
boolean=True, boolean=True,
), ),
Method(Tracker, "get_latest_sum_peers", "t.latest_sum_peers", Method(Tracker, 'get_latest_sum_peers', 't.latest_sum_peers',
min_version=(0, 9, 0) min_version=(0, 9, 0)
), ),
Method(Tracker, "get_latest_new_peers", "t.latest_new_peers", Method(Tracker, 'get_latest_new_peers', 't.latest_new_peers',
min_version=(0, 9, 0) min_version=(0, 9, 0)
), ),
# MODIFIERS # MODIFIERS
Method(Tracker, 'set_enabled', 't.set_enabled'), Method(Tracker, 'set_enabled', 't.set_enabled',
aliases=('t.is_enabled.set',)),
] ]

2
sickbeard/clients/generic.py

@ -152,7 +152,7 @@ class GenericClient(object):
def _get_torrent_hash(result): def _get_torrent_hash(result):
if result.url.startswith('magnet'): if result.url.startswith('magnet'):
result.hash = re.findall('urn:btih:([\w]{32,40})', result.url)[0] result.hash = re.findall(r'urn:btih:([\w]{32,40})', result.url)[0]
if 32 == len(result.hash): if 32 == len(result.hash):
result.hash = b16encode(b32decode(result.hash)).lower() result.hash = b16encode(b32decode(result.hash)).lower()
else: else:

63
sickbeard/clients/rtorrent.py

@ -1,5 +1,3 @@
# Author: jkaberg <joel.kaberg@gmail.com>, based on fuzemans work (https://github.com/RuudBurger/CouchPotatoServer/blob/develop/couchpotato/core/downloaders/rtorrent/main.py)
# URL: http://code.google.com/p/sickbeard/
# #
# This file is part of SickGear. # This file is part of SickGear.
# #
@ -16,11 +14,11 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with SickGear. If not, see <http://www.gnu.org/licenses/>. # along with SickGear. If not, see <http://www.gnu.org/licenses/>.
import xmlrpclib from lib.rtorrent.compat import xmlrpclib
import sickbeard
from sickbeard import helpers
from sickbeard.clients.generic import GenericClient
from lib.rtorrent import RTorrent from lib.rtorrent import RTorrent
from sickbeard import helpers, logger
from sickbeard.clients.generic import GenericClient
import sickbeard
class RtorrentAPI(GenericClient): class RtorrentAPI(GenericClient):
@ -31,60 +29,59 @@ class RtorrentAPI(GenericClient):
super(RtorrentAPI, self).__init__('rTorrent', host, username, password) super(RtorrentAPI, self).__init__('rTorrent', host, username, password)
# self.url = self.host
def _get_auth(self): def _get_auth(self):
self.auth = None self.auth = None
if self.host: if self.host:
try: try:
if self.host and self.host.startswith('scgi:'): if self.host.startswith('scgi:'):
self.username = self.password = None self.username = self.password = None
self.auth = RTorrent(self.host, self.username, self.password, True) self.auth = RTorrent(self.host, self.username, self.password)
except (AssertionError, xmlrpclib.ProtocolError) as e: except (AssertionError, xmlrpclib.ProtocolError):
pass pass
return self.auth return self.auth
def _add_torrent(self, cmd, **kwargs): def _add_torrent(self, cmd, data):
torrent = None torrent = None
if self.auth: if self.auth:
try: try:
if self.auth.has_local_id(data.hash):
logger.log('%s: Item already exists %s' % (self.name, data.name), logger.WARNING)
raise
params = {
'start': not sickbeard.TORRENT_PAUSED,
'extra': ([], ['d.set_custom1=%s' % sickbeard.TORRENT_LABEL])[any([sickbeard.TORRENT_LABEL])] +
([], ['d.set_directory=%s' % sickbeard.TORRENT_PATH])[any([sickbeard.TORRENT_PATH])]
or None}
# Send magnet to rTorrent # Send magnet to rTorrent
if 'file' == cmd: if 'file' == cmd:
torrent = self.auth.load_torrent(kwargs['file']) torrent = self.auth.load_torrent(data.content, **params)
elif 'magnet' == cmd: elif 'magnet' == cmd:
torrent = self.auth.load_magnet(kwargs['uri'], kwargs['btih']) torrent = self.auth.load_magnet(data.url, data.hash, **params)
if torrent: if torrent and sickbeard.TORRENT_LABEL:
label = torrent.get_custom(1)
if sickbeard.TORRENT_LABEL != label:
logger.log('%s: could not change custom1 category \'%s\' to \'%s\' for %s' % (
self.name, label, sickbeard.TORRENT_LABEL, torrent.name), logger.WARNING)
if sickbeard.TORRENT_LABEL: except(Exception, BaseException):
torrent.set_custom(1, sickbeard.TORRENT_LABEL)
if sickbeard.TORRENT_PATH:
torrent.set_directory(sickbeard.TORRENT_PATH)
torrent.start()
except (StandardError, Exception) as e:
pass pass
return any([torrent]) return any([torrent])
def _add_torrent_file(self, result): def _add_torrent_file(self, result):
if result: return result and self._add_torrent('file', result) or False
return self._add_torrent('file', file=result.content)
return False
def _add_torrent_uri(self, result): def _add_torrent_uri(self, result):
if result: return result and self._add_torrent('magnet', result) or False
return self._add_torrent('magnet', uri=result.url, btih=result.hash)
return False
#def _set_torrent_ratio(self, name): # def _set_torrent_ratio(self, name):
# if not name: # if not name:
# return False # return False
@ -122,9 +119,7 @@ class RtorrentAPI(GenericClient):
def test_authentication(self): def test_authentication(self):
try: try:
self._get_auth() if not self._get_auth():
if None is self.auth:
return False, 'Error: Unable to get %s authentication, check your config!' % self.name return False, 'Error: Unable to get %s authentication, check your config!' % self.name
return True, 'Success: Connected and Authenticated' return True, 'Success: Connected and Authenticated'

Loading…
Cancel
Save