├── .env ├── rtorrent_rpc ├── lib │ ├── __init__.py │ ├── xmlrpc │ │ ├── __init__.py │ │ ├── http.py │ │ ├── basic_auth.py │ │ ├── requests_transport.py │ │ └── scgi.py │ ├── torrentparser.py │ └── bencode.py ├── err.py ├── rpc │ ├── call.py │ ├── result.py │ ├── processors.py │ ├── caller.py │ ├── method.py │ └── __init__.py ├── common.py ├── file.py ├── peer.py ├── group.py ├── tracker.py ├── torrent.py └── __init__.py ├── MANIFEST.in ├── .flake8 ├── .gitignore ├── README.md ├── setup.py └── LICENSE /.env: -------------------------------------------------------------------------------- 1 | PYTHONPATH=. 2 | -------------------------------------------------------------------------------- /rtorrent_rpc/lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rtorrent_rpc/lib/xmlrpc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md DOCS.txt CHANGELOG.txt 2 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = 3 | .git, 4 | __pycache__, 5 | venv 6 | max-line-length = 88 7 | -------------------------------------------------------------------------------- /rtorrent_rpc/lib/xmlrpc/http.py: -------------------------------------------------------------------------------- 1 | import xmlrpc.client 2 | 3 | HTTPServerProxy = xmlrpc.client.ServerProxy 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *~ 3 | *.swp 4 | __pycache__ 5 | /build 6 | /dist 7 | /*egg-info 8 | _build 9 | /venv 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ABOUT THIS PROJECT 2 | ------------------ 3 | 4 | The xmlrpc interface to rTorrent is extremely unintuitive and has very little 5 | documentation, this project aims to make interfacing with rTorrent easier. 6 | 7 | Based on the work of 8 | 9 | - Chris Lucas (https://github.com/cjlucas/rtorrent-python) 10 | - miigotu (https://github.com/SickChill/rtorrent-python) 11 | -------------------------------------------------------------------------------- /rtorrent_rpc/err.py: -------------------------------------------------------------------------------- 1 | from rtorrent_rpc.common import convert_version_tuple_to_str 2 | 3 | 4 | class RTorrentVersionError(Exception): 5 | def __init__(self, min_version, cur_version): 6 | self.min_version = min_version 7 | self.cur_version = cur_version 8 | self.msg = "Minimum version required: {0}".format( 9 | convert_version_tuple_to_str(min_version) 10 | ) 11 | 12 | def __str__(self): 13 | return self.msg 14 | 15 | 16 | class MethodError(Exception): 17 | def __init__(self, msg): 18 | self.msg = msg 19 | 20 | def __str__(self): 21 | return self.msg 22 | -------------------------------------------------------------------------------- /rtorrent_rpc/rpc/call.py: -------------------------------------------------------------------------------- 1 | from rtorrent.rpc.method import RPCMethod 2 | 3 | 4 | class RPCCall(object): 5 | def __init__(self, rpc_method: RPCMethod, *args): 6 | self.rpc_method = rpc_method 7 | self.args = list(args) 8 | 9 | def get_method(self) -> RPCMethod: 10 | return self.rpc_method 11 | 12 | def get_args(self) -> list: 13 | return self.args 14 | 15 | def do_pre_processing(self): 16 | for processor in self.get_method().get_pre_processors(): 17 | self.args = processor(*self.get_args()) 18 | 19 | def do_post_processing(self, result): 20 | for processor in self.get_method().get_post_processors(): 21 | result = processor(result) 22 | 23 | return result 24 | -------------------------------------------------------------------------------- /rtorrent_rpc/rpc/result.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | from rtorrent.rpc.call import RPCCall 4 | 5 | 6 | class RPCResult(object): 7 | def __init__(self, rpc_calls: [RPCCall], results): 8 | self._rpc_calls_results_map = collections.OrderedDict() 9 | for call, result in zip(rpc_calls, results): 10 | self._rpc_calls_results_map[call] = result 11 | 12 | def __getitem__(self, item): 13 | if isinstance(item, RPCCall): 14 | return self._rpc_calls_results_map[item] 15 | elif isinstance(item, int): 16 | key = list(self._rpc_calls_results_map.keys())[item] 17 | return self._rpc_calls_results_map[key] 18 | else: 19 | raise AttributeError("Received unsupported item") 20 | 21 | def __iter__(self): 22 | for key in self._rpc_calls_results_map: 23 | yield (key, self._rpc_calls_results_map[key]) 24 | 25 | def __len__(self): 26 | return len(self._rpc_calls_results_map) 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from setuptools import setup, find_packages 4 | 5 | required_pkgs = [] 6 | 7 | classifiers = [ 8 | "Development Status :: 4 - Beta", 9 | "Intended Audience :: Developers", 10 | "License :: OSI Approved :: MIT License", 11 | "Natural Language :: English", 12 | "Operating System :: OS Independent", 13 | "Programming Language :: Python3", 14 | "Topic :: Communications :: File Sharing", 15 | "Topic :: Software Development :: Libraries :: Python Modules", 16 | ] 17 | 18 | setup( 19 | name="rtorrent_rpc", 20 | version="1.0.0-alpha", 21 | url="https://github.com/buzz/rtorrent-rpc", 22 | author="Chris Lucas", 23 | maintainer="buzz", 24 | description="A simple rTorrent interface written in Python3", 25 | keywords="rtorrent p2p", 26 | license="MIT", 27 | packages=find_packages(), 28 | scripts=[], 29 | install_requires=required_pkgs, 30 | classifiers=classifiers, 31 | include_package_data=True, 32 | ) 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) <2020> buzz 4 | Copyright (c) <2014> Chris Lucas 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /rtorrent_rpc/common.py: -------------------------------------------------------------------------------- 1 | def bool_to_int(value): 2 | """Translates python booleans to RPC-safe integers""" 3 | if value is True: 4 | return "1" 5 | elif value is False: 6 | return "0" 7 | else: 8 | return value 9 | 10 | 11 | def cmd_exists(cmds_list, cmd): 12 | """Check if given command is in list of available commands 13 | 14 | @param cmds_list: see L{RTorrent._rpc_methods} 15 | @type cmds_list: list 16 | 17 | @param cmd: name of command to be checked 18 | @type cmd: str 19 | 20 | @return: bool 21 | """ 22 | 23 | return cmd in cmds_list 24 | 25 | 26 | def find_torrent(info_hash, torrent_list): 27 | """Find torrent file in given list of Torrent classes 28 | 29 | @param info_hash: info hash of torrent 30 | @type info_hash: str 31 | 32 | @param torrent_list: list of L{Torrent} instances (see L{RTorrent.get_torrents}) 33 | @type torrent_list: list 34 | 35 | @return: L{Torrent} instance, or -1 if not found 36 | """ 37 | for t in torrent_list: 38 | if t.info_hash == info_hash: 39 | return t 40 | 41 | 42 | def is_valid_port(port): 43 | """Check if given port is valid""" 44 | return 0 <= int(port) <= 65535 45 | 46 | 47 | def convert_version_tuple_to_str(t): 48 | return ".".join([str(n) for n in t]) 49 | 50 | 51 | def safe_repr(fmt, *args, **kwargs): 52 | """ Formatter that handles unicode arguments """ 53 | return fmt.format(*args, **kwargs) 54 | -------------------------------------------------------------------------------- /rtorrent_rpc/rpc/processors.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | 4 | def valmap(from_list, to_list, index=0): 5 | assert len(from_list) == len(to_list) 6 | 7 | def inner(*args): 8 | args = list(args) 9 | to_list_i = from_list.index(args[index]) 10 | args[index] = to_list[to_list_i] 11 | return args 12 | 13 | return inner 14 | 15 | 16 | def choose(arg): 17 | def inner(*args): 18 | if isinstance(arg, int): 19 | return args[arg] 20 | elif isinstance(arg, (list, range)): 21 | return list(map(lambda i: args[i], arg)) 22 | else: 23 | raise AttributeError("Unhandled type {0}".format(type(arg))) 24 | 25 | return inner 26 | 27 | 28 | def int_to_bool(arg): 29 | return arg in [1, "1"] 30 | 31 | 32 | def bool_to_int(arg): 33 | return 1 if arg else 0 34 | 35 | 36 | def check_success(arg): 37 | return arg == 0 38 | 39 | 40 | def us_to_datetime(arg): 41 | return to_datetime(arg) 42 | 43 | 44 | def s_to_datetime(arg): 45 | # Return None if RTorrent returns a timestamp of zero 46 | if arg <= 0: 47 | return None 48 | 49 | # RTorrent timestamps are converted to UTC 50 | # utcfromtimestamp will return a generic datetime object 51 | # without a timezone associated with it 52 | return datetime.datetime.utcfromtimestamp(arg).replace(tzinfo=datetime.timezone.utc) 53 | 54 | 55 | def to_datetime(arg): 56 | # Return None if RTorrent returns a timestamp of zero 57 | if arg <= 0: 58 | return None 59 | 60 | # RTorrent timestamps are in microseconds, we need them it in seconds 61 | arg /= 1.0e6 62 | 63 | # RTorrent timestamps are converted to UTC 64 | # utcfromtimestamp will return a generic datetime object 65 | # without a timezone associated with it 66 | return datetime.datetime.utcfromtimestamp(arg).replace(tzinfo=datetime.timezone.utc) 67 | -------------------------------------------------------------------------------- /rtorrent_rpc/rpc/caller.py: -------------------------------------------------------------------------------- 1 | from rtorrent.rpc.call import RPCCall 2 | from rtorrent.rpc.result import RPCResult 3 | from rtorrent.rpc.method import RPCMethod 4 | 5 | 6 | import xmlrpc.client 7 | 8 | 9 | class RPCCaller(object): 10 | def __init__(self, context): 11 | self.context = context 12 | self.calls = [] 13 | self.available_methods = None 14 | 15 | def add(self, *args): 16 | if isinstance(args[0], RPCCall): 17 | call = args[0] 18 | elif isinstance(args[0], RPCMethod): 19 | call = RPCCall(args[0], *args[1:]) 20 | elif isinstance(args[0], str): 21 | call = RPCCall(RPCMethod(args[0]), *args[1:]) 22 | elif hasattr(args[0], "__self__"): 23 | call = args[0].__self__.rpc_call(args[0].__name__, *args[1:]) 24 | else: 25 | raise RuntimeError("Unexpected args[0]: {0}".format(args[0])) 26 | 27 | self.calls.append(call) 28 | return self 29 | 30 | def call(self): 31 | multi_call = xmlrpc.client.MultiCall(self.context.get_conn()) 32 | for rpc_call in self.calls: 33 | method_name = self._get_method_name(rpc_call.get_method()) 34 | rpc_call.do_pre_processing() 35 | getattr(multi_call, method_name)(*rpc_call.get_args()) 36 | 37 | results = [] 38 | for rpc_call, result in zip(self.calls, multi_call()): 39 | print(rpc_call.get_method().get_method_names()) 40 | result = rpc_call.do_post_processing(result) 41 | results.append(result) 42 | 43 | return RPCResult(self.calls, results) 44 | 45 | def _get_method_name(self, rpc_method: RPCMethod): 46 | if self.available_methods is None: 47 | self.available_methods = self.context.get_available_rpc_methods() 48 | 49 | method_name = rpc_method.get_available_method_name(self.available_methods) 50 | 51 | if method_name is None: 52 | # TODO: Use a different Error subclass 53 | raise AttributeError( 54 | "No matches found for {0}".format(rpc_method.get_method_names()) 55 | ) 56 | 57 | return method_name 58 | -------------------------------------------------------------------------------- /rtorrent_rpc/file.py: -------------------------------------------------------------------------------- 1 | import rtorrent_rpc.rpc 2 | 3 | from rtorrent_rpc.common import safe_repr 4 | 5 | Method = rtorrent_rpc.rpc.Method 6 | 7 | 8 | class File: 9 | """Represents an individual file within a L{Torrent} instance.""" 10 | 11 | def __init__(self, _rt_obj, info_hash, index, **kwargs): 12 | self._rt_obj = _rt_obj 13 | self.info_hash = ( 14 | info_hash # : info hash for the torrent the file is associated with 15 | ) 16 | self.index = index # : The position of the file within the file list 17 | for k in kwargs.keys(): 18 | setattr(self, k, kwargs.get(k, None)) 19 | 20 | self.rpc_id = "{0}:f{1}".format( 21 | self.info_hash, self.index 22 | ) # : unique id to pass to rTorrent 23 | 24 | def update(self): 25 | """Refresh file data 26 | 27 | @note: All fields are stored as attributes to self. 28 | 29 | @return: None 30 | """ 31 | multicall = rtorrent_rpc.rpc.Multicall(self) 32 | retriever_methods = [ 33 | m for m in methods if m.is_retriever() and m.is_available(self._rt_obj) 34 | ] 35 | for method in retriever_methods: 36 | multicall.add(method, self.rpc_id) 37 | 38 | multicall.call() 39 | 40 | def __repr__(self): 41 | return safe_repr('File(index={0} path="{1}")', self.index, self.path) 42 | 43 | 44 | methods = [ 45 | # RETRIEVERS 46 | Method(File, "get_last_touched", "f.last_touched"), 47 | Method(File, "get_range_second", "f.range_second"), 48 | Method(File, "get_size_bytes", "f.size_bytes"), 49 | Method(File, "get_priority", "f.priority"), 50 | Method(File, "get_match_depth_next", "f.match_depth_next"), 51 | Method(File, "is_resize_queued", "f.is_resize_queued", boolean=True,), 52 | Method(File, "get_range_first", "f.range_first"), 53 | Method(File, "get_match_depth_prev", "f.match_depth_prev"), 54 | Method(File, "get_path", "f.path"), 55 | Method(File, "get_completed_chunks", "f.completed_chunks"), 56 | Method(File, "get_path_components", "f.path_components"), 57 | Method(File, "is_created", "f.is_created", boolean=True,), 58 | Method(File, "is_open", "f.is_open", boolean=True,), 59 | Method(File, "get_size_chunks", "f.size_chunks"), 60 | Method(File, "get_offset", "f.offset"), 61 | Method(File, "get_frozen_path", "f.frozen_path"), 62 | Method(File, "get_path_depth", "f.path_depth"), 63 | Method(File, "is_create_queued", "f.is_create_queued", boolean=True,), 64 | # MODIFIERS 65 | ] 66 | -------------------------------------------------------------------------------- /rtorrent_rpc/peer.py: -------------------------------------------------------------------------------- 1 | import rtorrent_rpc.rpc 2 | 3 | from rtorrent_rpc.common import safe_repr 4 | 5 | Method = rtorrent_rpc.rpc.Method 6 | 7 | 8 | class Peer: 9 | """Represents an individual peer within a L{Torrent} instance.""" 10 | 11 | def __init__(self, _rt_obj, info_hash, **kwargs): 12 | self._rt_obj = _rt_obj 13 | self.info_hash = ( 14 | info_hash # : info hash for the torrent the peer is associated with 15 | ) 16 | for k in kwargs.keys(): 17 | setattr(self, k, kwargs.get(k, None)) 18 | 19 | self.rpc_id = "{0}:p{1}".format( 20 | self.info_hash, self.id 21 | ) # : unique id to pass to rTorrent 22 | 23 | def __repr__(self): 24 | return safe_repr("Peer(id={0})", self.id) 25 | 26 | def update(self): 27 | """Refresh peer data 28 | 29 | @note: All fields are stored as attributes to self. 30 | 31 | @return: None 32 | """ 33 | multicall = rtorrent_rpc.rpc.Multicall(self) 34 | retriever_methods = [ 35 | m for m in methods if m.is_retriever() and m.is_available(self._rt_obj) 36 | ] 37 | for method in retriever_methods: 38 | multicall.add(method, self.rpc_id) 39 | 40 | multicall.call() 41 | 42 | 43 | methods = [ 44 | # RETRIEVERS 45 | Method(Peer, "is_preferred", "p.is_preferred", boolean=True,), 46 | Method(Peer, "get_down_rate", "p.down_rate"), 47 | Method(Peer, "is_unwanted", "p.is_unwanted", boolean=True,), 48 | Method(Peer, "get_peer_total", "p.peer_total"), 49 | Method(Peer, "get_peer_rate", "p.peer_rate"), 50 | Method(Peer, "get_port", "p.port"), 51 | Method(Peer, "is_snubbed", "p.is_snubbed", boolean=True,), 52 | Method(Peer, "get_id_html", "p.id_html"), 53 | Method(Peer, "get_up_rate", "p.up_rate"), 54 | Method(Peer, "is_banned", "p.banned", boolean=True,), 55 | Method(Peer, "get_completed_percent", "p.completed_percent"), 56 | Method(Peer, "get_id", "p.id"), 57 | Method(Peer, "is_obfuscated", "p.is_obfuscated", boolean=True,), 58 | Method(Peer, "get_down_total", "p.down_total"), 59 | Method(Peer, "get_client_version", "p.client_version"), 60 | Method(Peer, "get_address", "p.address"), 61 | Method(Peer, "is_incoming", "p.is_incoming", boolean=True,), 62 | Method(Peer, "is_encrypted", "p.is_encrypted", boolean=True,), 63 | Method(Peer, "get_options_str", "p.options_str"), 64 | Method(Peer, "get_client_version", "p.client_version"), 65 | Method(Peer, "get_up_total", "p.up_total"), 66 | # MODIFIERS 67 | ] 68 | -------------------------------------------------------------------------------- /rtorrent_rpc/group.py: -------------------------------------------------------------------------------- 1 | import rtorrent_rpc.rpc 2 | 3 | Method = rtorrent_rpc.rpc.Method 4 | 5 | 6 | class Group: 7 | __name__ = "Group" 8 | 9 | def __init__(self, _rt_obj, name): 10 | self._rt_obj = _rt_obj 11 | self.name = name 12 | 13 | self.methods = [ 14 | # RETRIEVERS 15 | Method( 16 | Group, "get_max", "group." + self.name + ".ratio.max", varname="max" 17 | ), 18 | Method( 19 | Group, "get_min", "group." + self.name + ".ratio.min", varname="min" 20 | ), 21 | Method( 22 | Group, 23 | "get_upload", 24 | "group." + self.name + ".ratio.upload", 25 | varname="upload", 26 | ), 27 | # MODIFIERS 28 | Method( 29 | Group, "set_max", "group." + self.name + ".ratio.max.set", varname="max" 30 | ), 31 | Method( 32 | Group, "set_min", "group." + self.name + ".ratio.min.set", varname="min" 33 | ), 34 | Method( 35 | Group, 36 | "set_upload", 37 | "group." + self.name + ".ratio.upload.set", 38 | varname="upload", 39 | ), 40 | ] 41 | 42 | rtorrent_rpc.rpc._build_rpc_methods(self, self.methods) 43 | 44 | # Setup multicall_add method 45 | def caller(multicall, method, *args): 46 | multicall.add(method, *args) 47 | 48 | setattr(self, "multicall_add", caller) 49 | 50 | def _get_prefix(self): 51 | return "group." + self.name + ".ratio." 52 | 53 | def update(self): 54 | multicall = rtorrent_rpc.rpc.Multicall(self) 55 | 56 | retriever_methods = [ 57 | m for m in self.methods if m.is_retriever() and m.is_available(self._rt_obj) 58 | ] 59 | 60 | for method in retriever_methods: 61 | multicall.add(method) 62 | 63 | multicall.call() 64 | 65 | def enable(self): 66 | p = self._rt_obj._get_conn() 67 | return getattr(p, self._get_prefix() + "enable")() 68 | 69 | def disable(self): 70 | p = self._rt_obj._get_conn() 71 | return getattr(p, self._get_prefix() + "disable")() 72 | 73 | def set_command(self, *methods): 74 | methods = [m + "=" for m in methods] 75 | 76 | m = rtorrent_rpc.rpc.Multicall(self) 77 | self.multicall_add( 78 | m, "method.set", "", self._get_prefix() + "command", *methods 79 | ) 80 | 81 | return m.call()[-1] 82 | -------------------------------------------------------------------------------- /rtorrent_rpc/rpc/method.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | METHOD_TYPE_RETRIEVER = 0 4 | METHOD_TYPE_MODIFIER = 1 5 | METHOD_TYPE_RE = re.compile("\.?set_") 6 | METHOD_VAR_NAME_RE = re.compile("([ptdf]\.|system\.|get\_|is\_|set\_)+([^=]*)") 7 | 8 | int_to_bool = lambda x: x in [1, "1"] 9 | check_success = lambda x: x == 0 10 | 11 | 12 | def valmap(from_list, to_list, arg_index): 13 | def func(*args): 14 | args = list(args) 15 | args[arg_index] = to_list[from_list.index(args[arg_index])] 16 | return tuple(args) 17 | 18 | return func 19 | 20 | 21 | class RPCMethod(object): 22 | def __init__(self, method_names, *args, **kwargs): 23 | if isinstance(method_names, str): 24 | method_names = (method_names,) 25 | 26 | self.method_names = tuple(method_names) 27 | self.method_type = kwargs.get("method_type", self._identify_method_type()) 28 | self.var_name = kwargs.get("var_name", self._identify_var_name()) 29 | self.pre_processors = kwargs.get("pre_processors", []) 30 | self.post_processors = kwargs.get("post_processors", []) 31 | 32 | if kwargs.get("boolean", False): 33 | self.post_processors.append(int_to_bool) 34 | 35 | self.pre_processors = tuple(self.pre_processors) 36 | self.post_processors = tuple(self.post_processors) 37 | 38 | def get_key(self): 39 | return self.key 40 | 41 | def get_method_names(self): 42 | return self.method_names 43 | 44 | def get_var_name(self): 45 | return self.var_name 46 | 47 | def get_pre_processors(self): 48 | return tuple(self.pre_processors) 49 | 50 | def get_post_processors(self): 51 | return tuple(self.post_processors) 52 | 53 | def get_available_method_name(self, available_methods): 54 | matches = set(self.get_method_names()).intersection(available_methods) 55 | return matches.pop() if len(matches) > 0 else None 56 | 57 | def is_available(self, available_methods) -> bool: 58 | return self.get_available_method_name(available_methods) is not None 59 | 60 | def is_retriever(self) -> bool: 61 | return self._identify_method_type() == METHOD_TYPE_RETRIEVER 62 | 63 | def _identify_method_type(self): 64 | match = METHOD_TYPE_RE.search(self.method_names[0]) 65 | return METHOD_TYPE_MODIFIER if match else METHOD_TYPE_RETRIEVER 66 | 67 | def _identify_var_name(self): 68 | match = re.match(METHOD_VAR_NAME_RE, self.method_names[0]) 69 | if match is None: 70 | raise Exception( 71 | "Could not id var_name for method: {0}".format(self.method_names) 72 | ) 73 | 74 | return match.groups(-1) 75 | -------------------------------------------------------------------------------- /rtorrent_rpc/lib/xmlrpc/basic_auth.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2013 Dean Gardiner, 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining 5 | # a copy of this software and associated documentation files (the 6 | # "Software"), to deal in the Software without restriction, including 7 | # without limitation the rights to use, copy, modify, merge, publish, 8 | # distribute, sublicense, and/or sell copies of the Software, and to 9 | # permit persons to whom the Software is furnished to do so, subject to 10 | # the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | from base64 import encodestring 24 | import string 25 | import xmlrpclib 26 | 27 | 28 | class BasicAuthTransport(xmlrpclib.Transport): 29 | def __init__(self, username=None, password=None): 30 | xmlrpclib.Transport.__init__(self) 31 | 32 | self.username = username 33 | self.password = password 34 | 35 | def send_auth(self, h): 36 | if self.username is not None and self.password is not None: 37 | h.putheader( 38 | "AUTHORIZATION", 39 | "Basic %s" 40 | % string.replace( 41 | encodestring("%s:%s" % (self.username, self.password)), "\012", "" 42 | ), 43 | ) 44 | 45 | def single_request(self, host, handler, request_body, verbose=0): 46 | # issue XML-RPC request 47 | 48 | h = self.make_connection(host) 49 | if verbose: 50 | h.set_debuglevel(1) 51 | 52 | try: 53 | self.send_request(h, handler, request_body) 54 | self.send_host(h, host) 55 | self.send_user_agent(h) 56 | self.send_auth(h) 57 | self.send_content(h, request_body) 58 | 59 | response = h.getresponse(buffering=True) 60 | if response.status == 200: 61 | self.verbose = verbose 62 | return self.parse_response(response) 63 | except xmlrpclib.Fault: 64 | raise 65 | except Exception: 66 | self.close() 67 | raise 68 | 69 | # discard any response data and raise exception 70 | if response.getheader("content-length", 0): 71 | response.read() 72 | raise xmlrpclib.ProtocolError( 73 | host + handler, response.status, response.reason, response.msg, 74 | ) 75 | -------------------------------------------------------------------------------- /rtorrent_rpc/tracker.py: -------------------------------------------------------------------------------- 1 | import rtorrent_rpc.rpc 2 | 3 | from rtorrent_rpc.common import safe_repr 4 | 5 | Method = rtorrent_rpc.rpc.Method 6 | 7 | 8 | class Tracker: 9 | """Represents an individual tracker within a L{Torrent} instance.""" 10 | 11 | def __init__(self, _rt_obj, info_hash, **kwargs): 12 | self._rt_obj = _rt_obj 13 | self.info_hash = info_hash # : info hash for the torrent using this tracker 14 | for k in kwargs.keys(): 15 | setattr(self, k, kwargs.get(k, None)) 16 | 17 | # for clarity's sake... 18 | self.index = ( 19 | self.group 20 | ) # : position of tracker within the torrent's tracker list 21 | self.rpc_id = "{0}:t{1}".format( 22 | self.info_hash, self.index 23 | ) # : unique id to pass to rTorrent 24 | 25 | def __repr__(self): 26 | return safe_repr('Tracker(index={0}, url="{1}")', self.index, self.url) 27 | 28 | def enable(self): 29 | """Alias for set_enabled("yes")""" 30 | self.set_enabled("yes") 31 | 32 | def disable(self): 33 | """Alias for set_enabled("no")""" 34 | self.set_enabled("no") 35 | 36 | def update(self): 37 | """Refresh tracker data 38 | 39 | @note: All fields are stored as attributes to self. 40 | 41 | @return: None 42 | """ 43 | multicall = rtorrent_rpc.rpc.Multicall(self) 44 | retriever_methods = [ 45 | m for m in methods if m.is_retriever() and m.is_available(self._rt_obj) 46 | ] 47 | for method in retriever_methods: 48 | multicall.add(method, self.rpc_id) 49 | 50 | multicall.call() 51 | 52 | 53 | methods = [ 54 | # RETRIEVERS 55 | Method(Tracker, "is_enabled", "t.is_enabled", boolean=True), 56 | Method(Tracker, "get_id", "t.id"), 57 | Method(Tracker, "get_scrape_incomplete", "t.scrape_incomplete"), 58 | Method(Tracker, "is_open", "t.is_open", boolean=True), 59 | Method(Tracker, "get_min_interval", "t.min_interval"), 60 | Method(Tracker, "get_scrape_downloaded", "t.scrape_downloaded"), 61 | Method(Tracker, "get_group", "t.group"), 62 | Method(Tracker, "get_scrape_time_last", "t.scrape_time_last"), 63 | Method(Tracker, "get_type", "t.type"), 64 | Method(Tracker, "get_normal_interval", "t.normal_interval"), 65 | Method(Tracker, "get_url", "t.url"), 66 | Method(Tracker, "get_scrape_complete", "t.scrape_complete", min_version=(0, 8, 9),), 67 | Method( 68 | Tracker, 69 | "get_activity_time_last", 70 | "t.activity_time_last", 71 | min_version=(0, 8, 9), 72 | ), 73 | Method( 74 | Tracker, 75 | "get_activity_time_next", 76 | "t.activity_time_next", 77 | min_version=(0, 8, 9), 78 | ), 79 | Method( 80 | Tracker, "get_failed_time_last", "t.failed_time_last", min_version=(0, 8, 9), 81 | ), 82 | Method( 83 | Tracker, "get_failed_time_next", "t.failed_time_next", min_version=(0, 8, 9), 84 | ), 85 | Method( 86 | Tracker, "get_success_time_last", "t.success_time_last", min_version=(0, 8, 9), 87 | ), 88 | Method( 89 | Tracker, "get_success_time_next", "t.success_time_next", min_version=(0, 8, 9), 90 | ), 91 | Method(Tracker, "can_scrape", "t.can_scrape", min_version=(0, 9, 1), boolean=True), 92 | Method(Tracker, "get_failed_counter", "t.failed_counter", min_version=(0, 8, 9)), 93 | Method(Tracker, "get_scrape_counter", "t.scrape_counter", min_version=(0, 8, 9)), 94 | Method(Tracker, "get_success_counter", "t.success_counter", min_version=(0, 8, 9)), 95 | Method(Tracker, "is_usable", "t.is_usable", min_version=(0, 9, 1), boolean=True), 96 | Method(Tracker, "is_busy", "t.is_busy", min_version=(0, 9, 1), boolean=True), 97 | Method( 98 | Tracker, 99 | "is_extra_tracker", 100 | "t.is_extra_tracker", 101 | min_version=(0, 9, 1), 102 | boolean=True, 103 | ), 104 | Method( 105 | Tracker, "get_latest_sum_peers", "t.latest_sum_peers", min_version=(0, 9, 0) 106 | ), 107 | Method( 108 | Tracker, "get_latest_new_peers", "t.latest_new_peers", min_version=(0, 9, 0) 109 | ), 110 | # MODIFIERS 111 | Method(Tracker, "set_enabled", "t.is_enabled.set"), 112 | ] 113 | -------------------------------------------------------------------------------- /rtorrent_rpc/lib/torrentparser.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import re 3 | import rtorrent_rpc.lib.bencode as bencode 4 | import hashlib 5 | 6 | from urllib.request import urlopen 7 | 8 | 9 | class TorrentParser: 10 | def __init__(self, torrent): 11 | """Decode and parse given torrent 12 | 13 | @param torrent: handles: urls, file paths, string of torrent data 14 | @type torrent: str 15 | 16 | @raise AssertionError: Can be raised for a couple reasons: 17 | - If _get_raw_torrent() couldn't figure out 18 | what X{torrent} is 19 | - if X{torrent} isn't a valid bencoded torrent file 20 | """ 21 | self.torrent = torrent 22 | self._raw_torrent = None # : testing yo 23 | self._torrent_decoded = None # : what up 24 | self.file_type = None 25 | 26 | self._get_raw_torrent() 27 | assert self._raw_torrent is not None, "Couldn't get raw_torrent." 28 | if self._torrent_decoded is None: 29 | self._decode_torrent() 30 | assert isinstance(self._torrent_decoded, dict), "Invalid torrent file." 31 | self._parse_torrent() 32 | 33 | def _is_raw(self): 34 | raw = False 35 | if isinstance(self.torrent, (str, bytes)): 36 | if isinstance(self._decode_torrent(self.torrent), dict): 37 | raw = True 38 | else: 39 | # reset self._torrent_decoded (currently equals False) 40 | self._torrent_decoded = None 41 | 42 | return raw 43 | 44 | def _get_raw_torrent(self): 45 | """Get raw torrent data by determining what self.torrent is""" 46 | # already raw? 47 | if self._is_raw(): 48 | self.file_type = "raw" 49 | self._raw_torrent = self.torrent 50 | return 51 | # local file? 52 | if os.path.isfile(self.torrent): 53 | self.file_type = "file" 54 | self._raw_torrent = open(self.torrent, "rb").read() 55 | # url? 56 | elif re.search(r"^(http|ftp):\/\/", self.torrent, re.I): 57 | self.file_type = "url" 58 | self._raw_torrent = urlopen(self.torrent).read() 59 | 60 | def _decode_torrent(self, raw_torrent=None): 61 | if raw_torrent is None: 62 | raw_torrent = self._raw_torrent 63 | self._torrent_decoded = bencode.decode(raw_torrent) 64 | return self._torrent_decoded 65 | 66 | def _calc_info_hash(self): 67 | self.info_hash = None 68 | if "info" in self._torrent_decoded.keys(): 69 | info_encoded = bencode.encode(self._torrent_decoded["info"]) 70 | 71 | if info_encoded: 72 | self.info_hash = hashlib.sha1(info_encoded).hexdigest().upper() 73 | 74 | return self.info_hash 75 | 76 | def _parse_torrent(self): 77 | for k in self._torrent_decoded: 78 | key = k.replace(" ", "_").lower() 79 | setattr(self, key, self._torrent_decoded[k]) 80 | 81 | self._calc_info_hash() 82 | 83 | 84 | class NewTorrentParser(object): 85 | @staticmethod 86 | def _read_file(fp): 87 | return fp.read() 88 | 89 | @staticmethod 90 | def _write_file(fp): 91 | fp.write() 92 | return fp 93 | 94 | @staticmethod 95 | def _decode_torrent(data): 96 | return bencode.decode(data) 97 | 98 | def __init__(self, input): 99 | self.input = input 100 | self._raw_torrent = None 101 | self._decoded_torrent = None 102 | self._hash_outdated = False 103 | 104 | if isinstance(self.input, (str, bytes)): 105 | # path to file? 106 | if os.path.isfile(self.input): 107 | self._raw_torrent = self._read_file(open(self.input, "rb")) 108 | else: 109 | # assume input was the raw torrent data (do we really want 110 | # this?) 111 | self._raw_torrent = self.input 112 | 113 | # file-like object? 114 | elif self.input.hasattr("read"): 115 | self._raw_torrent = self._read_file(self.input) 116 | 117 | assert ( 118 | self._raw_torrent is not None 119 | ), "Invalid input: input must be a path or a file-like object" 120 | 121 | self._decoded_torrent = self._decode_torrent(self._raw_torrent) 122 | 123 | assert isinstance(self._decoded_torrent, dict), "File could not be decoded" 124 | 125 | def _calc_info_hash(self): 126 | self.info_hash = None 127 | info_dict = self._torrent_decoded["info"] 128 | self.info_hash = hashlib.sha1(bencode.encode(info_dict)).hexdigest().upper() 129 | 130 | return self.info_hash 131 | 132 | def set_tracker(self, tracker): 133 | self._decoded_torrent["announce"] = tracker 134 | 135 | def get_tracker(self): 136 | return self._decoded_torrent.get("announce") 137 | -------------------------------------------------------------------------------- /rtorrent_rpc/lib/xmlrpc/requests_transport.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2015 Alexandre Beloin, 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | """A transport for Python2/3 xmlrpc library using requests 17 | 18 | Support: 19 | -SSL with Basic and Digest authentication 20 | -Proxies 21 | """ 22 | 23 | import xmlrpc.client as xmlrpc_client 24 | 25 | import traceback 26 | 27 | import requests 28 | from requests.exceptions import RequestException 29 | from requests.auth import HTTPBasicAuth 30 | from requests.auth import HTTPDigestAuth 31 | from requests.packages.urllib3 import disable_warnings 32 | 33 | 34 | class RequestsTransport(xmlrpc_client.Transport): 35 | 36 | """Transport class for xmlrpc using requests""" 37 | 38 | def __init__( 39 | self, 40 | use_https=True, 41 | authtype=None, 42 | username=None, 43 | password=None, 44 | check_ssl_cert=True, 45 | proxies=None, 46 | ): 47 | """Inits RequestsTransport. 48 | 49 | Args: 50 | use_https: If true, https else http 51 | authtype: None, basic or digest 52 | username: Username 53 | password: Password 54 | check_ssl_cert: Check SSL certificate 55 | proxies: A dict of proxies( 56 | Ex: {"http": "http://10.10.1.10:3128", 57 | "https": "http://10.10.1.10:1080",}) 58 | 59 | Raises: 60 | ValueError: Invalid info 61 | """ 62 | # Python 2 can't use super on old style class. 63 | if issubclass(xmlrpc_client.Transport, object): 64 | super(RequestsTransport, self).__init__() 65 | else: 66 | xmlrpc_client.Transport.__init__(self) 67 | 68 | self.user_agent = "Python Requests/" + requests.__version__ 69 | 70 | self._use_https = use_https 71 | self._check_ssl_cert = check_ssl_cert 72 | 73 | if authtype == "basic" or authtype == "digest": 74 | self._authtype = authtype 75 | else: 76 | raise ValueError("Supported authentication are: basic and digest") 77 | if authtype and (not username or not password): 78 | raise ValueError("Username and password required when using authentication") 79 | 80 | self._username = username 81 | self._password = password 82 | if proxies is None: 83 | self._proxies = {} 84 | else: 85 | self._proxies = proxies 86 | 87 | def request(self, host, handler, request_body, verbose=0): 88 | """Replace the xmlrpc request function. 89 | 90 | Process xmlrpc request via requests library. 91 | 92 | Args: 93 | host: Target host 94 | handler: Target PRC handler. 95 | request_body: XML-RPC request body. 96 | verbose: Debugging flag. 97 | 98 | Returns: 99 | Parsed response. 100 | 101 | Raises: 102 | RequestException: Error in requests 103 | """ 104 | if verbose: 105 | self._debug() 106 | 107 | if not self._check_ssl_cert: 108 | disable_warnings() 109 | 110 | headers = { 111 | "User-Agent": self.user_agent, 112 | "Content-Type": "text/xml", 113 | } 114 | 115 | # Need to be done because the schema(http or https) is lost in 116 | # xmlrpc.Transport's init. 117 | if self._use_https: 118 | url = "https://{host}/{handler}".format(host=host, handler=handler) 119 | else: 120 | url = "http://{host}/{handler}".format(host=host, handler=handler) 121 | 122 | # TODO Construct kwargs query instead 123 | try: 124 | if self._authtype == "basic": 125 | response = requests.post( 126 | url, 127 | data=request_body, 128 | headers=headers, 129 | verify=self._check_ssl_cert, 130 | auth=HTTPBasicAuth(self._username, self._password), 131 | proxies=self._proxies, 132 | ) 133 | elif self._authtype == "digest": 134 | response = requests.post( 135 | url, 136 | data=request_body, 137 | headers=headers, 138 | verify=self._check_ssl_cert, 139 | auth=HTTPDigestAuth(self._username, self._password), 140 | proxies=self._proxies, 141 | ) 142 | else: 143 | response = requests.post( 144 | url, 145 | data=request_body, 146 | headers=headers, 147 | verify=self._check_ssl_cert, 148 | proxies=self._proxies, 149 | ) 150 | 151 | response.raise_for_status() 152 | except RequestException as error: 153 | raise xmlrpc_client.ProtocolError( 154 | url, error.message, traceback.format_exc(), response.headers 155 | ) 156 | 157 | return self.parse_response(response) 158 | 159 | def parse_response(self, response): 160 | """Replace the xmlrpc parse_response function. 161 | 162 | Parse response. 163 | 164 | Args: 165 | response: Requests return data 166 | 167 | Returns: 168 | Response tuple and target method. 169 | """ 170 | p, u = self.getparser() 171 | p.feed(response.text.encode("utf-8")) 172 | p.close() 173 | return u.close() 174 | 175 | def _debug(self): 176 | """Debug requests module. 177 | 178 | Enable verbose logging from requests 179 | """ 180 | # TODO Ugly 181 | import logging 182 | 183 | try: 184 | import http.client as http_client 185 | except ImportError: 186 | import httplib as http_client 187 | 188 | http_client.HTTPConnection.debuglevel = 1 189 | 190 | logging.basicConfig() 191 | logging.getLogger().setLevel(logging.DEBUG) 192 | requests_log = logging.getLogger("requests.packages.urllib3") 193 | requests_log.setLevel(logging.DEBUG) 194 | requests_log.propagate = True 195 | -------------------------------------------------------------------------------- /rtorrent_rpc/lib/xmlrpc/scgi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # rtorrent_xmlrpc 4 | # (c) 2011 Roger Que 5 | # 6 | # Modified portions: 7 | # (c) 2013 Dean Gardiner 8 | # 9 | # Python module for interacting with rtorrent's XML-RPC interface 10 | # directly over SCGI, instead of through an HTTP server intermediary. 11 | # Inspired by Glenn Washburn's xmlrpc2scgi.py [1], but subclasses the 12 | # built-in xmlrpc.client classes so that it is compatible with features 13 | # such as MultiCall objects. 14 | # 15 | # [1] 16 | # 17 | # Usage: server = SCGIServerProxy('scgi://localhost:7000/') 18 | # server = SCGIServerProxy('scgi:///path/to/scgi.sock') 19 | # print server.system.listMethods() 20 | # mc = xmlrpc.client.MultiCall(server) 21 | # mc.get_up_rate() 22 | # mc.get_down_rate() 23 | # print mc() 24 | # 25 | # 26 | # 27 | # This program is free software; you can redistribute it and/or modify 28 | # it under the terms of the GNU General Public License as published by 29 | # the Free Software Foundation; either version 2 of the License, or 30 | # (at your option) any later version. 31 | # 32 | # This program is distributed in the hope that it will be useful, 33 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 34 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 35 | # GNU General Public License for more details. 36 | # 37 | # You should have received a copy of the GNU General Public License 38 | # along with this program; if not, write to the Free Software 39 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA 40 | # 41 | # In addition, as a special exception, the copyright holders give 42 | # permission to link the code of portions of this program with the 43 | # OpenSSL library under certain conditions as described in each 44 | # individual source file, and distribute linked combinations 45 | # including the two. 46 | # 47 | # You must obey the GNU General Public License in all respects for 48 | # all of the code used other than OpenSSL. If you modify file(s) 49 | # with this exception, you may extend this exception to your version 50 | # of the file(s), but you are not obligated to do so. If you do not 51 | # wish to do so, delete this exception statement from your version. 52 | # If you delete this exception statement from all source files in the 53 | # program, then also delete it here. 54 | # 55 | # 56 | # 57 | # Portions based on Python's xmlrpc.client: 58 | # 59 | # Copyright (c) 1999-2002 by Secret Labs AB 60 | # Copyright (c) 1999-2002 by Fredrik Lundh 61 | # 62 | # By obtaining, using, and/or copying this software and/or its 63 | # associated documentation, you agree that you have read, understood, 64 | # and will comply with the following terms and conditions: 65 | # 66 | # Permission to use, copy, modify, and distribute this software and 67 | # its associated documentation for any purpose and without fee is 68 | # hereby granted, provided that the above copyright notice appears in 69 | # all copies, and that both that copyright notice and this permission 70 | # notice appear in supporting documentation, and that the name of 71 | # Secret Labs AB or the author not be used in advertising or publicity 72 | # pertaining to distribution of the software without specific, written 73 | # prior permission. 74 | # 75 | # SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD 76 | # TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- 77 | # ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR 78 | # BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY 79 | # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, 80 | # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS 81 | # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE 82 | # OF THIS SOFTWARE. 83 | 84 | import http.client 85 | import re 86 | import socket 87 | import urllib 88 | import xmlrpc.client 89 | import errno 90 | 91 | 92 | class SCGITransport(xmlrpc.client.Transport): 93 | # Added request() from Python 2.7 xmlrpc.client here to backport to Python 2.6 94 | def request(self, host, handler, request_body, verbose=0): 95 | # retry request once if cached connection has gone cold 96 | for i in (0, 1): 97 | try: 98 | return self.single_request(host, handler, request_body, verbose) 99 | except socket.error as e: 100 | if i or e.errno not in ( 101 | errno.ECONNRESET, 102 | errno.ECONNABORTED, 103 | errno.EPIPE, 104 | ): 105 | raise 106 | except http.client.BadStatusLine: # close after we sent request 107 | if i: 108 | raise 109 | 110 | def single_request(self, host, handler, request_body, verbose=0): 111 | # Add SCGI headers to the request. 112 | headers = {"CONTENT_LENGTH": str(len(request_body)), "SCGI": "1"} 113 | header = ( 114 | "\x00".join(("%s\x00%s" % item for item in headers.iteritems())) + "\x00" 115 | ) 116 | header = "%d:%s" % (len(header), header) 117 | request_body = "%s,%s" % (header, request_body) 118 | 119 | sock = None 120 | 121 | try: 122 | if host: 123 | host, port = urllib.splitport(host) 124 | addrinfo = socket.getaddrinfo( 125 | host, int(port), socket.AF_INET, socket.SOCK_STREAM 126 | ) 127 | sock = socket.socket(*addrinfo[0][:3]) 128 | sock.connect(addrinfo[0][4]) 129 | else: 130 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 131 | sock.connect(handler) 132 | 133 | self.verbose = verbose 134 | 135 | sock.send(request_body) 136 | return self.parse_response(sock.makefile()) 137 | finally: 138 | if sock: 139 | sock.close() 140 | 141 | def parse_response(self, response): 142 | p, u = self.getparser() 143 | 144 | response_body = "" 145 | while True: 146 | data = response.read(1024) 147 | if not data: 148 | break 149 | response_body += data 150 | 151 | # Remove SCGI headers from the response. 152 | response_header, response_body = re.split( 153 | r"\n\s*?\n", response_body, maxsplit=1 154 | ) 155 | 156 | if self.verbose: 157 | print("body:", repr(response_body)) 158 | 159 | p.feed(response_body) 160 | p.close() 161 | 162 | return u.close() 163 | 164 | 165 | class SCGIServerProxy(xmlrpc.client.ServerProxy): 166 | def __init__( 167 | self, 168 | uri, 169 | transport=None, 170 | encoding=None, 171 | verbose=False, 172 | allow_none=False, 173 | use_datetime=False, 174 | ): 175 | type, uri = urllib.splittype(uri) 176 | if type not in ("scgi"): 177 | raise IOError("unsupported XML-RPC protocol") 178 | self.__host, self.__handler = urllib.splithost(uri) 179 | if not self.__handler: 180 | self.__handler = "/" 181 | 182 | if transport is None: 183 | transport = SCGITransport(use_datetime=use_datetime) 184 | self.__transport = transport 185 | 186 | self.__encoding = encoding 187 | self.__verbose = verbose 188 | self.__allow_none = allow_none 189 | 190 | def __close(self): 191 | self.__transport.close() 192 | 193 | def __request(self, methodname, params): 194 | # call a method on the remote server 195 | 196 | request = xmlrpc.client.dumps( 197 | params, methodname, encoding=self.__encoding, allow_none=self.__allow_none 198 | ) 199 | 200 | response = self.__transport.request( 201 | self.__host, self.__handler, request, verbose=self.__verbose 202 | ) 203 | 204 | if len(response) == 1: 205 | response = response[0] 206 | 207 | return response 208 | 209 | def __repr__(self): 210 | return "" % (self.__host, self.__handler) 211 | 212 | __str__ = __repr__ 213 | 214 | def __getattr__(self, name): 215 | # magic method dispatcher 216 | return xmlrpc.client._Method(self.__request, name) 217 | 218 | # note: to call a remote object with an non-standard name, use 219 | # result getattr(server, "strange-python-name")(args) 220 | 221 | def __call__(self, attr): 222 | """A workaround to get special attributes on the ServerProxy 223 | without interfering with the magic __getattr__ 224 | """ 225 | if attr == "close": 226 | return self.__close 227 | elif attr == "transport": 228 | return self.__transport 229 | raise AttributeError("Attribute %r not found" % (attr,)) 230 | -------------------------------------------------------------------------------- /rtorrent_rpc/rpc/__init__.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import xmlrpc.client 3 | import re 4 | 5 | 6 | import rtorrent_rpc 7 | from rtorrent_rpc.common import bool_to_int, convert_version_tuple_to_str, safe_repr 8 | from rtorrent_rpc.err import MethodError 9 | 10 | 11 | def get_varname(rpc_call): 12 | """Transform rpc method into variable name. 13 | 14 | @newfield example: Example 15 | @example: if the name of the rpc method is 'p.down_rate', the variable 16 | name will be 'down_rate' 17 | """ 18 | # extract variable name from xmlrpc func name 19 | r = re.match(r"(?:[ptdf]\.)?(.+?)(?:\.set)?$", rpc_call) 20 | if r: 21 | return r.group(1) 22 | 23 | 24 | def _handle_unavailable_rpc_method(method, rt_obj): 25 | msg = "Method isn't available." 26 | if rt_obj._get_client_version_tuple() < method.min_version: 27 | msg = ( 28 | "This method is only available in " 29 | "RTorrent version v{0} or later".format( 30 | convert_version_tuple_to_str(method.min_version) 31 | ) 32 | ) 33 | 34 | raise MethodError(msg) 35 | 36 | 37 | class DummyClass: 38 | def __init__(self): 39 | pass 40 | 41 | 42 | class Method: 43 | """Represents an individual RPC method""" 44 | 45 | def __init__( 46 | self, _class, method_name, rpc_call, docstring=None, varname=None, **kwargs 47 | ): 48 | self._class = _class # : Class this method is associated with 49 | self.class_name = _class.__name__ 50 | self.method_name = method_name # : name of public-facing method 51 | self.rpc_call = rpc_call # : name of rpc method 52 | self.docstring = docstring # : docstring for rpc method (optional) 53 | # : variable for the result of the method call, usually set to self.varname 54 | self.varname = varname 55 | self.min_version = kwargs.get( 56 | "min_version", (0, 0, 0) 57 | ) # : Minimum version of rTorrent required 58 | self.boolean = kwargs.get("boolean", False) # : returns boolean value? 59 | self.post_process_func = kwargs.get( 60 | "post_process_func", None 61 | ) # : custom post process function 62 | self.aliases = kwargs.get("aliases", []) # : aliases for method (optional) 63 | self.required_args = [] 64 | #: Arguments required when calling the method (not utilized) 65 | 66 | self.method_type = self._get_method_type() 67 | 68 | if self.varname is None: 69 | self.varname = get_varname(self.rpc_call) 70 | assert self.varname is not None, "Couldn't get variable name." 71 | 72 | def __repr__(self): 73 | return safe_repr( 74 | "Method(method_name='{0}', rpc_call='{1}')", self.method_name, self.rpc_call 75 | ) 76 | 77 | def _get_method_type(self): 78 | """Determine whether method is a modifier or a retriever""" 79 | if self.method_name[:4] == "set_": 80 | return "m" # modifier 81 | else: 82 | return "r" # retriever 83 | 84 | def is_modifier(self): 85 | if self.method_type == "m": 86 | return True 87 | else: 88 | return False 89 | 90 | def is_retriever(self): 91 | if self.method_type == "r": 92 | return True 93 | else: 94 | return False 95 | 96 | def is_available(self, rt_obj): 97 | if ( 98 | rt_obj._get_client_version_tuple() < self.min_version 99 | or self.rpc_call not in rt_obj._get_rpc_methods() 100 | ): 101 | return False 102 | else: 103 | return True 104 | 105 | 106 | class Multicall: 107 | def __init__(self, class_obj, **kwargs): 108 | self.class_obj = class_obj 109 | if class_obj.__class__.__name__ == "RTorrent": 110 | self.rt_obj = class_obj 111 | else: 112 | self.rt_obj = class_obj._rt_obj 113 | self.calls = [] 114 | 115 | def add(self, method, *args): 116 | """Add call to multicall 117 | 118 | @param method: L{Method} instance or name of raw RPC method 119 | @type method: Method or str 120 | 121 | @param args: call arguments 122 | """ 123 | # if a raw rpc method was given instead of a Method instance, 124 | # try and find the instance for it. And if all else fails, create a 125 | # dummy Method instance 126 | if isinstance(method, str): 127 | result = find_method(method) 128 | # if result not found 129 | if result == -1: 130 | method = Method(DummyClass, method, method) 131 | else: 132 | method = result 133 | 134 | # ensure method is available before adding 135 | if not method.is_available(self.rt_obj): 136 | _handle_unavailable_rpc_method(method, self.rt_obj) 137 | 138 | self.calls.append((method, args)) 139 | 140 | def list_calls(self): 141 | for c in self.calls: 142 | print(c) 143 | 144 | def call(self): 145 | """Execute added multicall calls 146 | 147 | @return: the results (post-processed), in the order they were added 148 | @rtype: tuple 149 | """ 150 | m = xmlrpc.client.MultiCall(self.rt_obj._get_conn()) 151 | for call in self.calls: 152 | method, args = call 153 | rpc_call = getattr(method, "rpc_call") 154 | getattr(m, rpc_call)(*args) 155 | 156 | results = m() 157 | results = tuple(results) 158 | results_processed = [] 159 | 160 | for r, c in zip(results, self.calls): 161 | method = c[0] # Method instance 162 | result = process_result(method, r) 163 | results_processed.append(result) 164 | # assign result to class_obj 165 | exists = hasattr(self.class_obj, method.varname) 166 | if not exists or not inspect.ismethod( 167 | getattr(self.class_obj, method.varname) 168 | ): 169 | setattr(self.class_obj, method.varname, result) 170 | 171 | return tuple(results_processed) 172 | 173 | 174 | def call_method(class_obj, method, *args): 175 | """Handles single RPC calls 176 | 177 | @param class_obj: Peer/File/Torrent/Tracker/RTorrent instance 178 | @type class_obj: object 179 | 180 | @param method: L{Method} instance or name of raw RPC method 181 | @type method: Method or str 182 | """ 183 | if method.is_retriever(): 184 | args = args[:-1] 185 | else: 186 | assert args[-1] is not None, "No argument given." 187 | 188 | if class_obj.__class__.__name__ == "RTorrent": 189 | rt_obj = class_obj 190 | else: 191 | rt_obj = class_obj._rt_obj 192 | 193 | # check if rpc method is even available 194 | if not method.is_available(rt_obj): 195 | _handle_unavailable_rpc_method(method, rt_obj) 196 | 197 | m = Multicall(class_obj) 198 | m.add(method, *args) 199 | # only added one method, only getting one result back 200 | ret_value = m.call()[0] 201 | 202 | return ret_value 203 | 204 | 205 | def find_method(rpc_call): 206 | """Return L{Method} instance associated with given RPC call""" 207 | method_lists = [ 208 | rtorrent_rpc.methods, 209 | rtorrent_rpc.file.methods, 210 | rtorrent_rpc.tracker.methods, 211 | rtorrent_rpc.peer.methods, 212 | rtorrent_rpc.torrent.methods, 213 | ] 214 | 215 | for method_list in method_lists: 216 | for method_name in method_list: 217 | if method_name.rpc_call.lower() == rpc_call.lower(): 218 | return method_name 219 | 220 | return -1 221 | 222 | 223 | def process_result(method, result): 224 | """Process given C{B{result}} based on flags set in C{B{method}} 225 | 226 | @param method: L{Method} instance 227 | @type method: Method 228 | 229 | @param result: result to be processed (the result of given L{Method} instance) 230 | 231 | @note: Supported Processing: 232 | - boolean - convert ones and zeros returned by rTorrent and 233 | convert to python boolean values 234 | """ 235 | # handle custom post processing function 236 | if method.post_process_func is not None: 237 | result = method.post_process_func(result) 238 | 239 | # is boolean? 240 | if method.boolean: 241 | if result in [1, "1"]: 242 | result = True 243 | elif result in [0, "0"]: 244 | result = False 245 | 246 | return result 247 | 248 | 249 | def _build_rpc_methods(class_, method_list): 250 | """Build glorified aliases to raw RPC methods""" 251 | instance = None 252 | if not inspect.isclass(class_): 253 | instance = class_ 254 | class_ = instance.__class__ 255 | 256 | for method_name in method_list: 257 | class_name = method_name.class_name 258 | if class_name != class_.__name__: 259 | continue 260 | 261 | if class_name == "RTorrent": 262 | caller = lambda self, arg=None, method=method_name: call_method( 263 | self, method, bool_to_int(arg) 264 | ) 265 | elif class_name == "Torrent": 266 | caller = lambda self, arg=None, method=method_name: call_method( 267 | self, method, self.rpc_id, bool_to_int(arg) 268 | ) 269 | elif class_name in ["Tracker", "File"]: 270 | caller = lambda self, arg=None, method=method_name: call_method( 271 | self, method, self.rpc_id, bool_to_int(arg) 272 | ) 273 | 274 | elif class_name == "Peer": 275 | caller = lambda self, arg=None, method=method_name: call_method( 276 | self, method, self.rpc_id, bool_to_int(arg) 277 | ) 278 | 279 | elif class_name == "Group": 280 | caller = lambda arg=None, method=method_name: call_method( 281 | instance, method, bool_to_int(arg) 282 | ) 283 | 284 | if method_name.docstring is None: 285 | method_name.docstring = "" 286 | 287 | # print(m) 288 | docstring = """{0} 289 | 290 | @note: Variable where the result for this method is stored: {1}.{2}""".format( 291 | method_name.docstring, class_name, method_name.varname 292 | ) 293 | 294 | caller.__doc__ = docstring 295 | 296 | for method_name in [method_name.method_name] + list(method_name.aliases): 297 | if instance is None: 298 | setattr(class_, method_name, caller) 299 | else: 300 | setattr(instance, method_name, caller) 301 | -------------------------------------------------------------------------------- /rtorrent_rpc/lib/bencode.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2011 by clueless 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | # 21 | # Version: 20111107 22 | # 23 | # Changelog 24 | # --------- 25 | # 2011-11-07 - Added support for Python2 (tested on 2.6) 26 | # 2011-10-03 - Fixed: moved check for end of list at the top of the while loop 27 | # in _decode_list (in case the list is empty) (Chris Lucas) 28 | # - Converted dictionary keys to str 29 | # 2011-04-24 - Changed date format to YYYY-MM-DD for versioning, bigger 30 | # integer denotes a newer version 31 | # - Fixed a bug that would treat False as an integral type but 32 | # encode it using the 'False' string, attempting to encode a 33 | # boolean now results in an error 34 | # - Fixed a bug where an integer value of 0 in a list or 35 | # dictionary resulted in a parse error while decoding 36 | # 2011-04-03 - Original release 37 | 38 | import sys 39 | 40 | _py3 = sys.version_info[0] == 3 41 | 42 | if _py3: 43 | _VALID_STRING_TYPES = (str,) 44 | _VALID_INT_TYPES = (int,) 45 | else: 46 | _VALID_STRING_TYPES = (str, unicode) # @UndefinedVariable 47 | _VALID_INT_TYPES = (int, long) # @UndefinedVariable 48 | 49 | _TYPE_INT = 1 50 | _TYPE_STRING = 2 51 | _TYPE_LIST = 3 52 | _TYPE_DICTIONARY = 4 53 | _TYPE_END = 5 54 | _TYPE_INVALID = 6 55 | 56 | # Function to determine the type of he next value/item 57 | # Arguments: 58 | # char First character of the string that is to be decoded 59 | # Return value: 60 | # Returns an integer that describes what type the next value/item is 61 | 62 | 63 | def _gettype(char): 64 | if not isinstance(char, int): 65 | char = ord(char) 66 | if char == 0x6C: # 'l' 67 | return _TYPE_LIST 68 | elif char == 0x64: # 'd' 69 | return _TYPE_DICTIONARY 70 | elif char == 0x69: # 'i' 71 | return _TYPE_INT 72 | elif char == 0x65: # 'e' 73 | return _TYPE_END 74 | elif char >= 0x30 and char <= 0x39: # '0' '9' 75 | return _TYPE_STRING 76 | else: 77 | return _TYPE_INVALID 78 | 79 | 80 | # Function to parse a string from the bendcoded data 81 | # Arguments: 82 | # data bencoded data, must be guaranteed to be a string 83 | # Return Value: 84 | # Returns a tuple, the first member of the tuple is the parsed string 85 | # The second member is whatever remains of the bencoded data so it can 86 | # be used to parse the next part of the data 87 | 88 | 89 | def _decode_string(data): 90 | end = 1 91 | # if py3, data[end] is going to be an int 92 | # if py2, data[end] will be a string 93 | if _py3: 94 | char = 0x3A 95 | else: 96 | char = chr(0x3A) 97 | 98 | while data[end] != char: # ':' 99 | end = end + 1 100 | strlen = int(data[:end]) 101 | return (data[end + 1 : strlen + end + 1], data[strlen + end + 1 :]) 102 | 103 | 104 | # Function to parse an integer from the bencoded data 105 | # Arguments: 106 | # data bencoded data, must be guaranteed to be an integer 107 | # Return Value: 108 | # Returns a tuple, the first member of the tuple is the parsed string 109 | # The second member is whatever remains of the bencoded data so it can 110 | # be used to parse the next part of the data 111 | 112 | 113 | def _decode_int(data): 114 | end = 1 115 | # if py3, data[end] is going to be an int 116 | # if py2, data[end] will be a string 117 | if _py3: 118 | char = 0x65 119 | else: 120 | char = chr(0x65) 121 | 122 | while data[end] != char: # 'e' 123 | end = end + 1 124 | return (int(data[1:end]), data[end + 1 :]) 125 | 126 | 127 | # Function to parse a bencoded list 128 | # Arguments: 129 | # data bencoded data, must be guaranted to be the start of a list 130 | # Return Value: 131 | # Returns a tuple, the first member of the tuple is the parsed list 132 | # The second member is whatever remains of the bencoded data so it can 133 | # be used to parse the next part of the data 134 | 135 | 136 | def _decode_list(data): 137 | x = [] 138 | overflow = data[1:] 139 | while True: # Loop over the data 140 | if ( 141 | _gettype(overflow[0]) == _TYPE_END 142 | ): # - Break if we reach the end of the list 143 | return (x, overflow[1:]) # and return the list and overflow 144 | 145 | value, overflow = _decode(overflow) # 146 | if isinstance(value, bool) or overflow == "": # - if we have a parse error 147 | return (False, False) # Die with error 148 | else: # - Otherwise 149 | x.append(value) # add the value to the list 150 | 151 | 152 | # Function to parse a bencoded list 153 | # Arguments: 154 | # data bencoded data, must be guaranted to be the start of a list 155 | # Return Value: 156 | # Returns a tuple, the first member of the tuple is the parsed dictionary 157 | # The second member is whatever remains of the bencoded data so it can 158 | # be used to parse the next part of the data 159 | def _decode_dict(data): 160 | x = {} 161 | overflow = data[1:] 162 | while True: # Loop over the data 163 | if _gettype(overflow[0]) != _TYPE_STRING: # - If the key is not a string 164 | return (False, False) # Die with error 165 | key, overflow = _decode(overflow) # 166 | if key == False or overflow == "": # - If parse error 167 | return (False, False) # Die with error 168 | value, overflow = _decode(overflow) # 169 | if isinstance(value, bool) or overflow == "": # - If parse error 170 | print("Error parsing value") 171 | print(value) 172 | print(overflow) 173 | return (False, False) # Die with error 174 | else: 175 | # don't use bytes for the key 176 | key = key.decode() 177 | x[key] = value 178 | if _gettype(overflow[0]) == _TYPE_END: 179 | return (x, overflow[1:]) 180 | 181 | 182 | # Arguments: 183 | # data bencoded data in bytes format 184 | # Return Values: 185 | # Returns a tuple, the first member is the parsed data, could be a string, 186 | # an integer, a list or a dictionary, or a combination of those 187 | # The second member is the leftover of parsing, if everything parses correctly this 188 | # should be an empty byte string 189 | 190 | 191 | def _decode(data): 192 | btype = _gettype(data[0]) 193 | if btype == _TYPE_INT: 194 | return _decode_int(data) 195 | elif btype == _TYPE_STRING: 196 | return _decode_string(data) 197 | elif btype == _TYPE_LIST: 198 | return _decode_list(data) 199 | elif btype == _TYPE_DICTIONARY: 200 | return _decode_dict(data) 201 | else: 202 | return (False, False) 203 | 204 | 205 | # Function to decode bencoded data 206 | # Arguments: 207 | # data bencoded data, can be str or bytes 208 | # Return Values: 209 | # Returns the decoded data on success, this coud be bytes, int, dict or list 210 | # or a combinatin of those 211 | # If an error occurs the return value is False 212 | 213 | 214 | def decode(data): 215 | # if isinstance(data, str): 216 | # data = data.encode() 217 | decoded, overflow = _decode(data) 218 | return decoded 219 | 220 | 221 | # Args: data as integer 222 | # return: encoded byte string 223 | 224 | 225 | def _encode_int(data): 226 | return b"i" + str(data).encode() + b"e" 227 | 228 | 229 | # Args: data as string or bytes 230 | # Return: encoded byte string 231 | 232 | 233 | def _encode_string(data): 234 | return str(len(data)).encode() + b":" + data 235 | 236 | 237 | # Args: data as list 238 | # Return: Encoded byte string, false on error 239 | 240 | 241 | def _encode_list(data): 242 | elist = b"l" 243 | for item in data: 244 | eitem = encode(item) 245 | if eitem == False: 246 | return False 247 | elist += eitem 248 | return elist + b"e" 249 | 250 | 251 | # Args: data as dict 252 | # Return: encoded byte string, false on error 253 | 254 | 255 | def _encode_dict(data): 256 | edict = b"d" 257 | keys = [] 258 | for key in data: 259 | if not isinstance(key, _VALID_STRING_TYPES) and not isinstance(key, bytes): 260 | return False 261 | keys.append(key) 262 | keys.sort() 263 | for key in keys: 264 | ekey = encode(key) 265 | eitem = encode(data[key]) 266 | if ekey == False or eitem == False: 267 | return False 268 | edict += ekey + eitem 269 | return edict + b"e" 270 | 271 | 272 | # Function to encode a variable in bencoding 273 | # Arguments: 274 | # data Variable to be encoded, can be a list, dict, str, bytes, int or a combination of those 275 | # Return Values: 276 | # Returns the encoded data as a byte string when successful 277 | # If an error occurs the return value is False 278 | 279 | 280 | def encode(data): 281 | if isinstance(data, bool): 282 | return False 283 | elif isinstance(data, _VALID_INT_TYPES): 284 | return _encode_int(data) 285 | elif isinstance(data, bytes): 286 | return _encode_string(data) 287 | elif isinstance(data, _VALID_STRING_TYPES): 288 | return _encode_string(data.encode()) 289 | elif isinstance(data, list): 290 | return _encode_list(data) 291 | elif isinstance(data, dict): 292 | return _encode_dict(data) 293 | else: 294 | return False 295 | -------------------------------------------------------------------------------- /rtorrent_rpc/torrent.py: -------------------------------------------------------------------------------- 1 | import rtorrent_rpc.rpc 2 | 3 | import rtorrent_rpc.peer 4 | import rtorrent_rpc.tracker 5 | import rtorrent_rpc.file 6 | 7 | from rtorrent_rpc.common import safe_repr 8 | 9 | Peer = rtorrent_rpc.peer.Peer 10 | Tracker = rtorrent_rpc.tracker.Tracker 11 | File = rtorrent_rpc.file.File 12 | Method = rtorrent_rpc.rpc.Method 13 | 14 | 15 | class Torrent: 16 | """Represents an individual torrent within a L{RTorrent} instance.""" 17 | 18 | def __init__(self, _rt_obj, info_hash, **kwargs): 19 | self._rt_obj = _rt_obj 20 | self.info_hash = info_hash # : info hash for the torrent 21 | self.rpc_id = self.info_hash # : unique id to pass to rTorrent 22 | for k in kwargs.keys(): 23 | setattr(self, k, kwargs.get(k, None)) 24 | 25 | self.peers = [] 26 | self.trackers = [] 27 | self.files = [] 28 | 29 | self._call_custom_methods() 30 | 31 | def __repr__(self): 32 | return safe_repr( 33 | 'Torrent(info_hash="{0}" name="{1}")', self.info_hash, self.name 34 | ) 35 | 36 | def _call_custom_methods(self): 37 | """only calls methods that check instance variables.""" 38 | self._is_hash_checking_queued() 39 | self._is_started() 40 | self._is_paused() 41 | 42 | def get_peers(self): 43 | """Get list of Peer instances for given torrent. 44 | 45 | @return: L{Peer} instances 46 | @rtype: list 47 | 48 | @note: also assigns return value to self.peers 49 | """ 50 | self.peers = [] 51 | retriever_methods = [ 52 | m 53 | for m in rtorrent_rpc.peer.methods 54 | if m.is_retriever() and m.is_available(self._rt_obj) 55 | ] 56 | # need to leave 2nd arg empty (dunno why) 57 | m = rtorrent_rpc.rpc.Multicall(self) 58 | m.add( 59 | "p.multicall", 60 | self.info_hash, 61 | "", 62 | *[method.rpc_call + "=" for method in retriever_methods], 63 | ) 64 | 65 | results = m.call()[0] # only sent one call, only need first result 66 | 67 | for result in results: 68 | results_dict = {} 69 | # build results_dict 70 | for m, r in zip(retriever_methods, result): 71 | results_dict[m.varname] = rtorrent_rpc.rpc.process_result(m, r) 72 | 73 | self.peers.append(Peer(self._rt_obj, self.info_hash, **results_dict)) 74 | 75 | return self.peers 76 | 77 | def get_trackers(self): 78 | """Get list of Tracker instances for given torrent. 79 | 80 | @return: L{Tracker} instances 81 | @rtype: list 82 | 83 | @note: also assigns return value to self.trackers 84 | """ 85 | self.trackers = [] 86 | retriever_methods = [ 87 | m 88 | for m in rtorrent_rpc.tracker.methods 89 | if m.is_retriever() and m.is_available(self._rt_obj) 90 | ] 91 | 92 | # need to leave 2nd arg empty (dunno why) 93 | m = rtorrent_rpc.rpc.Multicall(self) 94 | m.add( 95 | "t.multicall", 96 | self.info_hash, 97 | "", 98 | *[method.rpc_call + "=" for method in retriever_methods], 99 | ) 100 | 101 | results = m.call()[0] # only sent one call, only need first result 102 | 103 | for result in results: 104 | results_dict = {} 105 | # build results_dict 106 | for m, r in zip(retriever_methods, result): 107 | results_dict[m.varname] = rtorrent_rpc.rpc.process_result(m, r) 108 | 109 | self.trackers.append(Tracker(self._rt_obj, self.info_hash, **results_dict)) 110 | 111 | return self.trackers 112 | 113 | def get_files(self): 114 | """Get list of File instances for given torrent. 115 | 116 | @return: L{File} instances 117 | @rtype: list 118 | 119 | @note: also assigns return value to self.files 120 | """ 121 | 122 | self.files = [] 123 | retriever_methods = [ 124 | m 125 | for m in rtorrent_rpc.file.methods 126 | if m.is_retriever() and m.is_available(self._rt_obj) 127 | ] 128 | # 2nd arg can be anything, but it'll return all files in torrent 129 | # regardless 130 | m = rtorrent_rpc.rpc.Multicall(self) 131 | m.add( 132 | "f.multicall", 133 | self.info_hash, 134 | "", 135 | *[method.rpc_call + "=" for method in retriever_methods], 136 | ) 137 | 138 | results = m.call()[0] # only sent one call, only need first result 139 | 140 | offset_method_index = retriever_methods.index( 141 | rtorrent_rpc.rpc.find_method("f.offset") 142 | ) 143 | 144 | # make a list of the offsets of all the files, sort appropriately 145 | offset_list = sorted([r[offset_method_index] for r in results]) 146 | 147 | for result in results: 148 | results_dict = {} 149 | # build results_dict 150 | for m, r in zip(retriever_methods, result): 151 | results_dict[m.varname] = rtorrent_rpc.rpc.process_result(m, r) 152 | 153 | # get proper index positions for each file (based on the file 154 | # offset) 155 | f_index = offset_list.index(results_dict["offset"]) 156 | 157 | self.files.append( 158 | File(self._rt_obj, self.info_hash, f_index, **results_dict) 159 | ) 160 | 161 | return self.files 162 | 163 | def get_state(self): 164 | m = rtorrent_rpc.rpc.Multicall(self) 165 | self.multicall_add(m, "d.state") 166 | 167 | return m.call()[-1] 168 | 169 | def set_directory(self, d): 170 | """Modify download directory 171 | 172 | @note: Needs to stop torrent in order to change the directory. 173 | Also doesn't restart after directory is set, that must be called 174 | separately. 175 | """ 176 | m = rtorrent_rpc.rpc.Multicall(self) 177 | self.multicall_add(m, "d.try_stop") 178 | self.multicall_add(m, "d.directory.set", d) 179 | 180 | self.directory = m.call()[-1] 181 | 182 | def set_directory_base(self, d): 183 | """Modify base download directory 184 | 185 | @note: Needs to stop torrent in order to change the directory. 186 | Also doesn't restart after directory is set, that must be called 187 | separately. 188 | """ 189 | m = rtorrent_rpc.rpc.Multicall(self) 190 | self.multicall_add(m, "d.try_stop") 191 | self.multicall_add(m, "d.directory_base.set", d) 192 | 193 | def start(self): 194 | """Start the torrent""" 195 | m = rtorrent_rpc.rpc.Multicall(self) 196 | self.multicall_add(m, "d.try_start") 197 | self.multicall_add(m, "d.is_active") 198 | 199 | self.active = m.call()[-1] 200 | return self.active 201 | 202 | def stop(self): 203 | """"Stop the torrent""" 204 | m = rtorrent_rpc.rpc.Multicall(self) 205 | self.multicall_add(m, "d.try_stop") 206 | self.multicall_add(m, "d.is_active") 207 | 208 | self.active = m.call()[-1] 209 | return self.active 210 | 211 | def pause(self): 212 | """Pause the torrent""" 213 | m = rtorrent_rpc.rpc.Multicall(self) 214 | self.multicall_add(m, "d.pause") 215 | 216 | return m.call()[-1] 217 | 218 | def resume(self): 219 | """Resume the torrent""" 220 | m = rtorrent_rpc.rpc.Multicall(self) 221 | self.multicall_add(m, "d.resume") 222 | 223 | return m.call()[-1] 224 | 225 | def close(self): 226 | """Close the torrent and it's files""" 227 | m = rtorrent_rpc.rpc.Multicall(self) 228 | self.multicall_add(m, "d.close") 229 | 230 | return m.call()[-1] 231 | 232 | def erase(self): 233 | """Delete the torrent 234 | 235 | @note: doesn't delete the downloaded files""" 236 | m = rtorrent_rpc.rpc.Multicall(self) 237 | self.multicall_add(m, "d.erase") 238 | 239 | return m.call()[-1] 240 | 241 | def check_hash(self): 242 | """(Re)hash check the torrent""" 243 | m = rtorrent_rpc.rpc.Multicall(self) 244 | self.multicall_add(m, "d.check_hash") 245 | 246 | return m.call()[-1] 247 | 248 | def poll(self): 249 | """poll rTorrent to get latest peer/tracker/file information""" 250 | self.get_peers() 251 | self.get_trackers() 252 | self.get_files() 253 | 254 | def update(self): 255 | """Refresh torrent data 256 | 257 | @note: All fields are stored as attributes to self. 258 | 259 | @return: None 260 | """ 261 | multicall = rtorrent_rpc.rpc.Multicall(self) 262 | retriever_methods = [ 263 | m for m in methods if m.is_retriever() and m.is_available(self._rt_obj) 264 | ] 265 | for method in retriever_methods: 266 | multicall.add(method, self.rpc_id) 267 | 268 | multicall.call() 269 | 270 | # custom functions (only call private methods, since they only check 271 | # local variables and are therefore faster) 272 | self._call_custom_methods() 273 | 274 | def accept_seeders(self, accept_seeds): 275 | """Enable/disable whether the torrent connects to seeders 276 | 277 | @param accept_seeds: enable/disable accepting seeders 278 | @type accept_seeds: bool""" 279 | if accept_seeds: 280 | call = "d.accepting_seeders.enable" 281 | else: 282 | call = "d.accepting_seeders.disable" 283 | 284 | m = rtorrent_rpc.rpc.Multicall(self) 285 | self.multicall_add(m, call) 286 | 287 | return m.call()[-1] 288 | 289 | def announce(self): 290 | """Announce torrent info to tracker(s)""" 291 | m = rtorrent_rpc.rpc.Multicall(self) 292 | self.multicall_add(m, "d.tracker_announce") 293 | 294 | return m.call()[-1] 295 | 296 | @staticmethod 297 | def _assert_custom_key_valid(key): 298 | assert ( 299 | type(key) == int and key > 0 and key < 6 300 | ), "key must be an integer between 1-5" 301 | 302 | def get_custom(self, key): 303 | """ 304 | Get custom value 305 | 306 | @param key: the index for the custom field (between 1-5) 307 | @type key: int 308 | 309 | @rtype: str 310 | """ 311 | 312 | self._assert_custom_key_valid(key) 313 | m = rtorrent_rpc.rpc.Multicall(self) 314 | 315 | field = "custom{0}".format(key) 316 | self.multicall_add(m, "d.{0}".format(field)) 317 | setattr(self, field, m.call()[-1]) 318 | 319 | return getattr(self, field) 320 | 321 | def set_custom(self, key, value): 322 | """ 323 | Set custom value 324 | 325 | @param key: the index for the custom field (between 1-5) 326 | @type key: int 327 | 328 | @param value: the value to be stored 329 | @type value: str 330 | 331 | @return: if successful, value will be returned 332 | @rtype: str 333 | """ 334 | 335 | self._assert_custom_key_valid(key) 336 | m = rtorrent_rpc.rpc.Multicall(self) 337 | 338 | self.multicall_add(m, "d.custom{0}.set".format(key), value) 339 | 340 | return m.call()[-1] 341 | 342 | def set_visible(self, view, visible=True): 343 | p = self._rt_obj._get_conn() 344 | 345 | if visible: 346 | return p.view.set_visible(self.info_hash, view) 347 | else: 348 | return p.view.set_not_visible(self.info_hash, view) 349 | 350 | ############################################################################ 351 | # CUSTOM METHODS (Not part of the official rTorrent API) 352 | ########################################################################## 353 | def _is_hash_checking_queued(self): 354 | """Only checks instance variables, shouldn't be called directly""" 355 | # if hashing == 3, then torrent is marked for hash checking 356 | # if hash_checking == False, then torrent is waiting to be checked 357 | self.hash_checking_queued = self.hashing == 3 and self.is_hash_checking is False 358 | 359 | return self.hash_checking_queued 360 | 361 | def is_hash_checking_queued(self): 362 | """Check if torrent is waiting to be hash checked 363 | 364 | @note: Variable where the result for this method is stored 365 | Torrent.hash_checking_queued 366 | """ 367 | m = rtorrent_rpc.rpc.Multicall(self) 368 | self.multicall_add(m, "d.hashing") 369 | self.multicall_add(m, "d.is_hash_checking") 370 | results = m.call() 371 | 372 | setattr(self, "hashing", results[0]) 373 | setattr(self, "hash_checking", results[1]) 374 | 375 | return self._is_hash_checking_queued() 376 | 377 | def _is_paused(self): 378 | """Only checks instance variables, shouldn't be called directly""" 379 | self.paused = self.state == 0 380 | return self.paused 381 | 382 | def is_paused(self): 383 | """Check if torrent is paused 384 | 385 | @note: Variable where the result for this method is stored: Torrent.paused""" 386 | self.get_state() 387 | return self._is_paused() 388 | 389 | def _is_started(self): 390 | """Only checks instance variables, shouldn't be called directly""" 391 | self.started = self.state == 1 392 | return self.started 393 | 394 | def is_started(self): 395 | """Check if torrent is started 396 | 397 | @note: Variable where the result for this method is stored: Torrent.started""" 398 | self.get_state() 399 | return self._is_started() 400 | 401 | 402 | methods = [ 403 | # RETRIEVERS 404 | Method(Torrent, "is_hash_checked", "d.is_hash_checked", boolean=True,), 405 | Method(Torrent, "is_hash_checking", "d.is_hash_checking", boolean=True,), 406 | Method(Torrent, "get_peers_max", "d.peers_max"), 407 | Method(Torrent, "get_tracker_focus", "d.tracker_focus"), 408 | Method(Torrent, "get_skip_total", "d.skip.total"), 409 | Method(Torrent, "get_state", "d.state"), 410 | Method(Torrent, "get_peer_exchange", "d.peer_exchange"), 411 | Method(Torrent, "get_down_rate", "d.down.rate"), 412 | Method(Torrent, "get_connection_seed", "d.connection_seed"), 413 | Method(Torrent, "get_uploads_max", "d.uploads_max"), 414 | Method(Torrent, "get_priority_str", "d.priority_str"), 415 | Method(Torrent, "is_open", "d.is_open", boolean=True,), 416 | Method(Torrent, "get_peers_min", "d.peers_min"), 417 | Method(Torrent, "get_peers_complete", "d.peers_complete"), 418 | Method(Torrent, "get_tracker_numwant", "d.tracker_numwant"), 419 | Method(Torrent, "get_connection_current", "d.connection_current"), 420 | Method(Torrent, "is_complete", "d.complete", boolean=True,), 421 | Method(Torrent, "get_peers_connected", "d.peers_connected"), 422 | Method(Torrent, "get_chunk_size", "d.chunk_size"), 423 | Method(Torrent, "get_state_counter", "d.state_counter"), 424 | Method(Torrent, "get_base_filename", "d.base_filename"), 425 | Method(Torrent, "get_state_changed", "d.state_changed"), 426 | Method(Torrent, "get_peers_not_connected", "d.peers_not_connected"), 427 | Method(Torrent, "get_directory", "d.directory"), 428 | Method(Torrent, "is_incomplete", "d.incomplete", boolean=True,), 429 | Method(Torrent, "get_tracker_size", "d.tracker_size"), 430 | Method(Torrent, "is_multi_file", "d.is_multi_file", boolean=True,), 431 | Method(Torrent, "get_local_id", "d.local_id"), 432 | Method(Torrent, "get_ratio", "d.ratio", post_process_func=lambda x: x / 1000.0,), 433 | Method(Torrent, "get_loaded_file", "d.loaded_file"), 434 | Method(Torrent, "get_max_file_size", "d.max_file_size"), 435 | Method(Torrent, "get_size_chunks", "d.size_chunks"), 436 | Method(Torrent, "is_pex_active", "d.is_pex_active", boolean=True,), 437 | Method(Torrent, "get_hashing", "d.hashing"), 438 | Method(Torrent, "get_bitfield", "d.bitfield"), 439 | Method(Torrent, "get_local_id_html", "d.local_id_html"), 440 | Method(Torrent, "get_connection_leech", "d.connection_leech"), 441 | Method(Torrent, "get_peers_accounted", "d.peers_accounted"), 442 | Method(Torrent, "get_message", "d.message"), 443 | Method(Torrent, "is_active", "d.is_active", boolean=True,), 444 | Method(Torrent, "get_size_bytes", "d.size_bytes"), 445 | Method(Torrent, "get_ignore_commands", "d.ignore_commands"), 446 | Method(Torrent, "get_creation_date", "d.creation_date"), 447 | Method(Torrent, "get_base_path", "d.base_path"), 448 | Method(Torrent, "get_left_bytes", "d.left_bytes"), 449 | Method(Torrent, "get_size_files", "d.size_files"), 450 | Method(Torrent, "get_size_pex", "d.size_pex"), 451 | Method(Torrent, "is_private", "d.is_private", boolean=True,), 452 | Method(Torrent, "get_max_size_pex", "d.max_size_pex"), 453 | Method( 454 | Torrent, 455 | "get_num_chunks_hashed", 456 | "d.chunks_hashed", 457 | aliases=("get_chunks_hashed",), 458 | ), 459 | Method(Torrent, "get_num_chunks_wanted", "d.wanted_chunks"), 460 | Method(Torrent, "get_priority", "d.priority"), 461 | Method(Torrent, "get_skip_rate", "d.skip.rate"), 462 | Method(Torrent, "get_completed_bytes", "d.completed_bytes"), 463 | Method(Torrent, "get_name", "d.name"), 464 | Method(Torrent, "get_completed_chunks", "d.completed_chunks"), 465 | Method(Torrent, "get_throttle_name", "d.throttle_name"), 466 | Method(Torrent, "get_free_diskspace", "d.free_diskspace"), 467 | Method(Torrent, "get_directory_base", "d.directory_base"), 468 | Method(Torrent, "get_hashing_failed", "d.hashing_failed"), 469 | Method(Torrent, "get_tied_to_file", "d.tied_to_file"), 470 | Method(Torrent, "get_down_total", "d.down.total"), 471 | Method(Torrent, "get_bytes_done", "d.bytes_done"), 472 | Method(Torrent, "get_up_rate", "d.up.rate"), 473 | Method(Torrent, "get_up_total", "d.up.total"), 474 | Method(Torrent, "is_accepting_seeders", "d.accepting_seeders", boolean=True,), 475 | Method(Torrent, "get_chunks_seen", "d.chunks_seen", min_version=(0, 9, 1),), 476 | Method(Torrent, "is_partially_done", "d.is_partially_done", boolean=True,), 477 | Method(Torrent, "is_not_partially_done", "d.is_not_partially_done", boolean=True,), 478 | Method(Torrent, "get_time_started", "d.timestamp.started"), 479 | Method(Torrent, "get_custom1", "d.custom1"), 480 | Method(Torrent, "get_custom2", "d.custom2"), 481 | Method(Torrent, "get_custom3", "d.custom3"), 482 | Method(Torrent, "get_custom4", "d.custom4"), 483 | Method(Torrent, "get_custom5", "d.custom5"), 484 | # MODIFIERS 485 | Method(Torrent, "set_uploads_max", "d.uploads_max.set"), 486 | Method(Torrent, "set_tied_to_file", "d.tied_to_file.set"), 487 | Method(Torrent, "set_tracker_numwant", "d.tracker_numwant.set"), 488 | Method(Torrent, "set_priority", "d.priority.set"), 489 | Method(Torrent, "set_peers_max", "d.peers_max.set"), 490 | Method(Torrent, "set_hashing_failed", "d.hashing_failed.set"), 491 | Method(Torrent, "set_message", "d.message.set"), 492 | Method(Torrent, "set_throttle_name", "d.throttle_name.set"), 493 | Method(Torrent, "set_peers_min", "d.peers_min.set"), 494 | Method(Torrent, "set_ignore_commands", "d.ignore_commands.set"), 495 | Method(Torrent, "set_max_file_size", "d.max_file_size.set"), 496 | Method(Torrent, "set_custom5", "d.custom5.set"), 497 | Method(Torrent, "set_custom4", "d.custom4.set"), 498 | Method(Torrent, "set_custom2", "d.custom2.set"), 499 | Method(Torrent, "set_custom1", "d.custom1.set"), 500 | Method(Torrent, "set_custom3", "d.custom3.set"), 501 | Method(Torrent, "set_connection_current", "d.connection_current.set"), 502 | ] 503 | -------------------------------------------------------------------------------- /rtorrent_rpc/__init__.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlparse 2 | import os.path 3 | import time 4 | import xmlrpc.client 5 | 6 | from rtorrent_rpc.common import is_valid_port, convert_version_tuple_to_str 7 | from rtorrent_rpc.lib.torrentparser import TorrentParser 8 | from rtorrent_rpc.lib.xmlrpc.http import HTTPServerProxy 9 | from rtorrent_rpc.lib.xmlrpc.scgi import SCGIServerProxy 10 | from rtorrent_rpc.rpc import Method 11 | from rtorrent_rpc.lib.xmlrpc.requests_transport import RequestsTransport 12 | from rtorrent_rpc.torrent import Torrent 13 | from rtorrent_rpc.group import Group 14 | import rtorrent_rpc.rpc 15 | 16 | MIN_RTORRENT_VERSION = (0, 9, 0) 17 | MIN_RTORRENT_VERSION_STR = convert_version_tuple_to_str(MIN_RTORRENT_VERSION) 18 | 19 | 20 | class RTorrent: 21 | 22 | """ Create a new rTorrent connection """ 23 | 24 | def __init__( 25 | self, 26 | uri, 27 | username=None, 28 | password=None, 29 | verify=False, 30 | sp=None, 31 | sp_kwargs=None, 32 | tp_kwargs=None, 33 | ): 34 | self.uri = uri # : From X{__init__(self, url)} 35 | 36 | self.username = username 37 | self.password = password 38 | 39 | self.schema = urlparse(uri).scheme 40 | 41 | if sp: 42 | self.sp = sp 43 | elif self.schema in ["http", "https"]: 44 | self.sp = HTTPServerProxy 45 | if self.schema == "https": 46 | self.isHttps = True 47 | else: 48 | self.isHttps = False 49 | elif self.schema == "scgi": 50 | self.sp = SCGIServerProxy 51 | else: 52 | raise NotImplementedError() 53 | 54 | self.sp_kwargs = sp_kwargs or {} 55 | 56 | self.tp_kwargs = tp_kwargs or {} 57 | 58 | self.torrents = [] # : List of L{Torrent} instances 59 | self._rpc_methods = [] # : List of rTorrent RPC methods 60 | self._torrent_cache = [] 61 | self._client_version_tuple = () 62 | 63 | if verify is True: 64 | self._verify_conn() 65 | 66 | def _get_conn(self): 67 | """Get ServerProxy instance""" 68 | if self.username is not None and self.password is not None: 69 | if self.schema == "scgi": 70 | raise NotImplementedError() 71 | 72 | if "authtype" not in self.tp_kwargs: 73 | authtype = None 74 | else: 75 | authtype = self.tp_kwargs["authtype"] 76 | 77 | if "check_ssl_cert" not in self.tp_kwargs: 78 | check_ssl_cert = True 79 | else: 80 | check_ssl_cert = self.tp_kwargs["check_ssl_cert"] 81 | 82 | if "proxies" not in self.tp_kwargs: 83 | proxies = None 84 | else: 85 | proxies = self.tp_kwargs["proxies"] 86 | 87 | return self.sp( 88 | self.uri, 89 | transport=RequestsTransport( 90 | use_https=self.isHttps, 91 | authtype=authtype, 92 | username=self.username, 93 | password=self.password, 94 | check_ssl_cert=check_ssl_cert, 95 | proxies=proxies, 96 | ), 97 | **self.sp_kwargs, 98 | ) 99 | 100 | return self.sp(self.uri, **self.sp_kwargs) 101 | 102 | def _verify_conn(self): 103 | # check for rpc methods that should be available 104 | assert ( 105 | "system.client_version" in self._get_rpc_methods() 106 | ), "Required RPC method not available." 107 | assert ( 108 | "system.library_version" in self._get_rpc_methods() 109 | ), "Required RPC method not available." 110 | 111 | # minimum rTorrent version check 112 | assert ( 113 | self._meets_version_requirement() is True 114 | ), "Error: Minimum rTorrent version required is {0}".format( 115 | MIN_RTORRENT_VERSION_STR 116 | ) 117 | 118 | def _meets_version_requirement(self): 119 | return self._get_client_version_tuple() >= MIN_RTORRENT_VERSION 120 | 121 | def _get_client_version_tuple(self): 122 | conn = self._get_conn() 123 | 124 | if not self._client_version_tuple: 125 | if not hasattr(self, "client_version"): 126 | setattr(self, "client_version", conn.system.client_version()) 127 | 128 | rtver = getattr(self, "client_version") 129 | self._client_version_tuple = tuple([int(i) for i in rtver.split(".")]) 130 | 131 | return self._client_version_tuple 132 | 133 | def _update_rpc_methods(self): 134 | self._rpc_methods = self._get_conn().system.listMethods() 135 | 136 | return self._rpc_methods 137 | 138 | def _get_rpc_methods(self): 139 | """ Get list of raw RPC commands 140 | 141 | @return: raw RPC commands 142 | @rtype: list 143 | """ 144 | 145 | return self._rpc_methods or self._update_rpc_methods() 146 | 147 | def get_torrents(self, view="main"): 148 | """Get list of all torrents in specified view 149 | 150 | @return: list of L{Torrent} instances 151 | 152 | @rtype: list 153 | 154 | @todo: add validity check for specified view 155 | """ 156 | self.torrents = [] 157 | methods = rtorrent_rpc.torrent.methods 158 | retriever_methods = [ 159 | m for m in methods if m.is_retriever() and m.is_available(self) 160 | ] 161 | 162 | m = rtorrent_rpc.rpc.Multicall(self) 163 | m.add( 164 | "d.multicall2", 165 | "", 166 | view, 167 | "d.hash=", 168 | *[method.rpc_call + "=" for method in retriever_methods], 169 | ) 170 | 171 | results = m.call()[0] # only sent one call, only need first result 172 | 173 | for result in results: 174 | results_dict = {} 175 | # build results_dict 176 | # result[0] is the info_hash 177 | for m, r in zip(retriever_methods, result[1:]): 178 | results_dict[m.varname] = rtorrent_rpc.rpc.process_result(m, r) 179 | 180 | self.torrents.append(Torrent(self, info_hash=result[0], **results_dict)) 181 | 182 | self._manage_torrent_cache() 183 | return self.torrents 184 | 185 | def _manage_torrent_cache(self): 186 | """Carry tracker/peer/file lists over to new torrent list""" 187 | for torrent in self._torrent_cache: 188 | new_torrent = rtorrent_rpc.common.find_torrent( 189 | torrent.info_hash, self.torrents 190 | ) 191 | if new_torrent is not None: 192 | new_torrent.files = torrent.files 193 | new_torrent.peers = torrent.peers 194 | new_torrent.trackers = torrent.trackers 195 | 196 | self._torrent_cache = self.torrents 197 | 198 | def _get_load_function(self, file_type, start, verbose): 199 | """Determine correct "load torrent" RPC method""" 200 | func_name = None 201 | if file_type == "url": 202 | # url strings can be input directly 203 | if start and verbose: 204 | func_name = "load.start_verbose" 205 | elif start: 206 | func_name = "load.start" 207 | elif verbose: 208 | func_name = "load.verbose" 209 | else: 210 | func_name = "load.normal" 211 | elif file_type in ["file", "raw"]: 212 | if start and verbose: 213 | func_name = "load.raw_start_verbose" 214 | elif start: 215 | func_name = "load.raw_start" 216 | elif verbose: 217 | func_name = "load.raw_verbose" 218 | else: 219 | func_name = "load.raw" 220 | 221 | return func_name 222 | 223 | def load_magnet(self, magneturl, info_hash, **kwargs): 224 | start = kwargs.pop("start", False) 225 | verbose = kwargs.pop("verbose", False) 226 | verify_load = kwargs.pop("verify_load", True) 227 | max_retries = kwargs.pop("max_retries", 5) 228 | params = kwargs.pop("params", []) 229 | target = kwargs.pop("target", "") 230 | 231 | p = self._get_conn() 232 | 233 | info_hash = info_hash.upper() 234 | 235 | func_name = self._get_load_function("url", start, verbose) 236 | 237 | # load magnet 238 | getattr(p, func_name)(target, magneturl, *params) 239 | 240 | if verify_load: 241 | i = 0 242 | while i < max_retries: 243 | for torrent in self.get_torrents(): 244 | if torrent.info_hash == info_hash: 245 | return True 246 | time.sleep(1) 247 | i += 1 248 | return False 249 | 250 | return True 251 | 252 | def load_torrent(self, torrent, **kwargs): 253 | """ 254 | Load torrent into rTorrent (with various enhancements). 255 | @param torrent: can be a url, a path to a local file, or the raw data 256 | of a torrent file 257 | @type torrent: str 258 | @param start: start torrent when loaded 259 | @type start: bool 260 | @param verbose: print error messages to rTorrent log 261 | @type verbose: bool 262 | @param verify_load: verify that torrent was added to rTorrent successfully 263 | @type verify_load: bool 264 | @return: Depends on verify_load: 265 | - if verify_load is True, (and the torrent was 266 | loaded successfully), it'll return a L{Torrent} instance 267 | - if verify_load is False, it'll return None 268 | @rtype: L{Torrent} instance or None 269 | @raise AssertionError: If the torrent wasn't successfully added to rTorrent 270 | - Check L{TorrentParser} for the AssertionError's 271 | it raises 272 | @note: Because this function includes url verification (if a url was input) 273 | as well as verification as to whether the torrent was successfully added, 274 | this function doesn't execute instantaneously. If that's what you're 275 | looking for, use load_torrent_simple() instead. 276 | """ 277 | start = kwargs.pop("start", False) 278 | verbose = kwargs.pop("verbose", False) 279 | verify_load = kwargs.pop("verify_load", True) 280 | max_retries = kwargs.pop("max_retries", 5) 281 | params = kwargs.pop("params", []) 282 | target = kwargs.pop("target", "") 283 | 284 | p = self._get_conn() 285 | tp = TorrentParser(torrent) 286 | torrent = xmlrpc.client.Binary(tp._raw_torrent) 287 | info_hash = tp.info_hash 288 | 289 | func_name = self._get_load_function("raw", start, verbose) 290 | 291 | getattr(p, func_name)(target, torrent, *params) 292 | 293 | if verify_load: 294 | i = 0 295 | while i < max_retries: 296 | for torrent in self.get_torrents(): 297 | if torrent.info_hash == info_hash: 298 | return True 299 | time.sleep(1) 300 | i += 1 301 | return False 302 | 303 | return True 304 | 305 | def load_torrent_simple(self, torrent, file_type, **kwargs): 306 | """Load torrent into rTorrent. 307 | @param torrent: can be a url, a path to a local file, or the raw data 308 | of a torrent file 309 | @type torrent: str 310 | @param file_type: valid options: 'url', 'file', or 'raw' 311 | @type file_type: str 312 | @param start: start torrent when loaded 313 | @type start: bool 314 | @param verbose: print error messages to rTorrent log 315 | @type verbose: bool 316 | @return: None 317 | @raise AssertionError: if incorrect file_type is specified 318 | @note: This function was written for speed, it includes no enhancements. 319 | If you input a url, it won't check if it's valid. You also can't get 320 | verification that the torrent was successfully added to rTorrent. 321 | Use load_torrent() if you would like these features. 322 | """ 323 | start = kwargs.pop("start", False) 324 | verbose = kwargs.pop("verbose", False) 325 | params = kwargs.pop("params", []) 326 | target = kwargs.pop("target", "") 327 | 328 | p = self._get_conn() 329 | 330 | if file_type not in ["raw", "file", "url"]: 331 | print("Invalid file_type, options are: 'url', 'file', 'raw'.") 332 | return False 333 | 334 | func_name = self._get_load_function(file_type, start, verbose) 335 | 336 | if file_type == "file": 337 | # since we have to assume we're connected to a remote rTorrent 338 | # client, we have to read the file and send it to rT as raw 339 | if not os.path.isfile(torrent): 340 | print("Invalid path: {0}".format(torrent)) 341 | return False 342 | 343 | torrent = open(torrent, "rb").read() 344 | 345 | if file_type in ["raw", "file"]: 346 | finput = xmlrpc.client.Binary(torrent) 347 | elif file_type == "url": 348 | finput = torrent 349 | 350 | getattr(p, func_name)(target, finput, *params) 351 | 352 | return True 353 | 354 | def get_views(self): 355 | p = self._get_conn() 356 | return p.view_list() 357 | 358 | def create_group(self, name, persistent=True, view=None): 359 | p = self._get_conn() 360 | 361 | if persistent is True: 362 | p.group.insert_persistent_view("", name) 363 | elif view is not None: 364 | p.group.insert("", name, view) 365 | 366 | self._update_rpc_methods() 367 | 368 | def get_group(self, name): 369 | assert name is not None, "group name required" 370 | 371 | group = Group(self, name) 372 | group.update() 373 | return group 374 | 375 | def set_dht_port(self, port): 376 | """Set DHT port 377 | 378 | @param port: port 379 | @type port: int 380 | 381 | @raise AssertionError: if invalid port is given 382 | """ 383 | assert is_valid_port(port), "Valid port range is 0-65535" 384 | self.dht_port = self._p.set_dht_port(port) 385 | 386 | def enable_check_hash(self): 387 | """Alias for set_check_hash(True)""" 388 | self.set_check_hash(True) 389 | 390 | def disable_check_hash(self): 391 | """Alias for set_check_hash(False)""" 392 | self.set_check_hash(False) 393 | 394 | def find_torrent(self, info_hash): 395 | """Frontend for rtorrent_rpc.common.find_torrent""" 396 | return rtorrent_rpc.common.find_torrent(info_hash, self.get_torrents()) 397 | 398 | def poll(self): 399 | """ poll rTorrent to get latest torrent/peer/tracker/file information 400 | 401 | @note: This essentially refreshes every aspect of the rTorrent 402 | connection, so it can be very slow if working with a remote 403 | connection that has a lot of torrents loaded. 404 | 405 | @return: None 406 | """ 407 | self.update() 408 | torrents = self.get_torrents() 409 | for t in torrents: 410 | t.poll() 411 | 412 | def update(self): 413 | """Refresh rTorrent client info 414 | 415 | @note: All fields are stored as attributes to self. 416 | 417 | @return: None 418 | """ 419 | multicall = rtorrent_rpc.rpc.Multicall(self) 420 | retriever_methods = [ 421 | m for m in methods if m.is_retriever() and m.is_available(self) 422 | ] 423 | for method in retriever_methods: 424 | multicall.add(method) 425 | 426 | multicall.call() 427 | 428 | 429 | def _build_class_methods(class_obj): 430 | # multicall add class 431 | def caller(self, multicall, method, *args): 432 | multicall.add(method, self.rpc_id, *args) 433 | 434 | caller.__doc__ = """Same as Multicall.add(), but with automatic inclusion 435 | of the rpc_id 436 | 437 | @param multicall: A L{Multicall} instance 438 | @type: multicall: Multicall 439 | 440 | @param method: L{Method} instance or raw rpc method 441 | @type: Method or str 442 | 443 | @param args: optional arguments to pass 444 | """ 445 | setattr(class_obj, "multicall_add", caller) 446 | 447 | 448 | def __compare_rpc_methods(rt_new, rt_old): 449 | from pprint import pprint 450 | 451 | rt_new_methods = set(rt_new._get_rpc_methods()) 452 | rt_old_methods = set(rt_old._get_rpc_methods()) 453 | print("New Methods:") 454 | pprint(rt_new_methods - rt_old_methods) 455 | print("Methods not in new rTorrent:") 456 | pprint(rt_old_methods - rt_new_methods) 457 | 458 | 459 | def __check_supported_methods(rt): 460 | from pprint import pprint 461 | 462 | supported_methods = set( 463 | [ 464 | m.rpc_call 465 | for m in methods 466 | + rtorrent_rpc.file.methods 467 | + rtorrent_rpc.torrent.methods 468 | + rtorrent_rpc.tracker.methods 469 | + rtorrent_rpc.peer.methods 470 | ] 471 | ) 472 | all_methods = set(rt._get_rpc_methods()) 473 | 474 | print("Methods NOT in supported methods") 475 | pprint(all_methods - supported_methods) 476 | print("Supported methods NOT in all methods") 477 | pprint(supported_methods - all_methods) 478 | 479 | 480 | methods = [ 481 | # RETRIEVERS 482 | Method(RTorrent, "get_xmlrpc_size_limit", "network.xmlrpc.size_limit"), 483 | Method(RTorrent, "get_proxy_address", "network.proxy_address"), 484 | Method(RTorrent, "get_file_split_suffix", "system.file.split_suffix"), 485 | Method(RTorrent, "get_global_up_limit", "throttle.global_up.max_rate"), 486 | Method(RTorrent, "get_max_memory_usage", "pieces.memory.max"), 487 | Method(RTorrent, "get_max_open_files", "network.max_open_files"), 488 | Method(RTorrent, "get_min_peers_seed", "throttle.min_peers.seed"), 489 | Method(RTorrent, "get_use_udp_trackers", "trackers.use_udp"), 490 | Method(RTorrent, "get_preload_min_size", "pieces.preload.min_size"), 491 | Method(RTorrent, "get_max_uploads", "throttle.max_uploads"), 492 | Method(RTorrent, "get_max_peers", "throttle.max_peers.normal"), 493 | Method(RTorrent, "get_timeout_sync", "pieces.sync.timeout"), 494 | Method(RTorrent, "get_receive_buffer_size", "network.receive_buffer.size"), 495 | Method(RTorrent, "get_split_file_size", "system.file.split_size"), 496 | Method(RTorrent, "get_dht_throttle", "dht.throttle.name"), 497 | Method(RTorrent, "get_max_peers_seed", "throttle.max_peers.seed"), 498 | Method(RTorrent, "get_min_peers", "throttle.min_peers.normal"), 499 | Method(RTorrent, "get_tracker_numwant", "trackers.numwant"), 500 | Method(RTorrent, "get_max_open_sockets", "network.max_open_sockets"), 501 | Method(RTorrent, "get_session_path", "session.path"), 502 | Method(RTorrent, "get_local_address", "network.local_address"), 503 | Method(RTorrent, "get_scgi_dont_route", "network.scgi.dont_route"), 504 | Method(RTorrent, "get_http_cacert", "network.http.cacert"), 505 | Method(RTorrent, "get_dht_port", "dht.port"), 506 | Method(RTorrent, "get_preload_type", "pieces.preload.type"), 507 | Method(RTorrent, "get_http_max_open", "network.http.max_open"), 508 | Method(RTorrent, "get_http_capath", "network.http.capath"), 509 | Method(RTorrent, "get_max_downloads_global", "throttle.max_downloads.global"), 510 | Method(RTorrent, "get_session_name", "session.name"), 511 | Method(RTorrent, "get_session_on_completion", "session.on_completion"), 512 | Method(RTorrent, "get_down_limit", "throttle.global_down.max_rate"), 513 | Method(RTorrent, "get_down_total", "throttle.global_down.total"), 514 | Method(RTorrent, "get_up_rate", "throttle.global_up.rate"), 515 | Method(RTorrent, "get_peer_exchange", "protocol.pex"), 516 | Method(RTorrent, "get_down_rate", "throttle.global_down.rate"), 517 | Method(RTorrent, "get_connection_seed", "protocol.connection.seed"), 518 | Method(RTorrent, "get_http_proxy_address", "network.http.proxy_address"), 519 | Method(RTorrent, "get_stats_preloaded", "pieces.stats_preloaded"), 520 | Method(RTorrent, "get_timeout_safe_sync", "pieces.sync.timeout_safe"), 521 | Method(RTorrent, "get_port_random", "network.port_random"), 522 | Method(RTorrent, "get_directory", "directory.default"), 523 | Method(RTorrent, "get_port_open", "network.port_open"), 524 | Method(RTorrent, "get_max_file_size", "system.file.max_size"), 525 | Method(RTorrent, "get_stats_not_preloaded", "pieces.stats_not_preloaded"), 526 | Method(RTorrent, "get_memory_usage", "pieces.memory.current"), 527 | Method(RTorrent, "get_connection_leech", "protocol.connection.leech"), 528 | Method( 529 | RTorrent, "get_hash_on_completion", "pieces.hash.on_completion", boolean=True, 530 | ), 531 | Method(RTorrent, "get_session_lock", "session.use_lock"), 532 | Method(RTorrent, "get_preload_min_rate", "pieces.preload.min_rate"), 533 | Method(RTorrent, "get_max_uploads_global", "throttle.max_uploads.global"), 534 | Method(RTorrent, "get_send_buffer_size", "network.send_buffer.size"), 535 | Method(RTorrent, "get_port_range", "network.port_range"), 536 | Method(RTorrent, "get_max_downloads_div", "throttle.max_downloads.div"), 537 | Method(RTorrent, "get_max_uploads_div", "throttle.max_uploads.div"), 538 | Method(RTorrent, "get_always_safe_sync", "pieces.sync.always_safe"), 539 | Method(RTorrent, "get_bind_address", "network.bind_address"), 540 | Method(RTorrent, "get_up_total", "throttle.global_up.total"), 541 | Method(RTorrent, "get_client_version", "system.client_version"), 542 | Method(RTorrent, "get_library_version", "system.library_version"), 543 | Method(RTorrent, "get_api_version", "system.api_version", min_version=(0, 9, 1)), 544 | Method( 545 | RTorrent, 546 | "get_system_time", 547 | "system.time", 548 | docstring="""Get the current time of the system rTorrent is running on 549 | @return: time (posix) 550 | @rtype: int""", 551 | ), 552 | # MODIFIERS 553 | Method(RTorrent, "set_http_proxy_address", "network.http.proxy_address.set"), 554 | Method(RTorrent, "set_max_memory_usage", "pieces.memory.max.set"), 555 | Method(RTorrent, "set_max_file_size", "system.file.max_size.set"), 556 | Method( 557 | RTorrent, 558 | "set_bind_address", 559 | "network.bind_address.set", 560 | docstring="""Set address bind 561 | @param arg: ip address 562 | @type arg: str 563 | """, 564 | ), 565 | Method( 566 | RTorrent, 567 | "set_up_limit", 568 | "throttle.global_up.max_rate.set", 569 | docstring="""Set global upload limit (in bytes) 570 | @param arg: speed limit 571 | @type arg: int 572 | """, 573 | ), 574 | Method(RTorrent, "set_port_random", "network.port_random.set"), 575 | Method(RTorrent, "set_connection_leech", "protocol.connection.leech.set"), 576 | Method(RTorrent, "set_tracker_numwant", "trackers.numwant.set"), 577 | Method(RTorrent, "set_max_peers", "throttle.max_peers.normal.set"), 578 | Method(RTorrent, "set_min_peers", "throttle.min_peers.normal.set"), 579 | Method(RTorrent, "set_max_uploads_div", "throttle.max_uploads.div.set"), 580 | Method(RTorrent, "set_max_open_files", "network.max_open_files.set"), 581 | Method(RTorrent, "set_max_downloads_global", "throttle.max_downloads.global.set"), 582 | Method(RTorrent, "set_session_lock", "session.use_lock.set"), 583 | Method(RTorrent, "set_session_path", "session.path.set"), 584 | Method(RTorrent, "set_file_split_suffix", "system.file.split_suffix.set"), 585 | Method(RTorrent, "set_port_range", "network.port_range.set"), 586 | Method(RTorrent, "set_min_peers_seed", "throttle.min_peers.seed.set"), 587 | Method(RTorrent, "set_scgi_dont_route", "network.scgi.dont_route.set"), 588 | Method(RTorrent, "set_preload_min_size", "pieces.preload.min_size.set"), 589 | Method(RTorrent, "set_max_uploads_global", "throttle.max_uploads.global.set"), 590 | Method( 591 | RTorrent, 592 | "set_down_limit", 593 | "throttle.global_down.max_rate.set", 594 | docstring="""Set global download limit (in bytes) 595 | @param arg: speed limit 596 | @type arg: int 597 | """, 598 | ), 599 | Method(RTorrent, "set_preload_min_rate", "pieces.preload.min_rate.set"), 600 | Method(RTorrent, "set_max_peers_seed", "throttle.max_peers.seed.set"), 601 | Method(RTorrent, "set_max_uploads", "throttle.max_uploads.set"), 602 | Method(RTorrent, "set_session_on_completion", "session.on_completion.set"), 603 | Method(RTorrent, "set_max_open_http", "network.http.max_open.set"), 604 | Method(RTorrent, "set_directory", "directory.default.set"), 605 | Method(RTorrent, "set_http_cacert", "network.http.cacert.set"), 606 | Method(RTorrent, "set_dht_throttle", "dht.throttle.name.set"), 607 | Method(RTorrent, "set_proxy_address", "network.proxy_address.set"), 608 | Method(RTorrent, "set_split_file_size", "system.file.split_size.set"), 609 | Method(RTorrent, "set_receive_buffer_size", "network.receive_buffer.size.set"), 610 | Method(RTorrent, "set_use_udp_trackers", "trackers.use_udp.set"), 611 | Method(RTorrent, "set_connection_seed", "protocol.connection.seed.set"), 612 | Method(RTorrent, "set_xmlrpc_size_limit", "network.xmlrpc.size_limit.set"), 613 | Method(RTorrent, "set_xmlrpc_dialect", "network.xmlrpc.dialect.set"), 614 | Method(RTorrent, "set_always_safe_sync", "pieces.sync.always_safe.set"), 615 | Method(RTorrent, "set_http_capath", "network.http.capath.set"), 616 | Method(RTorrent, "set_send_buffer_size", "network.send_buffer.size.set"), 617 | Method(RTorrent, "set_max_downloads_div", "throttle.max_downloads.div.set"), 618 | Method(RTorrent, "set_session_name", "session.name.set"), 619 | Method(RTorrent, "set_port_open", "network.port_open.set"), 620 | Method(RTorrent, "set_timeout_sync", "pieces.sync.timeout.set"), 621 | Method(RTorrent, "set_peer_exchange", "protocol.pex.set"), 622 | Method( 623 | RTorrent, 624 | "set_local_address", 625 | "network.local_address.set", 626 | docstring="""Set IP 627 | @param arg: ip address 628 | @type arg: str 629 | """, 630 | ), 631 | Method(RTorrent, "set_timeout_safe_sync", "pieces.sync.timeout_safe.set"), 632 | Method(RTorrent, "set_preload_type", "pieces.preload.type.set"), 633 | Method( 634 | RTorrent, 635 | "set_hash_on_completion", 636 | "pieces.hash.on_completion.set", 637 | docstring="""Enable/Disable hash checking on finished torrents 638 | @param arg: True to enable, False to disable 639 | @type arg: bool 640 | """, 641 | boolean=True, 642 | ), 643 | ] 644 | 645 | _all_methods_list = [ 646 | methods, 647 | rtorrent_rpc.file.methods, 648 | rtorrent_rpc.torrent.methods, 649 | rtorrent_rpc.tracker.methods, 650 | rtorrent_rpc.peer.methods, 651 | ] 652 | 653 | class_methods_pair = { 654 | RTorrent: methods, 655 | rtorrent_rpc.file.File: rtorrent_rpc.file.methods, 656 | rtorrent_rpc.torrent.Torrent: rtorrent_rpc.torrent.methods, 657 | rtorrent_rpc.tracker.Tracker: rtorrent_rpc.tracker.methods, 658 | rtorrent_rpc.peer.Peer: rtorrent_rpc.peer.methods, 659 | } 660 | for c in class_methods_pair.keys(): 661 | rtorrent_rpc.rpc._build_rpc_methods(c, class_methods_pair[c]) 662 | _build_class_methods(c) 663 | --------------------------------------------------------------------------------