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