16 changed files with 2502 additions and 0 deletions
@ -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.', |
|||
}, |
|||
], |
|||
} |
|||
], |
|||
}] |
@ -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 |
@ -0,0 +1,567 @@ |
|||
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com> |
|||
# 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) |
@ -0,0 +1,86 @@ |
|||
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com> |
|||
# 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) |
@ -0,0 +1,30 @@ |
|||
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com> |
|||
# 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 |
@ -0,0 +1,40 @@ |
|||
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com> |
|||
# 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) |
@ -0,0 +1,91 @@ |
|||
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com> |
|||
# 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 |
|||
] |
@ -0,0 +1,281 @@ |
|||
# Copyright (C) 2011 by clueless <clueless.nospam ! mail.com> |
|||
# |
|||
# 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 |
@ -0,0 +1,159 @@ |
|||
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com> |
|||
# 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") |
@ -0,0 +1,23 @@ |
|||
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com> |
|||
# 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 |
@ -0,0 +1,98 @@ |
|||
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com> |
|||
# 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 |
|||
] |
@ -0,0 +1,354 @@ |
|||
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com> |
|||
# 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) |
@ -0,0 +1,484 @@ |
|||
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com> |
|||
# 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'), |
|||
] |
@ -0,0 +1,138 @@ |
|||
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com> |
|||
# 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'), |
|||
] |
Loading…
Reference in new issue