From 3bd18753211956e9e05b345deefdb7db330638ae Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sat, 27 Jul 2013 21:01:23 +1200 Subject: [PATCH] Added initial rtorrent downloader, currently testing, possibly has some bugs. --- couchpotato/core/downloaders/rtorrent/__init__.py | 54 +++ couchpotato/core/downloaders/rtorrent/main.py | 97 ++++ libs/rtorrent/__init__.py | 567 ++++++++++++++++++++++ libs/rtorrent/common.py | 86 ++++ libs/rtorrent/compat.py | 30 ++ libs/rtorrent/err.py | 40 ++ libs/rtorrent/file.py | 91 ++++ libs/rtorrent/lib/__init__.py | 0 libs/rtorrent/lib/bencode.py | 281 +++++++++++ libs/rtorrent/lib/torrentparser.py | 159 ++++++ libs/rtorrent/lib/xmlrpc/__init__.py | 0 libs/rtorrent/lib/xmlrpc/http.py | 23 + libs/rtorrent/peer.py | 98 ++++ libs/rtorrent/rpc/__init__.py | 354 ++++++++++++++ libs/rtorrent/torrent.py | 484 ++++++++++++++++++ libs/rtorrent/tracker.py | 138 ++++++ 16 files changed, 2502 insertions(+) create mode 100755 couchpotato/core/downloaders/rtorrent/__init__.py create mode 100755 couchpotato/core/downloaders/rtorrent/main.py create mode 100755 libs/rtorrent/__init__.py create mode 100755 libs/rtorrent/common.py create mode 100755 libs/rtorrent/compat.py create mode 100755 libs/rtorrent/err.py create mode 100755 libs/rtorrent/file.py create mode 100755 libs/rtorrent/lib/__init__.py create mode 100755 libs/rtorrent/lib/bencode.py create mode 100755 libs/rtorrent/lib/torrentparser.py create mode 100755 libs/rtorrent/lib/xmlrpc/__init__.py create mode 100755 libs/rtorrent/lib/xmlrpc/http.py create mode 100755 libs/rtorrent/peer.py create mode 100755 libs/rtorrent/rpc/__init__.py create mode 100755 libs/rtorrent/torrent.py create mode 100755 libs/rtorrent/tracker.py diff --git a/couchpotato/core/downloaders/rtorrent/__init__.py b/couchpotato/core/downloaders/rtorrent/__init__.py new file mode 100755 index 0000000..d004789 --- /dev/null +++ b/couchpotato/core/downloaders/rtorrent/__init__.py @@ -0,0 +1,54 @@ +from .main import rTorrent + +def start(): + return rTorrent() + +config = [{ + 'name': 'rtorrent', + 'groups': [ + { + 'tab': 'downloaders', + 'list': 'download_providers', + 'name': 'rtorrent', + 'label': 'rTorrent', + 'description': '', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + 'radio_group': 'torrent', + }, + { + 'name': 'url', + 'default': 'http://localhost:80/RPC2', + }, + { + 'name': 'username', + }, + { + 'name': 'password', + 'type': 'password', + }, + { + 'name': 'label', + 'description': 'Label to add torrent as.', + }, + { + 'name': 'paused', + 'type': 'bool', + 'default': False, + 'description': 'Add the torrent paused.', + }, + { + 'name': 'manual', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/downloaders/rtorrent/main.py b/couchpotato/core/downloaders/rtorrent/main.py new file mode 100755 index 0000000..5da64cb --- /dev/null +++ b/couchpotato/core/downloaders/rtorrent/main.py @@ -0,0 +1,97 @@ +from base64 import b16encode, b32decode +from bencode import bencode, bdecode +from couchpotato.core.downloaders.base import Downloader, StatusList +from couchpotato.core.helpers.encoding import isInt, ss +from couchpotato.core.logger import CPLog +from datetime import timedelta +from hashlib import sha1 +from multipartpost import MultipartPostHandler +import cookielib +import httplib +import json +import re +import time +import urllib +import urllib2 +from rtorrent import RTorrent + + +log = CPLog(__name__) + + +class rTorrent(Downloader): + + type = ['torrent', 'torrent_magnet'] + rtorrent_api = None + + def get_conn(self): + return RTorrent( + self.conf('url'), + self.conf('username'), + self.conf('password') + ) + + def download(self, data, movie, filedata=None): + log.debug('Sending "%s" (%s) to rTorrent.', (data.get('name'), data.get('type'))) + + torrent_params = {} + if self.conf('label'): + torrent_params['label'] = self.conf('label') + + if not filedata and data.get('type') == 'torrent': + log.error('Failed sending torrent, no data') + return False + + if data.get('type') == 'torrent_magnet': + log.info('magnet torrents are not supported') + return False + + info = bdecode(filedata)["info"] + torrent_hash = sha1(bencode(info)).hexdigest().upper() + torrent_filename = self.createFileName(data, filedata, movie) + + # Convert base 32 to hex + if len(torrent_hash) == 32: + torrent_hash = b16encode(b32decode(torrent_hash)) + + # Send request to rTorrent + try: + if not self.rtorrent_api: + self.rtorrent_api = self.get_conn() + + torrent = self.rtorrent_api.load_torrent(filedata, not self.conf('paused', default=0)) + + return self.downloadReturnId(torrent_hash) + except Exception, err: + log.error('Failed to send torrent to rTorrent: %s', err) + return False + + + def getAllDownloadStatus(self): + + log.debug('Checking rTorrent download status.') + + try: + if not self.rtorrent_api: + self.rtorrent_api = self.get_conn() + + torrents = self.rtorrent_api.get_torrents() + + statuses = StatusList(self) + + for item in torrents: + statuses.append({ + 'id': item.info_hash, + 'name': item.name, + 'status': 'completed' if item.complete else 'busy', + 'original_status': item.state, + 'timeleft': str(timedelta(seconds=float(item.left_bytes) / item.down_rate)) + if item.down_rate > 0 else -1, + 'folder': '' + }) + + return statuses + + except Exception, err: + log.error('Failed to send torrent to rTorrent: %s', err) + return False diff --git a/libs/rtorrent/__init__.py b/libs/rtorrent/__init__.py new file mode 100755 index 0000000..e427b65 --- /dev/null +++ b/libs/rtorrent/__init__.py @@ -0,0 +1,567 @@ +# Copyright (c) 2013 Chris Lucas, +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from rtorrent.common import find_torrent, \ + is_valid_port, convert_version_tuple_to_str +from rtorrent.lib.torrentparser import TorrentParser +from rtorrent.lib.xmlrpc.http import HTTPServerProxy +from rtorrent.rpc import Method, BasicAuthTransport +from rtorrent.torrent import Torrent +import os.path +import rtorrent.rpc # @UnresolvedImport +import time +import xmlrpclib + +__version__ = "0.2.9" +__author__ = "Chris Lucas" +__contact__ = "chris@chrisjlucas.com" +__license__ = "MIT" + +MIN_RTORRENT_VERSION = (0, 8, 1) +MIN_RTORRENT_VERSION_STR = convert_version_tuple_to_str(MIN_RTORRENT_VERSION) + + +class RTorrent: + """ Create a new rTorrent connection """ + rpc_prefix = None + + def __init__(self, url, username=None, password=None, + verify=False, sp=HTTPServerProxy, sp_kwargs={}): + self.url = url # : From X{__init__(self, url)} + self.username = username + self.password = password + self.sp = sp + self.sp_kwargs = sp_kwargs + + self.torrents = [] # : List of L{Torrent} instances + self._rpc_methods = [] # : List of rTorrent RPC methods + self._torrent_cache = [] + self._client_version_tuple = () + + if verify is True: + self._verify_conn() + + def _get_conn(self): + """Get ServerProxy instance""" + if self.username is not None and self.password is not None: + return self.sp( + self.url, + transport=BasicAuthTransport(self.username, self.password), + **self.sp_kwargs + ) + return self.sp(self.url, **self.sp_kwargs) + + def _verify_conn(self): + # check for rpc methods that should be available + assert {"system.client_version", + "system.library_version"}.issubset(set(self._get_rpc_methods())),\ + "Required RPC methods not available." + + # minimum rTorrent version check + + assert self._meets_version_requirement() is True,\ + "Error: Minimum rTorrent version required is {0}".format( + MIN_RTORRENT_VERSION_STR) + + def _meets_version_requirement(self): + return self._get_client_version_tuple() >= MIN_RTORRENT_VERSION + + def _get_client_version_tuple(self): + conn = self._get_conn() + + if not self._client_version_tuple: + if not hasattr(self, "client_version"): + setattr(self, "client_version", + conn.system.client_version()) + + rtver = getattr(self, "client_version") + self._client_version_tuple = tuple([int(i) for i in + rtver.split(".")]) + + return self._client_version_tuple + + def _get_rpc_methods(self): + """ Get list of raw RPC commands + + @return: raw RPC commands + @rtype: list + """ + + if self._rpc_methods == []: + self._rpc_methods = self._get_conn().system.listMethods() + + return(self._rpc_methods) + + def get_torrents(self, view="main"): + """Get list of all torrents in specified view + + @return: list of L{Torrent} instances + + @rtype: list + + @todo: add validity check for specified view + """ + self.torrents = [] + methods = rtorrent.torrent.methods + retriever_methods = [m for m in methods + if m.is_retriever() and m.is_available(self)] + + m = rtorrent.rpc.Multicall(self) + m.add("d.multicall", view, "d.get_hash=", + *[method.rpc_call + "=" for method in retriever_methods]) + + results = m.call()[0] # only sent one call, only need first result + + for result in results: + results_dict = {} + # build results_dict + for m, r in zip(retriever_methods, result[1:]): # result[0] is the info_hash + results_dict[m.varname] = rtorrent.rpc.process_result(m, r) + + self.torrents.append( + Torrent(self, info_hash=result[0], **results_dict) + ) + + self._manage_torrent_cache() + return(self.torrents) + + def _manage_torrent_cache(self): + """Carry tracker/peer/file lists over to new torrent list""" + for torrent in self._torrent_cache: + new_torrent = rtorrent.common.find_torrent(torrent.info_hash, + self.torrents) + if new_torrent is not None: + new_torrent.files = torrent.files + new_torrent.peers = torrent.peers + new_torrent.trackers = torrent.trackers + + self._torrent_cache = self.torrents + + def _get_load_function(self, file_type, start, verbose): + """Determine correct "load torrent" RPC method""" + func_name = None + if file_type == "url": + # url strings can be input directly + if start and verbose: + func_name = "load_start_verbose" + elif start: + func_name = "load_start" + elif verbose: + func_name = "load_verbose" + else: + func_name = "load" + elif file_type in ["file", "raw"]: + if start and verbose: + func_name = "load_raw_start_verbose" + elif start: + func_name = "load_raw_start" + elif verbose: + func_name = "load_raw_verbose" + else: + func_name = "load_raw" + + return(func_name) + + def load_torrent(self, torrent, start=False, verbose=False, verify_load=True): + """ + Loads torrent into rTorrent (with various enhancements) + + @param torrent: can be a url, a path to a local file, or the raw data + of a torrent file + @type torrent: str + + @param start: start torrent when loaded + @type start: bool + + @param verbose: print error messages to rTorrent log + @type verbose: bool + + @param verify_load: verify that torrent was added to rTorrent successfully + @type verify_load: bool + + @return: Depends on verify_load: + - if verify_load is True, (and the torrent was + loaded successfully), it'll return a L{Torrent} instance + - if verify_load is False, it'll return None + + @rtype: L{Torrent} instance or None + + @raise AssertionError: If the torrent wasn't successfully added to rTorrent + - Check L{TorrentParser} for the AssertionError's + it raises + + + @note: Because this function includes url verification (if a url was input) + as well as verification as to whether the torrent was successfully added, + this function doesn't execute instantaneously. If that's what you're + looking for, use load_torrent_simple() instead. + """ + p = self._get_conn() + tp = TorrentParser(torrent) + torrent = xmlrpclib.Binary(tp._raw_torrent) + info_hash = tp.info_hash + + func_name = self._get_load_function("raw", start, verbose) + + # load torrent + getattr(p, func_name)(torrent) + + if verify_load: + MAX_RETRIES = 3 + i = 0 + while i < MAX_RETRIES: + self.get_torrents() + if info_hash in [t.info_hash for t in self.torrents]: + break + + # was still getting AssertionErrors, delay should help + time.sleep(1) + i += 1 + + assert info_hash in [t.info_hash for t in self.torrents],\ + "Adding torrent was unsuccessful." + + return(find_torrent(info_hash, self.torrents)) + + def load_torrent_simple(self, torrent, file_type, + start=False, verbose=False): + """Loads torrent into rTorrent + + @param torrent: can be a url, a path to a local file, or the raw data + of a torrent file + @type torrent: str + + @param file_type: valid options: "url", "file", or "raw" + @type file_type: str + + @param start: start torrent when loaded + @type start: bool + + @param verbose: print error messages to rTorrent log + @type verbose: bool + + @return: None + + @raise AssertionError: if incorrect file_type is specified + + @note: This function was written for speed, it includes no enhancements. + If you input a url, it won't check if it's valid. You also can't get + verification that the torrent was successfully added to rTorrent. + Use load_torrent() if you would like these features. + """ + p = self._get_conn() + + assert file_type in ["raw", "file", "url"], \ + "Invalid file_type, options are: 'url', 'file', 'raw'." + func_name = self._get_load_function(file_type, start, verbose) + + if file_type == "file": + # since we have to assume we're connected to a remote rTorrent + # client, we have to read the file and send it to rT as raw + assert os.path.isfile(torrent), \ + "Invalid path: \"{0}\"".format(torrent) + torrent = open(torrent, "rb").read() + + if file_type in ["raw", "file"]: + finput = xmlrpclib.Binary(torrent) + elif file_type == "url": + finput = torrent + + getattr(p, func_name)(finput) + + def set_dht_port(self, port): + """Set DHT port + + @param port: port + @type port: int + + @raise AssertionError: if invalid port is given + """ + assert is_valid_port(port), "Valid port range is 0-65535" + self.dht_port = self._p.set_dht_port(port) + + def enable_check_hash(self): + """Alias for set_check_hash(True)""" + self.set_check_hash(True) + + def disable_check_hash(self): + """Alias for set_check_hash(False)""" + self.set_check_hash(False) + + def find_torrent(self, info_hash): + """Frontend for rtorrent.common.find_torrent""" + return(rtorrent.common.find_torrent(info_hash, self.get_torrents())) + + def poll(self): + """ poll rTorrent to get latest torrent/peer/tracker/file information + + @note: This essentially refreshes every aspect of the rTorrent + connection, so it can be very slow if working with a remote + connection that has a lot of torrents loaded. + + @return: None + """ + self.update() + torrents = self.get_torrents() + for t in torrents: + t.poll() + + def update(self): + """Refresh rTorrent client info + + @note: All fields are stored as attributes to self. + + @return: None + """ + multicall = rtorrent.rpc.Multicall(self) + retriever_methods = [m for m in methods + if m.is_retriever() and m.is_available(self)] + for method in retriever_methods: + multicall.add(method) + + multicall.call() + + +def _build_class_methods(class_obj): + # multicall add class + caller = lambda self, multicall, method, *args:\ + multicall.add(method, self.rpc_id, *args) + + caller.__doc__ = """Same as Multicall.add(), but with automatic inclusion + of the rpc_id + + @param multicall: A L{Multicall} instance + @type: multicall: Multicall + + @param method: L{Method} instance or raw rpc method + @type: Method or str + + @param args: optional arguments to pass + """ + setattr(class_obj, "multicall_add", caller) + + +def __compare_rpc_methods(rt_new, rt_old): + from pprint import pprint + rt_new_methods = set(rt_new._get_rpc_methods()) + rt_old_methods = set(rt_old._get_rpc_methods()) + print("New Methods:") + pprint(rt_new_methods - rt_old_methods) + print("Methods not in new rTorrent:") + pprint(rt_old_methods - rt_new_methods) + + +def __check_supported_methods(rt): + from pprint import pprint + supported_methods = set([m.rpc_call for m in + methods + + rtorrent.file.methods + + rtorrent.torrent.methods + + rtorrent.tracker.methods + + rtorrent.peer.methods]) + all_methods = set(rt._get_rpc_methods()) + + print("Methods NOT in supported methods") + pprint(all_methods - supported_methods) + print("Supported methods NOT in all methods") + pprint(supported_methods - all_methods) + +methods = [ + # RETRIEVERS + Method(RTorrent, 'get_xmlrpc_size_limit', 'get_xmlrpc_size_limit'), + Method(RTorrent, 'get_proxy_address', 'get_proxy_address'), + Method(RTorrent, 'get_split_suffix', 'get_split_suffix'), + Method(RTorrent, 'get_up_limit', 'get_upload_rate'), + Method(RTorrent, 'get_max_memory_usage', 'get_max_memory_usage'), + Method(RTorrent, 'get_max_open_files', 'get_max_open_files'), + Method(RTorrent, 'get_min_peers_seed', 'get_min_peers_seed'), + Method(RTorrent, 'get_use_udp_trackers', 'get_use_udp_trackers'), + Method(RTorrent, 'get_preload_min_size', 'get_preload_min_size'), + Method(RTorrent, 'get_max_uploads', 'get_max_uploads'), + Method(RTorrent, 'get_max_peers', 'get_max_peers'), + Method(RTorrent, 'get_timeout_sync', 'get_timeout_sync'), + Method(RTorrent, 'get_receive_buffer_size', 'get_receive_buffer_size'), + Method(RTorrent, 'get_split_file_size', 'get_split_file_size'), + Method(RTorrent, 'get_dht_throttle', 'get_dht_throttle'), + Method(RTorrent, 'get_max_peers_seed', 'get_max_peers_seed'), + Method(RTorrent, 'get_min_peers', 'get_min_peers'), + Method(RTorrent, 'get_tracker_numwant', 'get_tracker_numwant'), + Method(RTorrent, 'get_max_open_sockets', 'get_max_open_sockets'), + Method(RTorrent, 'get_session', 'get_session'), + Method(RTorrent, 'get_ip', 'get_ip'), + Method(RTorrent, 'get_scgi_dont_route', 'get_scgi_dont_route'), + Method(RTorrent, 'get_hash_read_ahead', 'get_hash_read_ahead'), + Method(RTorrent, 'get_http_cacert', 'get_http_cacert'), + Method(RTorrent, 'get_dht_port', 'get_dht_port'), + Method(RTorrent, 'get_handshake_log', 'get_handshake_log'), + Method(RTorrent, 'get_preload_type', 'get_preload_type'), + Method(RTorrent, 'get_max_open_http', 'get_max_open_http'), + Method(RTorrent, 'get_http_capath', 'get_http_capath'), + Method(RTorrent, 'get_max_downloads_global', 'get_max_downloads_global'), + Method(RTorrent, 'get_name', 'get_name'), + Method(RTorrent, 'get_session_on_completion', 'get_session_on_completion'), + Method(RTorrent, 'get_down_limit', 'get_download_rate'), + Method(RTorrent, 'get_down_total', 'get_down_total'), + Method(RTorrent, 'get_up_rate', 'get_up_rate'), + Method(RTorrent, 'get_hash_max_tries', 'get_hash_max_tries'), + Method(RTorrent, 'get_peer_exchange', 'get_peer_exchange'), + Method(RTorrent, 'get_down_rate', 'get_down_rate'), + Method(RTorrent, 'get_connection_seed', 'get_connection_seed'), + Method(RTorrent, 'get_http_proxy', 'get_http_proxy'), + Method(RTorrent, 'get_stats_preloaded', 'get_stats_preloaded'), + Method(RTorrent, 'get_timeout_safe_sync', 'get_timeout_safe_sync'), + Method(RTorrent, 'get_hash_interval', 'get_hash_interval'), + Method(RTorrent, 'get_port_random', 'get_port_random'), + Method(RTorrent, 'get_directory', 'get_directory'), + Method(RTorrent, 'get_port_open', 'get_port_open'), + Method(RTorrent, 'get_max_file_size', 'get_max_file_size'), + Method(RTorrent, 'get_stats_not_preloaded', 'get_stats_not_preloaded'), + Method(RTorrent, 'get_memory_usage', 'get_memory_usage'), + Method(RTorrent, 'get_connection_leech', 'get_connection_leech'), + Method(RTorrent, 'get_check_hash', 'get_check_hash', + boolean=True, + ), + Method(RTorrent, 'get_session_lock', 'get_session_lock'), + Method(RTorrent, 'get_preload_required_rate', 'get_preload_required_rate'), + Method(RTorrent, 'get_max_uploads_global', 'get_max_uploads_global'), + Method(RTorrent, 'get_send_buffer_size', 'get_send_buffer_size'), + Method(RTorrent, 'get_port_range', 'get_port_range'), + Method(RTorrent, 'get_max_downloads_div', 'get_max_downloads_div'), + Method(RTorrent, 'get_max_uploads_div', 'get_max_uploads_div'), + Method(RTorrent, 'get_safe_sync', 'get_safe_sync'), + Method(RTorrent, 'get_bind', 'get_bind'), + Method(RTorrent, 'get_up_total', 'get_up_total'), + Method(RTorrent, 'get_client_version', 'system.client_version'), + Method(RTorrent, 'get_library_version', 'system.library_version'), + Method(RTorrent, 'get_api_version', 'system.api_version', + min_version=(0, 9, 1) + ), + Method(RTorrent, "get_system_time", "system.time", + docstring="""Get the current time of the system rTorrent is running on + + @return: time (posix) + @rtype: int""", + ), + + # MODIFIERS + Method(RTorrent, 'set_http_proxy', 'set_http_proxy'), + Method(RTorrent, 'set_max_memory_usage', 'set_max_memory_usage'), + Method(RTorrent, 'set_max_file_size', 'set_max_file_size'), + Method(RTorrent, 'set_bind', 'set_bind', + docstring="""Set address bind + + @param arg: ip address + @type arg: str + """, + ), + Method(RTorrent, 'set_up_limit', 'set_upload_rate', + docstring="""Set global upload limit (in bytes) + + @param arg: speed limit + @type arg: int + """, + ), + Method(RTorrent, 'set_port_random', 'set_port_random'), + Method(RTorrent, 'set_connection_leech', 'set_connection_leech'), + Method(RTorrent, 'set_tracker_numwant', 'set_tracker_numwant'), + Method(RTorrent, 'set_max_peers', 'set_max_peers'), + Method(RTorrent, 'set_min_peers', 'set_min_peers'), + Method(RTorrent, 'set_max_uploads_div', 'set_max_uploads_div'), + Method(RTorrent, 'set_max_open_files', 'set_max_open_files'), + Method(RTorrent, 'set_max_downloads_global', 'set_max_downloads_global'), + Method(RTorrent, 'set_session_lock', 'set_session_lock'), + Method(RTorrent, 'set_session', 'set_session'), + Method(RTorrent, 'set_split_suffix', 'set_split_suffix'), + Method(RTorrent, 'set_hash_interval', 'set_hash_interval'), + Method(RTorrent, 'set_handshake_log', 'set_handshake_log'), + Method(RTorrent, 'set_port_range', 'set_port_range'), + Method(RTorrent, 'set_min_peers_seed', 'set_min_peers_seed'), + Method(RTorrent, 'set_scgi_dont_route', 'set_scgi_dont_route'), + Method(RTorrent, 'set_preload_min_size', 'set_preload_min_size'), + Method(RTorrent, 'set_log.tracker', 'set_log.tracker'), + Method(RTorrent, 'set_max_uploads_global', 'set_max_uploads_global'), + Method(RTorrent, 'set_down_limit', 'set_download_rate', + docstring="""Set global download limit (in bytes) + + @param arg: speed limit + @type arg: int + """, + ), + Method(RTorrent, 'set_preload_required_rate', 'set_preload_required_rate'), + Method(RTorrent, 'set_hash_read_ahead', 'set_hash_read_ahead'), + Method(RTorrent, 'set_max_peers_seed', 'set_max_peers_seed'), + Method(RTorrent, 'set_max_uploads', 'set_max_uploads'), + Method(RTorrent, 'set_session_on_completion', 'set_session_on_completion'), + Method(RTorrent, 'set_max_open_http', 'set_max_open_http'), + Method(RTorrent, 'set_directory', 'set_directory'), + Method(RTorrent, 'set_http_cacert', 'set_http_cacert'), + Method(RTorrent, 'set_dht_throttle', 'set_dht_throttle'), + Method(RTorrent, 'set_hash_max_tries', 'set_hash_max_tries'), + Method(RTorrent, 'set_proxy_address', 'set_proxy_address'), + Method(RTorrent, 'set_split_file_size', 'set_split_file_size'), + Method(RTorrent, 'set_receive_buffer_size', 'set_receive_buffer_size'), + Method(RTorrent, 'set_use_udp_trackers', 'set_use_udp_trackers'), + Method(RTorrent, 'set_connection_seed', 'set_connection_seed'), + Method(RTorrent, 'set_xmlrpc_size_limit', 'set_xmlrpc_size_limit'), + Method(RTorrent, 'set_xmlrpc_dialect', 'set_xmlrpc_dialect'), + Method(RTorrent, 'set_safe_sync', 'set_safe_sync'), + Method(RTorrent, 'set_http_capath', 'set_http_capath'), + Method(RTorrent, 'set_send_buffer_size', 'set_send_buffer_size'), + Method(RTorrent, 'set_max_downloads_div', 'set_max_downloads_div'), + Method(RTorrent, 'set_name', 'set_name'), + Method(RTorrent, 'set_port_open', 'set_port_open'), + Method(RTorrent, 'set_timeout_sync', 'set_timeout_sync'), + Method(RTorrent, 'set_peer_exchange', 'set_peer_exchange'), + Method(RTorrent, 'set_ip', 'set_ip', + docstring="""Set IP + + @param arg: ip address + @type arg: str + """, + ), + Method(RTorrent, 'set_timeout_safe_sync', 'set_timeout_safe_sync'), + Method(RTorrent, 'set_preload_type', 'set_preload_type'), + Method(RTorrent, 'set_check_hash', 'set_check_hash', + docstring="""Enable/Disable hash checking on finished torrents + + @param arg: True to enable, False to disable + @type arg: bool + """, + boolean=True, + ), +] + +_all_methods_list = [methods, + rtorrent.file.methods, + rtorrent.torrent.methods, + rtorrent.tracker.methods, + rtorrent.peer.methods, + ] + +class_methods_pair = { + RTorrent: methods, + rtorrent.file.File: rtorrent.file.methods, + rtorrent.torrent.Torrent: rtorrent.torrent.methods, + rtorrent.tracker.Tracker: rtorrent.tracker.methods, + rtorrent.peer.Peer: rtorrent.peer.methods, +} +for c in class_methods_pair.keys(): + rtorrent.rpc._build_rpc_methods(c, class_methods_pair[c]) + _build_class_methods(c) diff --git a/libs/rtorrent/common.py b/libs/rtorrent/common.py new file mode 100755 index 0000000..371c71c --- /dev/null +++ b/libs/rtorrent/common.py @@ -0,0 +1,86 @@ +# Copyright (c) 2013 Chris Lucas, +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +from rtorrent.compat import is_py3 + + +def bool_to_int(value): + """Translates python booleans to RPC-safe integers""" + if value is True: + return("1") + elif value is False: + return("0") + else: + return(value) + + +def cmd_exists(cmds_list, cmd): + """Check if given command is in list of available commands + + @param cmds_list: see L{RTorrent._rpc_methods} + @type cmds_list: list + + @param cmd: name of command to be checked + @type cmd: str + + @return: bool + """ + + return(cmd in cmds_list) + + +def find_torrent(info_hash, torrent_list): + """Find torrent file in given list of Torrent classes + + @param info_hash: info hash of torrent + @type info_hash: str + + @param torrent_list: list of L{Torrent} instances (see L{RTorrent.get_torrents}) + @type torrent_list: list + + @return: L{Torrent} instance, or -1 if not found + """ + for t in torrent_list: + if t.info_hash == info_hash: + return t + + return None + + +def is_valid_port(port): + """Check if given port is valid""" + return(0 <= int(port) <= 65535) + + +def convert_version_tuple_to_str(t): + return(".".join([str(n) for n in t])) + + +def safe_repr(fmt, *args, **kwargs): + """ Formatter that handles unicode arguments """ + + if not is_py3(): + # unicode fmt can take str args, str fmt cannot take unicode args + fmt = fmt.decode("utf-8") + out = fmt.format(*args, **kwargs) + return out.encode("utf-8") + else: + return fmt.format(*args, **kwargs) diff --git a/libs/rtorrent/compat.py b/libs/rtorrent/compat.py new file mode 100755 index 0000000..1778818 --- /dev/null +++ b/libs/rtorrent/compat.py @@ -0,0 +1,30 @@ +# Copyright (c) 2013 Chris Lucas, +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import sys + + +def is_py3(): + return sys.version_info[0] == 3 + +if is_py3(): + import xmlrpc.client as xmlrpclib +else: + import xmlrpclib diff --git a/libs/rtorrent/err.py b/libs/rtorrent/err.py new file mode 100755 index 0000000..920b838 --- /dev/null +++ b/libs/rtorrent/err.py @@ -0,0 +1,40 @@ +# Copyright (c) 2013 Chris Lucas, +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from rtorrent.common import convert_version_tuple_to_str + + +class RTorrentVersionError(Exception): + def __init__(self, min_version, cur_version): + self.min_version = min_version + self.cur_version = cur_version + self.msg = "Minimum version required: {0}".format( + convert_version_tuple_to_str(min_version)) + + def __str__(self): + return(self.msg) + + +class MethodError(Exception): + def __init__(self, msg): + self.msg = msg + + def __str__(self): + return(self.msg) diff --git a/libs/rtorrent/file.py b/libs/rtorrent/file.py new file mode 100755 index 0000000..a3db35c --- /dev/null +++ b/libs/rtorrent/file.py @@ -0,0 +1,91 @@ +# Copyright (c) 2013 Chris Lucas, +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# from rtorrent.rpc import Method +import rtorrent.rpc + +from rtorrent.common import safe_repr + +Method = rtorrent.rpc.Method + + +class File: + """Represents an individual file within a L{Torrent} instance.""" + + def __init__(self, _rt_obj, info_hash, index, **kwargs): + self._rt_obj = _rt_obj + self.info_hash = info_hash # : info hash for the torrent the file is associated with + self.index = index # : The position of the file within the file list + for k in kwargs.keys(): + setattr(self, k, kwargs.get(k, None)) + + self.rpc_id = "{0}:f{1}".format( + self.info_hash, self.index) # : unique id to pass to rTorrent + + def update(self): + """Refresh file data + + @note: All fields are stored as attributes to self. + + @return: None + """ + multicall = rtorrent.rpc.Multicall(self) + retriever_methods = [m for m in methods + if m.is_retriever() and m.is_available(self._rt_obj)] + for method in retriever_methods: + multicall.add(method, self.rpc_id) + + multicall.call() + + def __repr__(self): + return safe_repr("File(index={0} path=\"{1}\")", self.index, self.path) + +methods = [ + # RETRIEVERS + Method(File, 'get_last_touched', 'f.get_last_touched'), + Method(File, 'get_range_second', 'f.get_range_second'), + Method(File, 'get_size_bytes', 'f.get_size_bytes'), + Method(File, 'get_priority', 'f.get_priority'), + Method(File, 'get_match_depth_next', 'f.get_match_depth_next'), + Method(File, 'is_resize_queued', 'f.is_resize_queued', + boolean=True, + ), + Method(File, 'get_range_first', 'f.get_range_first'), + Method(File, 'get_match_depth_prev', 'f.get_match_depth_prev'), + Method(File, 'get_path', 'f.get_path'), + Method(File, 'get_completed_chunks', 'f.get_completed_chunks'), + Method(File, 'get_path_components', 'f.get_path_components'), + Method(File, 'is_created', 'f.is_created', + boolean=True, + ), + Method(File, 'is_open', 'f.is_open', + boolean=True, + ), + Method(File, 'get_size_chunks', 'f.get_size_chunks'), + Method(File, 'get_offset', 'f.get_offset'), + Method(File, 'get_frozen_path', 'f.get_frozen_path'), + Method(File, 'get_path_depth', 'f.get_path_depth'), + Method(File, 'is_create_queued', 'f.is_create_queued', + boolean=True, + ), + + + # MODIFIERS +] diff --git a/libs/rtorrent/lib/__init__.py b/libs/rtorrent/lib/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/libs/rtorrent/lib/bencode.py b/libs/rtorrent/lib/bencode.py new file mode 100755 index 0000000..97bd2f0 --- /dev/null +++ b/libs/rtorrent/lib/bencode.py @@ -0,0 +1,281 @@ +# Copyright (C) 2011 by clueless +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# Version: 20111107 +# +# Changelog +# --------- +# 2011-11-07 - Added support for Python2 (tested on 2.6) +# 2011-10-03 - Fixed: moved check for end of list at the top of the while loop +# in _decode_list (in case the list is empty) (Chris Lucas) +# - Converted dictionary keys to str +# 2011-04-24 - Changed date format to YYYY-MM-DD for versioning, bigger +# integer denotes a newer version +# - Fixed a bug that would treat False as an integral type but +# encode it using the 'False' string, attempting to encode a +# boolean now results in an error +# - Fixed a bug where an integer value of 0 in a list or +# dictionary resulted in a parse error while decoding +# +# 2011-04-03 - Original release + +import sys + +_py3 = sys.version_info[0] == 3 + +if _py3: + _VALID_STRING_TYPES = (str,) +else: + _VALID_STRING_TYPES = (str, unicode) # @UndefinedVariable + +_TYPE_INT = 1 +_TYPE_STRING = 2 +_TYPE_LIST = 3 +_TYPE_DICTIONARY = 4 +_TYPE_END = 5 +_TYPE_INVALID = 6 + +# Function to determine the type of he next value/item +# Arguments: +# char First character of the string that is to be decoded +# Return value: +# Returns an integer that describes what type the next value/item is + + +def _gettype(char): + if not isinstance(char, int): + char = ord(char) + if char == 0x6C: # 'l' + return _TYPE_LIST + elif char == 0x64: # 'd' + return _TYPE_DICTIONARY + elif char == 0x69: # 'i' + return _TYPE_INT + elif char == 0x65: # 'e' + return _TYPE_END + elif char >= 0x30 and char <= 0x39: # '0' '9' + return _TYPE_STRING + else: + return _TYPE_INVALID + +# Function to parse a string from the bendcoded data +# Arguments: +# data bencoded data, must be guaranteed to be a string +# Return Value: +# Returns a tuple, the first member of the tuple is the parsed string +# The second member is whatever remains of the bencoded data so it can +# be used to parse the next part of the data + + +def _decode_string(data): + end = 1 + # if py3, data[end] is going to be an int + # if py2, data[end] will be a string + if _py3: + char = 0x3A + else: + char = chr(0x3A) + + while data[end] != char: # ':' + end = end + 1 + strlen = int(data[:end]) + return (data[end + 1:strlen + end + 1], data[strlen + end + 1:]) + +# Function to parse an integer from the bencoded data +# Arguments: +# data bencoded data, must be guaranteed to be an integer +# Return Value: +# Returns a tuple, the first member of the tuple is the parsed string +# The second member is whatever remains of the bencoded data so it can +# be used to parse the next part of the data + + +def _decode_int(data): + end = 1 + # if py3, data[end] is going to be an int + # if py2, data[end] will be a string + if _py3: + char = 0x65 + else: + char = chr(0x65) + + while data[end] != char: # 'e' + end = end + 1 + return (int(data[1:end]), data[end + 1:]) + +# Function to parse a bencoded list +# Arguments: +# data bencoded data, must be guaranted to be the start of a list +# Return Value: +# Returns a tuple, the first member of the tuple is the parsed list +# The second member is whatever remains of the bencoded data so it can +# be used to parse the next part of the data + + +def _decode_list(data): + x = [] + overflow = data[1:] + while True: # Loop over the data + if _gettype(overflow[0]) == _TYPE_END: # - Break if we reach the end of the list + return (x, overflow[1:]) # and return the list and overflow + + value, overflow = _decode(overflow) # + if isinstance(value, bool) or overflow == '': # - if we have a parse error + return (False, False) # Die with error + else: # - Otherwise + x.append(value) # add the value to the list + + +# Function to parse a bencoded list +# Arguments: +# data bencoded data, must be guaranted to be the start of a list +# Return Value: +# Returns a tuple, the first member of the tuple is the parsed dictionary +# The second member is whatever remains of the bencoded data so it can +# be used to parse the next part of the data +def _decode_dict(data): + x = {} + overflow = data[1:] + while True: # Loop over the data + if _gettype(overflow[0]) != _TYPE_STRING: # - If the key is not a string + return (False, False) # Die with error + key, overflow = _decode(overflow) # + if key == False or overflow == '': # - If parse error + return (False, False) # Die with error + value, overflow = _decode(overflow) # + if isinstance(value, bool) or overflow == '': # - If parse error + print("Error parsing value") + print(value) + print(overflow) + return (False, False) # Die with error + else: + # don't use bytes for the key + key = key.decode() + x[key] = value + if _gettype(overflow[0]) == _TYPE_END: + return (x, overflow[1:]) + +# Arguments: +# data bencoded data in bytes format +# Return Values: +# Returns a tuple, the first member is the parsed data, could be a string, +# an integer, a list or a dictionary, or a combination of those +# The second member is the leftover of parsing, if everything parses correctly this +# should be an empty byte string + + +def _decode(data): + btype = _gettype(data[0]) + if btype == _TYPE_INT: + return _decode_int(data) + elif btype == _TYPE_STRING: + return _decode_string(data) + elif btype == _TYPE_LIST: + return _decode_list(data) + elif btype == _TYPE_DICTIONARY: + return _decode_dict(data) + else: + return (False, False) + +# Function to decode bencoded data +# Arguments: +# data bencoded data, can be str or bytes +# Return Values: +# Returns the decoded data on success, this coud be bytes, int, dict or list +# or a combinatin of those +# If an error occurs the return value is False + + +def decode(data): + # if isinstance(data, str): + # data = data.encode() + decoded, overflow = _decode(data) + return decoded + +# Args: data as integer +# return: encoded byte string + + +def _encode_int(data): + return b'i' + str(data).encode() + b'e' + +# Args: data as string or bytes +# Return: encoded byte string + + +def _encode_string(data): + return str(len(data)).encode() + b':' + data + +# Args: data as list +# Return: Encoded byte string, false on error + + +def _encode_list(data): + elist = b'l' + for item in data: + eitem = encode(item) + if eitem == False: + return False + elist += eitem + return elist + b'e' + +# Args: data as dict +# Return: encoded byte string, false on error + + +def _encode_dict(data): + edict = b'd' + keys = [] + for key in data: + if not isinstance(key, _VALID_STRING_TYPES) and not isinstance(key, bytes): + return False + keys.append(key) + keys.sort() + for key in keys: + ekey = encode(key) + eitem = encode(data[key]) + if ekey == False or eitem == False: + return False + edict += ekey + eitem + return edict + b'e' + +# Function to encode a variable in bencoding +# Arguments: +# data Variable to be encoded, can be a list, dict, str, bytes, int or a combination of those +# Return Values: +# Returns the encoded data as a byte string when successful +# If an error occurs the return value is False + + +def encode(data): + if isinstance(data, bool): + return False + elif isinstance(data, int): + return _encode_int(data) + elif isinstance(data, bytes): + return _encode_string(data) + elif isinstance(data, _VALID_STRING_TYPES): + return _encode_string(data.encode()) + elif isinstance(data, list): + return _encode_list(data) + elif isinstance(data, dict): + return _encode_dict(data) + else: + return False diff --git a/libs/rtorrent/lib/torrentparser.py b/libs/rtorrent/lib/torrentparser.py new file mode 100755 index 0000000..19dd12a --- /dev/null +++ b/libs/rtorrent/lib/torrentparser.py @@ -0,0 +1,159 @@ +# Copyright (c) 2013 Chris Lucas, +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from rtorrent.compat import is_py3 +import os.path +import re +import rtorrent.lib.bencode as bencode +import hashlib + +if is_py3(): + from urllib.request import urlopen # @UnresolvedImport @UnusedImport +else: + from urllib2 import urlopen # @UnresolvedImport @Reimport + + +class TorrentParser(): + def __init__(self, torrent): + """Decode and parse given torrent + + @param torrent: handles: urls, file paths, string of torrent data + @type torrent: str + + @raise AssertionError: Can be raised for a couple reasons: + - If _get_raw_torrent() couldn't figure out + what X{torrent} is + - if X{torrent} isn't a valid bencoded torrent file + """ + self.torrent = torrent + self._raw_torrent = None # : testing yo + self._torrent_decoded = None # : what up + self.file_type = None + + self._get_raw_torrent() + assert self._raw_torrent is not None, "Couldn't get raw_torrent." + if self._torrent_decoded is None: + self._decode_torrent() + assert isinstance(self._torrent_decoded, dict), "Invalid torrent file." + self._parse_torrent() + + def _is_raw(self): + raw = False + if isinstance(self.torrent, (str, bytes)): + if isinstance(self._decode_torrent(self.torrent), dict): + raw = True + else: + # reset self._torrent_decoded (currently equals False) + self._torrent_decoded = None + + return(raw) + + def _get_raw_torrent(self): + """Get raw torrent data by determining what self.torrent is""" + # already raw? + if self._is_raw(): + self.file_type = "raw" + self._raw_torrent = self.torrent + return + # local file? + if os.path.isfile(self.torrent): + self.file_type = "file" + self._raw_torrent = open(self.torrent, "rb").read() + # url? + elif re.search("^(http|ftp):\/\/", self.torrent, re.I): + self.file_type = "url" + self._raw_torrent = urlopen(self.torrent).read() + + def _decode_torrent(self, raw_torrent=None): + if raw_torrent is None: + raw_torrent = self._raw_torrent + self._torrent_decoded = bencode.decode(raw_torrent) + return(self._torrent_decoded) + + def _calc_info_hash(self): + self.info_hash = None + if "info" in self._torrent_decoded.keys(): + info_dict = self._torrent_decoded["info"] + self.info_hash = hashlib.sha1(bencode.encode( + info_dict)).hexdigest().upper() + + return(self.info_hash) + + def _parse_torrent(self): + for k in self._torrent_decoded: + key = k.replace(" ", "_").lower() + setattr(self, key, self._torrent_decoded[k]) + + self._calc_info_hash() + + +class NewTorrentParser(object): + @staticmethod + def _read_file(fp): + return fp.read() + + @staticmethod + def _write_file(fp): + fp.write() + return fp + + @staticmethod + def _decode_torrent(data): + return bencode.decode(data) + + def __init__(self, input): + self.input = input + self._raw_torrent = None + self._decoded_torrent = None + self._hash_outdated = False + + if isinstance(self.input, (str, bytes)): + # path to file? + if os.path.isfile(self.input): + self._raw_torrent = self._read_file(open(self.input, "rb")) + else: + # assume input was the raw torrent data (do we really want + # this?) + self._raw_torrent = self.input + + # file-like object? + elif self.input.hasattr("read"): + self._raw_torrent = self._read_file(self.input) + + assert self._raw_torrent is not None, "Invalid input: input must be a path or a file-like object" + + self._decoded_torrent = self._decode_torrent(self._raw_torrent) + + assert isinstance( + self._decoded_torrent, dict), "File could not be decoded" + + def _calc_info_hash(self): + self.info_hash = None + info_dict = self._torrent_decoded["info"] + self.info_hash = hashlib.sha1(bencode.encode( + info_dict)).hexdigest().upper() + + return(self.info_hash) + + def set_tracker(self, tracker): + self._decoded_torrent["announce"] = tracker + + def get_tracker(self): + return self._decoded_torrent.get("announce") diff --git a/libs/rtorrent/lib/xmlrpc/__init__.py b/libs/rtorrent/lib/xmlrpc/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/libs/rtorrent/lib/xmlrpc/http.py b/libs/rtorrent/lib/xmlrpc/http.py new file mode 100755 index 0000000..3eb8521 --- /dev/null +++ b/libs/rtorrent/lib/xmlrpc/http.py @@ -0,0 +1,23 @@ +# Copyright (c) 2013 Chris Lucas, +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from rtorrent.compat import xmlrpclib + +HTTPServerProxy = xmlrpclib.ServerProxy diff --git a/libs/rtorrent/peer.py b/libs/rtorrent/peer.py new file mode 100755 index 0000000..61ca094 --- /dev/null +++ b/libs/rtorrent/peer.py @@ -0,0 +1,98 @@ +# Copyright (c) 2013 Chris Lucas, +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# from rtorrent.rpc import Method +import rtorrent.rpc + +from rtorrent.common import safe_repr + +Method = rtorrent.rpc.Method + + +class Peer: + """Represents an individual peer within a L{Torrent} instance.""" + def __init__(self, _rt_obj, info_hash, **kwargs): + self._rt_obj = _rt_obj + self.info_hash = info_hash # : info hash for the torrent the peer is associated with + for k in kwargs.keys(): + setattr(self, k, kwargs.get(k, None)) + + self.rpc_id = "{0}:p{1}".format( + self.info_hash, self.id) # : unique id to pass to rTorrent + + def __repr__(self): + return safe_repr("Peer(id={0})", self.id) + + def update(self): + """Refresh peer data + + @note: All fields are stored as attributes to self. + + @return: None + """ + multicall = rtorrent.rpc.Multicall(self) + retriever_methods = [m for m in methods + if m.is_retriever() and m.is_available(self._rt_obj)] + for method in retriever_methods: + multicall.add(method, self.rpc_id) + + multicall.call() + +methods = [ + # RETRIEVERS + Method(Peer, 'is_preferred', 'p.is_preferred', + boolean=True, + ), + Method(Peer, 'get_down_rate', 'p.get_down_rate'), + Method(Peer, 'is_unwanted', 'p.is_unwanted', + boolean=True, + ), + Method(Peer, 'get_peer_total', 'p.get_peer_total'), + Method(Peer, 'get_peer_rate', 'p.get_peer_rate'), + Method(Peer, 'get_port', 'p.get_port'), + Method(Peer, 'is_snubbed', 'p.is_snubbed', + boolean=True, + ), + Method(Peer, 'get_id_html', 'p.get_id_html'), + Method(Peer, 'get_up_rate', 'p.get_up_rate'), + Method(Peer, 'is_banned', 'p.banned', + boolean=True, + ), + Method(Peer, 'get_completed_percent', 'p.get_completed_percent'), + Method(Peer, 'completed_percent', 'p.completed_percent'), + Method(Peer, 'get_id', 'p.get_id'), + Method(Peer, 'is_obfuscated', 'p.is_obfuscated', + boolean=True, + ), + Method(Peer, 'get_down_total', 'p.get_down_total'), + Method(Peer, 'get_client_version', 'p.get_client_version'), + Method(Peer, 'get_address', 'p.get_address'), + Method(Peer, 'is_incoming', 'p.is_incoming', + boolean=True, + ), + Method(Peer, 'is_encrypted', 'p.is_encrypted', + boolean=True, + ), + Method(Peer, 'get_options_str', 'p.get_options_str'), + Method(Peer, 'get_client_version', 'p.client_version'), + Method(Peer, 'get_up_total', 'p.get_up_total'), + + # MODIFIERS +] diff --git a/libs/rtorrent/rpc/__init__.py b/libs/rtorrent/rpc/__init__.py new file mode 100755 index 0000000..8190de4 --- /dev/null +++ b/libs/rtorrent/rpc/__init__.py @@ -0,0 +1,354 @@ +# Copyright (c) 2013 Chris Lucas, +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +from base64 import encodestring +import httplib +import string + +import rtorrent +import re +from rtorrent.common import bool_to_int, convert_version_tuple_to_str,\ + safe_repr +from rtorrent.err import RTorrentVersionError, MethodError +from rtorrent.compat import xmlrpclib + + +class BasicAuthTransport(xmlrpclib.Transport): + def __init__(self, username=None, password=None): + xmlrpclib.Transport.__init__(self) + self.username = username + self.password = password + + def send_auth(self, h): + if self.username is not None and self.password is not None: + h.putheader('AUTHORIZATION', "Basic %s" % string.replace( + encodestring("%s:%s" % (self.username, self.password)), + "\012", "" + )) + + def single_request(self, host, handler, request_body, verbose=0): + # issue XML-RPC request + + h = self.make_connection(host) + if verbose: + h.set_debuglevel(1) + + try: + self.send_request(h, handler, request_body) + self.send_host(h, host) + self.send_user_agent(h) + self.send_auth(h) + self.send_content(h, request_body) + + response = h.getresponse(buffering=True) + if response.status == 200: + self.verbose = verbose + return self.parse_response(response) + except xmlrpclib.Fault: + raise + except Exception: + self.close() + raise + + #discard any response data and raise exception + #if (response.getheader("content-length", 0)): + # response.read() + raise xmlrpclib.ProtocolError( + host + handler, + response.status, response.reason, + response.msg, + ) + + +def get_varname(rpc_call): + """Transform rpc method into variable name. + + @newfield example: Example + @example: if the name of the rpc method is 'p.get_down_rate', the variable + name will be 'down_rate' + """ + # extract variable name from xmlrpc func name + r = re.search( + "([ptdf]\.|system\.|get\_|is\_|set\_)+([^=]*)", rpc_call, re.I) + if r: + return(r.groups()[-1]) + else: + return(None) + + +def _handle_unavailable_rpc_method(method, rt_obj): + msg = "Method isn't available." + if rt_obj._get_client_version_tuple() < method.min_version: + msg = "This method is only available in " \ + "RTorrent version v{0} or later".format( + convert_version_tuple_to_str(method.min_version)) + + raise MethodError(msg) + + +class DummyClass: + def __init__(self): + pass + + +class Method: + """Represents an individual RPC method""" + + def __init__(self, _class, method_name, + rpc_call, docstring=None, varname=None, **kwargs): + self._class = _class # : Class this method is associated with + self.class_name = _class.__name__ + self.method_name = method_name # : name of public-facing method + self.rpc_call = rpc_call # : name of rpc method + self.docstring = docstring # : docstring for rpc method (optional) + self.varname = varname # : variable for the result of the method call, usually set to self.varname + self.min_version = kwargs.get("min_version", ( + 0, 0, 0)) # : Minimum version of rTorrent required + self.boolean = kwargs.get("boolean", False) # : returns boolean value? + self.post_process_func = kwargs.get( + "post_process_func", None) # : custom post process function + self.aliases = kwargs.get( + "aliases", []) # : aliases for method (optional) + self.required_args = [] + #: Arguments required when calling the method (not utilized) + + self.method_type = self._get_method_type() + + if self.varname is None: + self.varname = get_varname(self.rpc_call) + assert self.varname is not None, "Couldn't get variable name." + + def __repr__(self): + return safe_repr("Method(method_name='{0}', rpc_call='{1}')", + self.method_name, self.rpc_call) + + def _get_method_type(self): + """Determine whether method is a modifier or a retriever""" + if self.method_name[:4] == "set_": return('m') # modifier + else: + return('r') # retriever + + def is_modifier(self): + if self.method_type == 'm': + return(True) + else: + return(False) + + def is_retriever(self): + if self.method_type == 'r': + return(True) + else: + return(False) + + def is_available(self, rt_obj): + if rt_obj._get_client_version_tuple() < self.min_version or \ + self.rpc_call not in rt_obj._get_rpc_methods(): + return(False) + else: + return(True) + + +class Multicall: + def __init__(self, class_obj, **kwargs): + self.class_obj = class_obj + if class_obj.__class__.__name__ == "RTorrent": + self.rt_obj = class_obj + else: + self.rt_obj = class_obj._rt_obj + self.calls = [] + + def add(self, method, *args): + """Add call to multicall + + @param method: L{Method} instance or name of raw RPC method + @type method: Method or str + + @param args: call arguments + """ + # if a raw rpc method was given instead of a Method instance, + # try and find the instance for it. And if all else fails, create a + # dummy Method instance + if isinstance(method, str): + result = find_method(method) + # if result not found + if result == -1: + method = Method(DummyClass, method, method) + else: + method = result + + # ensure method is available before adding + if not method.is_available(self.rt_obj): + _handle_unavailable_rpc_method(method, self.rt_obj) + + self.calls.append((method, args)) + + def list_calls(self): + for c in self.calls: + print(c) + + def call(self): + """Execute added multicall calls + + @return: the results (post-processed), in the order they were added + @rtype: tuple + """ + m = xmlrpclib.MultiCall(self.rt_obj._get_conn()) + for call in self.calls: + method, args = call + rpc_call = getattr(method, "rpc_call") + getattr(m, rpc_call)(*args) + + results = m() + results = tuple(results) + results_processed = [] + + for r, c in zip(results, self.calls): + method = c[0] # Method instance + result = process_result(method, r) + results_processed.append(result) + # assign result to class_obj + setattr(self.class_obj, method.varname, result) + + return(tuple(results_processed)) + + +def call_method(class_obj, method, *args): + """Handles single RPC calls + + @param class_obj: Peer/File/Torrent/Tracker/RTorrent instance + @type class_obj: object + + @param method: L{Method} instance or name of raw RPC method + @type method: Method or str + """ + if method.is_retriever(): + args = args[:-1] + else: + assert args[-1] is not None, "No argument given." + + if class_obj.__class__.__name__ == "RTorrent": + rt_obj = class_obj + else: + rt_obj = class_obj._rt_obj + + # check if rpc method is even available + if not method.is_available(rt_obj): + _handle_unavailable_rpc_method(method, rt_obj) + + m = Multicall(class_obj) + m.add(method, *args) + # only added one method, only getting one result back + ret_value = m.call()[0] + + ####### OBSOLETE ########################################################## + # if method.is_retriever(): + # #value = process_result(method, ret_value) + # value = ret_value #MultiCall already processed the result + # else: + # # we're setting the user's input to method.varname + # # but we'll return the value that xmlrpc gives us + # value = process_result(method, args[-1]) + ########################################################################## + + return(ret_value) + + +def find_method(rpc_call): + """Return L{Method} instance associated with given RPC call""" + method_lists = [ + rtorrent.methods, + rtorrent.file.methods, + rtorrent.tracker.methods, + rtorrent.peer.methods, + rtorrent.torrent.methods, + ] + + for l in method_lists: + for m in l: + if m.rpc_call.lower() == rpc_call.lower(): + return(m) + + return(-1) + + +def process_result(method, result): + """Process given C{B{result}} based on flags set in C{B{method}} + + @param method: L{Method} instance + @type method: Method + + @param result: result to be processed (the result of given L{Method} instance) + + @note: Supported Processing: + - boolean - convert ones and zeros returned by rTorrent and + convert to python boolean values + """ + # handle custom post processing function + if method.post_process_func is not None: + result = method.post_process_func(result) + + # is boolean? + if method.boolean: + if result in [1, '1']: + result = True + elif result in [0, '0']: + result = False + + return(result) + + +def _build_rpc_methods(class_, method_list): + """Build glorified aliases to raw RPC methods""" + for m in method_list: + class_name = m.class_name + if class_name != class_.__name__: + continue + + if class_name == "RTorrent": + caller = lambda self, arg = None, method = m:\ + call_method(self, method, bool_to_int(arg)) + elif class_name == "Torrent": + caller = lambda self, arg = None, method = m:\ + call_method(self, method, self.rpc_id, + bool_to_int(arg)) + elif class_name in ["Tracker", "File"]: + caller = lambda self, arg = None, method = m:\ + call_method(self, method, self.rpc_id, + bool_to_int(arg)) + + elif class_name == "Peer": + caller = lambda self, arg = None, method = m:\ + call_method(self, method, self.rpc_id, + bool_to_int(arg)) + + if m.docstring is None: + m.docstring = "" + + # print(m) + docstring = """{0} + + @note: Variable where the result for this method is stored: {1}.{2}""".format( + m.docstring, + class_name, + m.varname) + + caller.__doc__ = docstring + + for method_name in [m.method_name] + list(m.aliases): + setattr(class_, method_name, caller) diff --git a/libs/rtorrent/torrent.py b/libs/rtorrent/torrent.py new file mode 100755 index 0000000..1e06e1c --- /dev/null +++ b/libs/rtorrent/torrent.py @@ -0,0 +1,484 @@ +# Copyright (c) 2013 Chris Lucas, +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import rtorrent.rpc +# from rtorrent.rpc import Method +import rtorrent.peer +import rtorrent.tracker +import rtorrent.file +import rtorrent.compat + +from rtorrent.common import safe_repr + +Peer = rtorrent.peer.Peer +Tracker = rtorrent.tracker.Tracker +File = rtorrent.file.File +Method = rtorrent.rpc.Method + + +class Torrent: + """Represents an individual torrent within a L{RTorrent} instance.""" + + def __init__(self, _rt_obj, info_hash, **kwargs): + self._rt_obj = _rt_obj + self.info_hash = info_hash # : info hash for the torrent + self.rpc_id = self.info_hash # : unique id to pass to rTorrent + for k in kwargs.keys(): + setattr(self, k, kwargs.get(k, None)) + + self.peers = [] + self.trackers = [] + self.files = [] + + self._call_custom_methods() + + def __repr__(self): + return safe_repr("Torrent(info_hash=\"{0}\" name=\"{1}\")", + self.info_hash, self.name) + + def _call_custom_methods(self): + """only calls methods that check instance variables.""" + self._is_hash_checking_queued() + self._is_started() + self._is_paused() + + def get_peers(self): + """Get list of Peer instances for given torrent. + + @return: L{Peer} instances + @rtype: list + + @note: also assigns return value to self.peers + """ + self.peers = [] + retriever_methods = [m for m in rtorrent.peer.methods + if m.is_retriever() and m.is_available(self._rt_obj)] + # need to leave 2nd arg empty (dunno why) + m = rtorrent.rpc.Multicall(self) + m.add("p.multicall", self.info_hash, "", + *[method.rpc_call + "=" for method in retriever_methods]) + + results = m.call()[0] # only sent one call, only need first result + + for result in results: + results_dict = {} + # build results_dict + for m, r in zip(retriever_methods, result): + results_dict[m.varname] = rtorrent.rpc.process_result(m, r) + + self.peers.append(Peer( + self._rt_obj, self.info_hash, **results_dict)) + + return(self.peers) + + def get_trackers(self): + """Get list of Tracker instances for given torrent. + + @return: L{Tracker} instances + @rtype: list + + @note: also assigns return value to self.trackers + """ + self.trackers = [] + retriever_methods = [m for m in rtorrent.tracker.methods + if m.is_retriever() and m.is_available(self._rt_obj)] + + # need to leave 2nd arg empty (dunno why) + m = rtorrent.rpc.Multicall(self) + m.add("t.multicall", self.info_hash, "", + *[method.rpc_call + "=" for method in retriever_methods]) + + results = m.call()[0] # only sent one call, only need first result + + for result in results: + results_dict = {} + # build results_dict + for m, r in zip(retriever_methods, result): + results_dict[m.varname] = rtorrent.rpc.process_result(m, r) + + self.trackers.append(Tracker( + self._rt_obj, self.info_hash, **results_dict)) + + return(self.trackers) + + def get_files(self): + """Get list of File instances for given torrent. + + @return: L{File} instances + @rtype: list + + @note: also assigns return value to self.files + """ + + self.files = [] + retriever_methods = [m for m in rtorrent.file.methods + if m.is_retriever() and m.is_available(self._rt_obj)] + # 2nd arg can be anything, but it'll return all files in torrent + # regardless + m = rtorrent.rpc.Multicall(self) + m.add("f.multicall", self.info_hash, "", + *[method.rpc_call + "=" for method in retriever_methods]) + + results = m.call()[0] # only sent one call, only need first result + + offset_method_index = retriever_methods.index( + rtorrent.rpc.find_method("f.get_offset")) + + # make a list of the offsets of all the files, sort appropriately + offset_list = sorted([r[offset_method_index] for r in results]) + + for result in results: + results_dict = {} + # build results_dict + for m, r in zip(retriever_methods, result): + results_dict[m.varname] = rtorrent.rpc.process_result(m, r) + + # get proper index positions for each file (based on the file + # offset) + f_index = offset_list.index(results_dict["offset"]) + + self.files.append(File(self._rt_obj, self.info_hash, + f_index, **results_dict)) + + return(self.files) + + def set_directory(self, d): + """Modify download directory + + @note: Needs to stop torrent in order to change the directory. + Also doesn't restart after directory is set, that must be called + separately. + """ + m = rtorrent.rpc.Multicall(self) + self.multicall_add(m, "d.try_stop") + self.multicall_add(m, "d.set_directory", d) + + self.directory = m.call()[-1] + + def start(self): + """Start the torrent""" + m = rtorrent.rpc.Multicall(self) + self.multicall_add(m, "d.try_start") + self.multicall_add(m, "d.is_active") + + self.active = m.call()[-1] + return(self.active) + + def stop(self): + """"Stop the torrent""" + m = rtorrent.rpc.Multicall(self) + self.multicall_add(m, "d.try_stop") + self.multicall_add(m, "d.is_active") + + self.active = m.call()[-1] + return(self.active) + + def close(self): + """Close the torrent and it's files""" + m = rtorrent.rpc.Multicall(self) + self.multicall_add(m, "d.close") + + return(m.call()[-1]) + + def erase(self): + """Delete the torrent + + @note: doesn't delete the downloaded files""" + m = rtorrent.rpc.Multicall(self) + self.multicall_add(m, "d.erase") + + return(m.call()[-1]) + + def check_hash(self): + """(Re)hash check the torrent""" + m = rtorrent.rpc.Multicall(self) + self.multicall_add(m, "d.check_hash") + + return(m.call()[-1]) + + def poll(self): + """poll rTorrent to get latest peer/tracker/file information""" + self.get_peers() + self.get_trackers() + self.get_files() + + def update(self): + """Refresh torrent data + + @note: All fields are stored as attributes to self. + + @return: None + """ + multicall = rtorrent.rpc.Multicall(self) + retriever_methods = [m for m in methods + if m.is_retriever() and m.is_available(self._rt_obj)] + for method in retriever_methods: + multicall.add(method, self.rpc_id) + + multicall.call() + + # custom functions (only call private methods, since they only check + # local variables and are therefore faster) + self._call_custom_methods() + + def accept_seeders(self, accept_seeds): + """Enable/disable whether the torrent connects to seeders + + @param accept_seeds: enable/disable accepting seeders + @type accept_seeds: bool""" + if accept_seeds: + call = "d.accepting_seeders.enable" + else: + call = "d.accepting_seeders.disable" + + m = rtorrent.rpc.Multicall(self) + self.multicall_add(m, call) + + return(m.call()[-1]) + + def announce(self): + """Announce torrent info to tracker(s)""" + m = rtorrent.rpc.Multicall(self) + self.multicall_add(m, "d.tracker_announce") + + return(m.call()[-1]) + + @staticmethod + def _assert_custom_key_valid(key): + assert type(key) == int and key > 0 and key < 6, \ + "key must be an integer between 1-5" + + def get_custom(self, key): + """ + Get custom value + + @param key: the index for the custom field (between 1-5) + @type key: int + + @rtype: str + """ + + self._assert_custom_key_valid(key) + m = rtorrent.rpc.Multicall(self) + + field = "custom{0}".format(key) + self.multicall_add(m, "d.get_{0}".format(field)) + setattr(self, field, m.call()[-1]) + + return (getattr(self, field)) + + def set_custom(self, key, value): + """ + Set custom value + + @param key: the index for the custom field (between 1-5) + @type key: int + + @param value: the value to be stored + @type value: str + + @return: if successful, value will be returned + @rtype: str + """ + + self._assert_custom_key_valid(key) + m = rtorrent.rpc.Multicall(self) + + self.multicall_add(m, "d.set_custom{0}".format(key), value) + + return(m.call()[-1]) + + ############################################################################ + # CUSTOM METHODS (Not part of the official rTorrent API) + ########################################################################## + def _is_hash_checking_queued(self): + """Only checks instance variables, shouldn't be called directly""" + # if hashing == 3, then torrent is marked for hash checking + # if hash_checking == False, then torrent is waiting to be checked + self.hash_checking_queued = (self.hashing == 3 and + self.hash_checking is False) + + return(self.hash_checking_queued) + + def is_hash_checking_queued(self): + """Check if torrent is waiting to be hash checked + + @note: Variable where the result for this method is stored Torrent.hash_checking_queued""" + m = rtorrent.rpc.Multicall(self) + self.multicall_add(m, "d.get_hashing") + self.multicall_add(m, "d.is_hash_checking") + results = m.call() + + setattr(self, "hashing", results[0]) + setattr(self, "hash_checking", results[1]) + + return(self._is_hash_checking_queued()) + + def _is_paused(self): + """Only checks instance variables, shouldn't be called directly""" + self.paused = (self.state == 0) + return(self.paused) + + def is_paused(self): + """Check if torrent is paused + + @note: Variable where the result for this method is stored: Torrent.paused""" + self.get_state() + return(self._is_paused()) + + def _is_started(self): + """Only checks instance variables, shouldn't be called directly""" + self.started = (self.state == 1) + return(self.started) + + def is_started(self): + """Check if torrent is started + + @note: Variable where the result for this method is stored: Torrent.started""" + self.get_state() + return(self._is_started()) + + +methods = [ + # RETRIEVERS + Method(Torrent, 'is_hash_checked', 'd.is_hash_checked', + boolean=True, + ), + Method(Torrent, 'is_hash_checking', 'd.is_hash_checking', + boolean=True, + ), + Method(Torrent, 'get_peers_max', 'd.get_peers_max'), + Method(Torrent, 'get_tracker_focus', 'd.get_tracker_focus'), + Method(Torrent, 'get_skip_total', 'd.get_skip_total'), + Method(Torrent, 'get_state', 'd.get_state'), + Method(Torrent, 'get_peer_exchange', 'd.get_peer_exchange'), + Method(Torrent, 'get_down_rate', 'd.get_down_rate'), + Method(Torrent, 'get_connection_seed', 'd.get_connection_seed'), + Method(Torrent, 'get_uploads_max', 'd.get_uploads_max'), + Method(Torrent, 'get_priority_str', 'd.get_priority_str'), + Method(Torrent, 'is_open', 'd.is_open', + boolean=True, + ), + Method(Torrent, 'get_peers_min', 'd.get_peers_min'), + Method(Torrent, 'get_peers_complete', 'd.get_peers_complete'), + Method(Torrent, 'get_tracker_numwant', 'd.get_tracker_numwant'), + Method(Torrent, 'get_connection_current', 'd.get_connection_current'), + Method(Torrent, 'is_complete', 'd.get_complete', + boolean=True, + ), + Method(Torrent, 'get_peers_connected', 'd.get_peers_connected'), + Method(Torrent, 'get_chunk_size', 'd.get_chunk_size'), + Method(Torrent, 'get_state_counter', 'd.get_state_counter'), + Method(Torrent, 'get_base_filename', 'd.get_base_filename'), + Method(Torrent, 'get_state_changed', 'd.get_state_changed'), + Method(Torrent, 'get_peers_not_connected', 'd.get_peers_not_connected'), + Method(Torrent, 'get_directory', 'd.get_directory'), + Method(Torrent, 'is_incomplete', 'd.incomplete', + boolean=True, + ), + Method(Torrent, 'get_tracker_size', 'd.get_tracker_size'), + Method(Torrent, 'is_multi_file', 'd.is_multi_file', + boolean=True, + ), + Method(Torrent, 'get_local_id', 'd.get_local_id'), + Method(Torrent, 'get_ratio', 'd.get_ratio', + post_process_func=lambda x: x / 1000.0, + ), + Method(Torrent, 'get_loaded_file', 'd.get_loaded_file'), + Method(Torrent, 'get_max_file_size', 'd.get_max_file_size'), + Method(Torrent, 'get_size_chunks', 'd.get_size_chunks'), + Method(Torrent, 'is_pex_active', 'd.is_pex_active', + boolean=True, + ), + Method(Torrent, 'get_hashing', 'd.get_hashing'), + Method(Torrent, 'get_bitfield', 'd.get_bitfield'), + Method(Torrent, 'get_local_id_html', 'd.get_local_id_html'), + Method(Torrent, 'get_connection_leech', 'd.get_connection_leech'), + Method(Torrent, 'get_peers_accounted', 'd.get_peers_accounted'), + Method(Torrent, 'get_message', 'd.get_message'), + Method(Torrent, 'is_active', 'd.is_active', + boolean=True, + ), + Method(Torrent, 'get_size_bytes', 'd.get_size_bytes'), + Method(Torrent, 'get_ignore_commands', 'd.get_ignore_commands'), + Method(Torrent, 'get_creation_date', 'd.get_creation_date'), + Method(Torrent, 'get_base_path', 'd.get_base_path'), + Method(Torrent, 'get_left_bytes', 'd.get_left_bytes'), + Method(Torrent, 'get_size_files', 'd.get_size_files'), + Method(Torrent, 'get_size_pex', 'd.get_size_pex'), + Method(Torrent, 'is_private', 'd.is_private', + boolean=True, + ), + Method(Torrent, 'get_max_size_pex', 'd.get_max_size_pex'), + Method(Torrent, 'get_num_chunks_hashed', 'd.get_chunks_hashed', + aliases=("get_chunks_hashed",)), + Method(Torrent, 'get_num_chunks_wanted', 'd.wanted_chunks'), + Method(Torrent, 'get_priority', 'd.get_priority'), + Method(Torrent, 'get_skip_rate', 'd.get_skip_rate'), + Method(Torrent, 'get_completed_bytes', 'd.get_completed_bytes'), + Method(Torrent, 'get_name', 'd.get_name'), + Method(Torrent, 'get_completed_chunks', 'd.get_completed_chunks'), + Method(Torrent, 'get_throttle_name', 'd.get_throttle_name'), + Method(Torrent, 'get_free_diskspace', 'd.get_free_diskspace'), + Method(Torrent, 'get_directory_base', 'd.get_directory_base'), + Method(Torrent, 'get_hashing_failed', 'd.get_hashing_failed'), + Method(Torrent, 'get_tied_to_file', 'd.get_tied_to_file'), + Method(Torrent, 'get_down_total', 'd.get_down_total'), + Method(Torrent, 'get_bytes_done', 'd.get_bytes_done'), + Method(Torrent, 'get_up_rate', 'd.get_up_rate'), + Method(Torrent, 'get_up_total', 'd.get_up_total'), + Method(Torrent, 'is_accepting_seeders', 'd.accepting_seeders', + boolean=True, + ), + Method(Torrent, "get_chunks_seen", "d.chunks_seen", + min_version=(0, 9, 1), + ), + Method(Torrent, "is_partially_done", "d.is_partially_done", + boolean=True, + ), + Method(Torrent, "is_not_partially_done", "d.is_not_partially_done", + boolean=True, + ), + Method(Torrent, "get_time_started", "d.timestamp.started"), + Method(Torrent, "get_custom1", "d.get_custom1"), + Method(Torrent, "get_custom2", "d.get_custom2"), + Method(Torrent, "get_custom3", "d.get_custom3"), + Method(Torrent, "get_custom4", "d.get_custom4"), + Method(Torrent, "get_custom5", "d.get_custom5"), + + # MODIFIERS + Method(Torrent, 'set_uploads_max', 'd.set_uploads_max'), + Method(Torrent, 'set_tied_to_file', 'd.set_tied_to_file'), + Method(Torrent, 'set_tracker_numwant', 'd.set_tracker_numwant'), + Method(Torrent, 'set_priority', 'd.set_priority'), + Method(Torrent, 'set_peers_max', 'd.set_peers_max'), + Method(Torrent, 'set_hashing_failed', 'd.set_hashing_failed'), + Method(Torrent, 'set_message', 'd.set_message'), + Method(Torrent, 'set_throttle_name', 'd.set_throttle_name'), + Method(Torrent, 'set_peers_min', 'd.set_peers_min'), + Method(Torrent, 'set_ignore_commands', 'd.set_ignore_commands'), + Method(Torrent, 'set_max_file_size', 'd.set_max_file_size'), + Method(Torrent, 'set_custom5', 'd.set_custom5'), + Method(Torrent, 'set_custom4', 'd.set_custom4'), + Method(Torrent, 'set_custom2', 'd.set_custom2'), + Method(Torrent, 'set_custom1', 'd.set_custom1'), + Method(Torrent, 'set_custom3', 'd.set_custom3'), + Method(Torrent, 'set_connection_current', 'd.set_connection_current'), +] diff --git a/libs/rtorrent/tracker.py b/libs/rtorrent/tracker.py new file mode 100755 index 0000000..81af2e4 --- /dev/null +++ b/libs/rtorrent/tracker.py @@ -0,0 +1,138 @@ +# Copyright (c) 2013 Chris Lucas, +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# from rtorrent.rpc import Method +import rtorrent.rpc + +from rtorrent.common import safe_repr + +Method = rtorrent.rpc.Method + + +class Tracker: + """Represents an individual tracker within a L{Torrent} instance.""" + + def __init__(self, _rt_obj, info_hash, **kwargs): + self._rt_obj = _rt_obj + self.info_hash = info_hash # : info hash for the torrent using this tracker + for k in kwargs.keys(): + setattr(self, k, kwargs.get(k, None)) + + # for clarity's sake... + self.index = self.group # : position of tracker within the torrent's tracker list + self.rpc_id = "{0}:t{1}".format( + self.info_hash, self.index) # : unique id to pass to rTorrent + + def __repr__(self): + return safe_repr("Tracker(index={0}, url=\"{1}\")", + self.index, self.url) + + def enable(self): + """Alias for set_enabled("yes")""" + self.set_enabled("yes") + + def disable(self): + """Alias for set_enabled("no")""" + self.set_enabled("no") + + def update(self): + """Refresh tracker data + + @note: All fields are stored as attributes to self. + + @return: None + """ + multicall = rtorrent.rpc.Multicall(self) + retriever_methods = [m for m in methods + if m.is_retriever() and m.is_available(self._rt_obj)] + for method in retriever_methods: + multicall.add(method, self.rpc_id) + + multicall.call() + +methods = [ + # RETRIEVERS + Method(Tracker, 'is_enabled', 't.is_enabled', boolean=True), + Method(Tracker, 'get_id', 't.get_id'), + Method(Tracker, 'get_scrape_incomplete', 't.get_scrape_incomplete'), + Method(Tracker, 'is_open', 't.is_open', boolean=True), + Method(Tracker, 'get_min_interval', 't.get_min_interval'), + Method(Tracker, 'get_scrape_downloaded', 't.get_scrape_downloaded'), + Method(Tracker, 'get_group', 't.get_group'), + Method(Tracker, 'get_scrape_time_last', 't.get_scrape_time_last'), + Method(Tracker, 'get_type', 't.get_type'), + Method(Tracker, 'get_normal_interval', 't.get_normal_interval'), + Method(Tracker, 'get_url', 't.get_url'), + Method(Tracker, 'get_scrape_complete', 't.get_scrape_complete', + min_version=(0, 8, 9), + ), + Method(Tracker, 'get_activity_time_last', 't.activity_time_last', + min_version=(0, 8, 9), + ), + Method(Tracker, 'get_activity_time_next', 't.activity_time_next', + min_version=(0, 8, 9), + ), + Method(Tracker, 'get_failed_time_last', 't.failed_time_last', + min_version=(0, 8, 9), + ), + Method(Tracker, 'get_failed_time_next', 't.failed_time_next', + min_version=(0, 8, 9), + ), + Method(Tracker, 'get_success_time_last', 't.success_time_last', + min_version=(0, 8, 9), + ), + Method(Tracker, 'get_success_time_next', 't.success_time_next', + min_version=(0, 8, 9), + ), + Method(Tracker, 'can_scrape', 't.can_scrape', + min_version=(0, 9, 1), + boolean=True + ), + Method(Tracker, 'get_failed_counter', 't.failed_counter', + min_version=(0, 8, 9) + ), + Method(Tracker, 'get_scrape_counter', 't.scrape_counter', + min_version=(0, 8, 9) + ), + Method(Tracker, 'get_success_counter', 't.success_counter', + min_version=(0, 8, 9) + ), + Method(Tracker, 'is_usable', 't.is_usable', + min_version=(0, 9, 1), + boolean=True + ), + Method(Tracker, 'is_busy', 't.is_busy', + min_version=(0, 9, 1), + boolean=True + ), + Method(Tracker, 'is_extra_tracker', 't.is_extra_tracker', + min_version=(0, 9, 1), + boolean=True, + ), + Method(Tracker, "get_latest_sum_peers", "t.latest_sum_peers", + min_version=(0, 9, 0) + ), + Method(Tracker, "get_latest_new_peers", "t.latest_new_peers", + min_version=(0, 9, 0) + ), + + # MODIFIERS + Method(Tracker, 'set_enabled', 't.set_enabled'), +]