19 changed files with 987 additions and 58 deletions
@ -0,0 +1,81 @@ |
|||
import HTMLParser |
|||
from couchpotato import fireEvent |
|||
from couchpotato.core.logger import CPLog |
|||
from couchpotato.core.media.movie.providers.automation.base import Automation |
|||
|
|||
log = CPLog(__name__) |
|||
|
|||
autoload = 'YTSPopular' |
|||
|
|||
|
|||
class YTSPopular(Automation): |
|||
|
|||
interval = 1800 |
|||
url = 'https://yts.lt/' |
|||
|
|||
def getIMDBids(self): |
|||
|
|||
movies = [] |
|||
source = self.getHTMLData(self.url) |
|||
|
|||
class MyHTMLParser(HTMLParser): |
|||
doparse = False |
|||
dotitle = False |
|||
doyear = False |
|||
currentmovie = {'title':"", 'year':""} |
|||
movies = [] |
|||
def handle_starttag(self, tag, attrs): |
|||
for attr in attrs: |
|||
self.doparse = (attr[0] == "id" and attr[1] == "popular-downloads") or self.doparse |
|||
self.dotitle = (attr[0] == "class" and attr[1] == "browse-movie-title" and self.doparse) |
|||
self.doyear = (attr[0] == "class" and attr[1] == "browse-movie-year" and self.doparse) |
|||
if (attr[0] == "class" and attr[1] == "home-movies"): |
|||
self.doparse = False |
|||
|
|||
def handle_endtag(self, tag): |
|||
self.dotitle = False |
|||
self.doyear = False |
|||
|
|||
def handle_data(self, data): |
|||
if (self.doparse): |
|||
if (self.dotitle): |
|||
self.dotitle = False |
|||
self.currentmovie['title'] = data |
|||
if (self.doyear): |
|||
self.doyear = False |
|||
self.currentmovie['year'] = data |
|||
self.movies.append(self.currentmovie) |
|||
self.currentmovie = {'title':"", 'year':""} |
|||
|
|||
def getMovies(self): |
|||
return self.movies |
|||
|
|||
parser = MyHTMLParser() |
|||
parser.feed(source) |
|||
for el in parser.getMovies(): |
|||
imdb = self.search(el['title'], el['year']) |
|||
if imdb and self.isMinimalMovie(imdb): |
|||
movies.append(imdb['imdb']) |
|||
|
|||
return movies |
|||
|
|||
|
|||
config = [{ |
|||
'name': 'ytspopular', |
|||
'groups': [ |
|||
{ |
|||
'tab': 'automation', |
|||
'list': 'automation_providers', |
|||
'name': 'ytspopular_automation', |
|||
'label': 'YTS Popular', |
|||
'description': 'Imports popular downloas as currently listed on YTS.', |
|||
'options': [ |
|||
{ |
|||
'name': 'automation_enabled', |
|||
'default': False, |
|||
'type': 'enabler', |
|||
}, |
|||
], |
|||
}, |
|||
], |
|||
}] |
@ -0,0 +1,275 @@ |
|||
import logging |
|||
import socket |
|||
import ssl |
|||
import struct |
|||
import warnings |
|||
import zlib |
|||
|
|||
from .rencode import dumps, loads |
|||
|
|||
RPC_RESPONSE = 1 |
|||
RPC_ERROR = 2 |
|||
RPC_EVENT = 3 |
|||
|
|||
MESSAGE_HEADER_SIZE = 5 |
|||
READ_SIZE = 10 |
|||
|
|||
logger = logging.getLogger(__name__) |
|||
|
|||
|
|||
class DelugeClientException(Exception): |
|||
"""Base exception for all deluge client exceptions""" |
|||
|
|||
|
|||
class ConnectionLostException(DelugeClientException): |
|||
pass |
|||
|
|||
|
|||
class CallTimeoutException(DelugeClientException): |
|||
pass |
|||
|
|||
|
|||
class InvalidHeaderException(DelugeClientException): |
|||
pass |
|||
|
|||
|
|||
class FailedToReconnectException(DelugeClientException): |
|||
pass |
|||
|
|||
|
|||
class RemoteException(DelugeClientException): |
|||
pass |
|||
|
|||
|
|||
class DelugeRPCClient(object): |
|||
timeout = 20 |
|||
|
|||
def __init__(self, host, port, username, password, decode_utf8=False, automatic_reconnect=True): |
|||
self.host = host |
|||
self.port = port |
|||
self.username = username |
|||
self.password = password |
|||
self.deluge_version = None |
|||
# This is only applicable if deluge_version is 2 |
|||
self.deluge_protocol_version = None |
|||
|
|||
self.decode_utf8 = decode_utf8 |
|||
if not self.decode_utf8: |
|||
warnings.warn('Using `decode_utf8=False` is deprecated, please set it to True.' |
|||
'The argument will be removed in a future release where it will be always True', DeprecationWarning) |
|||
|
|||
self.automatic_reconnect = automatic_reconnect |
|||
|
|||
self.request_id = 1 |
|||
self.connected = False |
|||
self._create_socket() |
|||
|
|||
def _create_socket(self, ssl_version=None): |
|||
if ssl_version is not None: |
|||
self._socket = ssl.wrap_socket(socket.socket(socket.AF_INET, socket.SOCK_STREAM), ssl_version=ssl_version) |
|||
else: |
|||
self._socket = ssl.wrap_socket(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) |
|||
self._socket.settimeout(self.timeout) |
|||
|
|||
def connect(self): |
|||
""" |
|||
Connects to the Deluge instance |
|||
""" |
|||
self._connect() |
|||
logger.debug('Connected to Deluge, detecting daemon version') |
|||
self._detect_deluge_version() |
|||
logger.debug('Daemon version {} detected, logging in'.format(self.deluge_version)) |
|||
if self.deluge_version == 2: |
|||
result = self.call('daemon.login', self.username, self.password, client_version='deluge-client') |
|||
else: |
|||
result = self.call('daemon.login', self.username, self.password) |
|||
logger.debug('Logged in with value %r' % result) |
|||
self.connected = True |
|||
|
|||
def _connect(self): |
|||
logger.info('Connecting to %s:%s' % (self.host, self.port)) |
|||
try: |
|||
self._socket.connect((self.host, self.port)) |
|||
except ssl.SSLError as e: |
|||
# Note: have not verified that we actually get errno 258 for this error |
|||
if (hasattr(ssl, 'PROTOCOL_SSLv3') and |
|||
(getattr(e, 'reason', None) == 'UNSUPPORTED_PROTOCOL' or e.errno == 258)): |
|||
logger.warning('Was unable to ssl handshake, trying to force SSLv3 (insecure)') |
|||
self._create_socket(ssl_version=ssl.PROTOCOL_SSLv3) |
|||
self._socket.connect((self.host, self.port)) |
|||
else: |
|||
raise |
|||
|
|||
def disconnect(self): |
|||
""" |
|||
Disconnect from deluge |
|||
""" |
|||
if self.connected: |
|||
self._socket.close() |
|||
self._socket = None |
|||
self.connected = False |
|||
|
|||
def _detect_deluge_version(self): |
|||
if self.deluge_version is not None: |
|||
return |
|||
|
|||
self._send_call(1, None, 'daemon.info') |
|||
self._send_call(2, None, 'daemon.info') |
|||
self._send_call(2, 1, 'daemon.info') |
|||
result = self._socket.recv(1) |
|||
if result[:1] == b'D': |
|||
# This is a protocol deluge 2.0 was using before release |
|||
self.deluge_version = 2 |
|||
self.deluge_protocol_version = None |
|||
# If we need the specific version of deluge 2, this is it. |
|||
daemon_version = self._receive_response(2, None, partial_data=result) |
|||
elif ord(result[:1]) == 1: |
|||
self.deluge_version = 2 |
|||
self.deluge_protocol_version = 1 |
|||
# If we need the specific version of deluge 2, this is it. |
|||
daemon_version = self._receive_response(2, 1, partial_data=result) |
|||
else: |
|||
self.deluge_version = 1 |
|||
# Deluge 1 doesn't recover well from the bad request. Re-connect the socket. |
|||
self._socket.close() |
|||
self._create_socket() |
|||
self._connect() |
|||
|
|||
def _send_call(self, deluge_version, protocol_version, method, *args, **kwargs): |
|||
self.request_id += 1 |
|||
if method == 'daemon.login': |
|||
debug_args = list(args) |
|||
if len(debug_args) >= 2: |
|||
debug_args[1] = '<password hidden>' |
|||
logger.debug('Calling reqid %s method %r with args:%r kwargs:%r' % (self.request_id, method, debug_args, kwargs)) |
|||
else: |
|||
logger.debug('Calling reqid %s method %r with args:%r kwargs:%r' % (self.request_id, method, args, kwargs)) |
|||
|
|||
req = ((self.request_id, method, args, kwargs), ) |
|||
req = zlib.compress(dumps(req)) |
|||
|
|||
if deluge_version == 2: |
|||
if protocol_version is None: |
|||
# This was a protocol for deluge 2 before they introduced protocol version numbers |
|||
self._socket.send(b'D' + struct.pack("!i", len(req))) |
|||
elif protocol_version == 1: |
|||
self._socket.send(struct.pack('!BI', protocol_version, len(req))) |
|||
else: |
|||
raise Exception('Deluge protocol version {} is not (yet) supported.'.format(protocol_version)) |
|||
self._socket.send(req) |
|||
|
|||
def _receive_response(self, deluge_version, protocol_version, partial_data=b''): |
|||
expected_bytes = None |
|||
data = partial_data |
|||
while True: |
|||
try: |
|||
d = self._socket.recv(READ_SIZE) |
|||
except ssl.SSLError: |
|||
raise CallTimeoutException() |
|||
|
|||
data += d |
|||
if deluge_version == 2: |
|||
if expected_bytes is None: |
|||
if len(data) < 5: |
|||
continue |
|||
|
|||
header = data[:MESSAGE_HEADER_SIZE] |
|||
data = data[MESSAGE_HEADER_SIZE:] |
|||
|
|||
if protocol_version is None: |
|||
if header[0] != b'D'[0]: |
|||
raise InvalidHeaderException('Expected D as first byte in reply') |
|||
elif ord(header[:1]) != protocol_version: |
|||
raise InvalidHeaderException( |
|||
'Expected protocol version ({}) as first byte in reply'.format(protocol_version) |
|||
) |
|||
|
|||
if protocol_version is None: |
|||
expected_bytes = struct.unpack('!i', header[1:])[0] |
|||
else: |
|||
expected_bytes = struct.unpack('!I', header[1:])[0] |
|||
|
|||
if len(data) >= expected_bytes: |
|||
data = zlib.decompress(data) |
|||
break |
|||
else: |
|||
try: |
|||
data = zlib.decompress(data) |
|||
except zlib.error: |
|||
if not d: |
|||
raise ConnectionLostException() |
|||
continue |
|||
break |
|||
|
|||
data = list(loads(data, decode_utf8=self.decode_utf8)) |
|||
msg_type = data.pop(0) |
|||
request_id = data.pop(0) |
|||
|
|||
if msg_type == RPC_ERROR: |
|||
if self.deluge_version == 2: |
|||
exception_type, exception_msg, _, traceback = data |
|||
# On deluge 2, exception arguments are sent as tuple |
|||
if self.decode_utf8: |
|||
exception_msg = ', '.join(exception_msg) |
|||
else: |
|||
exception_msg = b', '.join(exception_msg) |
|||
else: |
|||
exception_type, exception_msg, traceback = data[0] |
|||
if self.decode_utf8: |
|||
exception = type(str(exception_type), (RemoteException, ), {}) |
|||
exception_msg = '%s\n%s' % (exception_msg, |
|||
traceback) |
|||
else: |
|||
exception = type(str(exception_type.decode('utf-8', 'ignore')), (RemoteException, ), {}) |
|||
exception_msg = '%s\n%s' % (exception_msg.decode('utf-8', 'ignore'), |
|||
traceback.decode('utf-8', 'ignore')) |
|||
raise exception(exception_msg) |
|||
elif msg_type == RPC_RESPONSE: |
|||
retval = data[0] |
|||
return retval |
|||
|
|||
def reconnect(self): |
|||
""" |
|||
Reconnect |
|||
""" |
|||
self.disconnect() |
|||
self._create_socket() |
|||
self.connect() |
|||
|
|||
def call(self, method, *args, **kwargs): |
|||
""" |
|||
Calls an RPC function |
|||
""" |
|||
tried_reconnect = False |
|||
for _ in range(2): |
|||
try: |
|||
self._send_call(self.deluge_version, self.deluge_protocol_version, method, *args, **kwargs) |
|||
return self._receive_response(self.deluge_version, self.deluge_protocol_version) |
|||
except (socket.error, ConnectionLostException, CallTimeoutException): |
|||
if self.automatic_reconnect: |
|||
if tried_reconnect: |
|||
raise FailedToReconnectException() |
|||
else: |
|||
try: |
|||
self.reconnect() |
|||
except (socket.error, ConnectionLostException, CallTimeoutException): |
|||
raise FailedToReconnectException() |
|||
|
|||
tried_reconnect = True |
|||
else: |
|||
raise |
|||
|
|||
def __getattr__(self, item): |
|||
return RPCCaller(self.call, item) |
|||
|
|||
|
|||
class RPCCaller(object): |
|||
def __init__(self, caller, method=''): |
|||
self.caller = caller |
|||
self.method = method |
|||
|
|||
def __getattr__(self, item): |
|||
return RPCCaller(self.caller, self.method+'.'+item) |
|||
|
|||
def __call__(self, *args, **kwargs): |
|||
return self.caller(self.method, *args, **kwargs) |
@ -0,0 +1,474 @@ |
|||
# Original bencode module by Petru Paler, et al. |
|||
# |
|||
# Modifications by Connelly Barnes: |
|||
# |
|||
# - Added support for floats (sent as 32-bit or 64-bit in network |
|||
# order), bools, None. |
|||
# - Allowed dict keys to be of any serializable type. |
|||
# - Lists/tuples are always decoded as tuples (thus, tuples can be |
|||
# used as dict keys). |
|||
# - Embedded extra information in the 'typecodes' to save some space. |
|||
# - Added a restriction on integer length, so that malicious hosts |
|||
# cannot pass us large integers which take a long time to decode. |
|||
# |
|||
# Licensed by Bram Cohen under the "MIT license": |
|||
# |
|||
# "Copyright (C) 2001-2002 Bram Cohen |
|||
# |
|||
# 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." |
|||
# |
|||
# (The rencode module is licensed under the above license as well). |
|||
# |
|||
|
|||
""" |
|||
rencode -- Web safe object pickling/unpickling. |
|||
|
|||
Public domain, Connelly Barnes 2006-2007. |
|||
|
|||
The rencode module is a modified version of bencode from the |
|||
BitTorrent project. For complex, heterogeneous data structures with |
|||
many small elements, r-encodings take up significantly less space than |
|||
b-encodings: |
|||
|
|||
>>> len(rencode.dumps({'a':0, 'b':[1,2], 'c':99})) |
|||
13 |
|||
>>> len(bencode.bencode({'a':0, 'b':[1,2], 'c':99})) |
|||
26 |
|||
|
|||
The rencode format is not standardized, and may change with different |
|||
rencode module versions, so you should check that you are using the |
|||
same rencode version throughout your project. |
|||
""" |
|||
|
|||
import struct |
|||
import sys |
|||
from threading import Lock |
|||
|
|||
try: |
|||
from future_builtins import zip |
|||
except ImportError: |
|||
# Ignore on Py3. |
|||
pass |
|||
|
|||
__version__ = ('Python', 1, 0, 4) |
|||
__all__ = ['dumps', 'loads'] |
|||
|
|||
py3 = sys.version_info[0] >= 3 |
|||
if py3: |
|||
long = int # pylint: disable=redefined-builtin |
|||
unicode = str # pylint: disable=redefined-builtin |
|||
|
|||
def int2byte(c): |
|||
return bytes([c]) |
|||
else: |
|||
def int2byte(c): |
|||
return chr(c) |
|||
|
|||
# Default number of bits for serialized floats, either 32 or 64 (also a parameter for dumps()). |
|||
DEFAULT_FLOAT_BITS = 32 |
|||
|
|||
# Maximum length of integer when written as base 10 string. |
|||
MAX_INT_LENGTH = 64 |
|||
|
|||
# The bencode 'typecodes' such as i, d, etc have been extended and |
|||
# relocated on the base-256 character set. |
|||
CHR_LIST = int2byte(59) |
|||
CHR_DICT = int2byte(60) |
|||
CHR_INT = int2byte(61) |
|||
CHR_INT1 = int2byte(62) |
|||
CHR_INT2 = int2byte(63) |
|||
CHR_INT4 = int2byte(64) |
|||
CHR_INT8 = int2byte(65) |
|||
CHR_FLOAT32 = int2byte(66) |
|||
CHR_FLOAT64 = int2byte(44) |
|||
CHR_TRUE = int2byte(67) |
|||
CHR_FALSE = int2byte(68) |
|||
CHR_NONE = int2byte(69) |
|||
CHR_TERM = int2byte(127) |
|||
|
|||
# Positive integers with value embedded in typecode. |
|||
INT_POS_FIXED_START = 0 |
|||
INT_POS_FIXED_COUNT = 44 |
|||
|
|||
# Dictionaries with length embedded in typecode. |
|||
DICT_FIXED_START = 102 |
|||
DICT_FIXED_COUNT = 25 |
|||
|
|||
# Negative integers with value embedded in typecode. |
|||
INT_NEG_FIXED_START = 70 |
|||
INT_NEG_FIXED_COUNT = 32 |
|||
|
|||
# Strings with length embedded in typecode. |
|||
STR_FIXED_START = 128 |
|||
STR_FIXED_COUNT = 64 |
|||
|
|||
# Lists with length embedded in typecode. |
|||
LIST_FIXED_START = STR_FIXED_START + STR_FIXED_COUNT |
|||
LIST_FIXED_COUNT = 64 |
|||
|
|||
# Whether strings should be decoded when loading |
|||
_decode_utf8 = False |
|||
|
|||
|
|||
def decode_int(x, f): |
|||
f += 1 |
|||
newf = x.index(CHR_TERM, f) |
|||
if newf - f >= MAX_INT_LENGTH: |
|||
raise ValueError('overflow') |
|||
try: |
|||
n = int(x[f:newf]) |
|||
except (OverflowError, ValueError): |
|||
n = long(x[f:newf]) |
|||
if x[f:f + 1] == '-': |
|||
if x[f + 1:f + 2] == '0': |
|||
raise ValueError |
|||
elif x[f:f + 1] == '0' and newf != f + 1: |
|||
raise ValueError |
|||
return (n, newf + 1) |
|||
|
|||
|
|||
def decode_intb(x, f): |
|||
f += 1 |
|||
return (struct.unpack('!b', x[f:f + 1])[0], f + 1) |
|||
|
|||
|
|||
def decode_inth(x, f): |
|||
f += 1 |
|||
return (struct.unpack('!h', x[f:f + 2])[0], f + 2) |
|||
|
|||
|
|||
def decode_intl(x, f): |
|||
f += 1 |
|||
|
|||
return (struct.unpack('!l', x[f:f + 4])[0], f + 4) |
|||
|
|||
|
|||
def decode_intq(x, f): |
|||
f += 1 |
|||
return (struct.unpack('!q', x[f:f + 8])[0], f + 8) |
|||
|
|||
|
|||
def decode_float32(x, f): |
|||
f += 1 |
|||
n = struct.unpack('!f', x[f:f + 4])[0] |
|||
return (n, f + 4) |
|||
|
|||
|
|||
def decode_float64(x, f): |
|||
f += 1 |
|||
n = struct.unpack('!d', x[f:f + 8])[0] |
|||
return (n, f + 8) |
|||
|
|||
|
|||
def decode_string(x, f): |
|||
colon = x.index(b':', f) |
|||
try: |
|||
n = int(x[f:colon]) |
|||
except (OverflowError, ValueError): |
|||
n = long(x[f:colon]) |
|||
if x[f] == '0' and colon != f + 1: |
|||
raise ValueError |
|||
colon += 1 |
|||
s = x[colon:colon + n] |
|||
if _decode_utf8: |
|||
s = s.decode('utf8') |
|||
return (s, colon + n) |
|||
|
|||
|
|||
def decode_list(x, f): |
|||
r, f = [], f + 1 |
|||
while x[f:f + 1] != CHR_TERM: |
|||
v, f = decode_func[x[f:f + 1]](x, f) |
|||
r.append(v) |
|||
return (tuple(r), f + 1) |
|||
|
|||
|
|||
def decode_dict(x, f): |
|||
r, f = {}, f + 1 |
|||
while x[f:f + 1] != CHR_TERM: |
|||
k, f = decode_func[x[f:f + 1]](x, f) |
|||
r[k], f = decode_func[x[f:f + 1]](x, f) |
|||
return (r, f + 1) |
|||
|
|||
|
|||
def decode_true(x, f): |
|||
return (True, f + 1) |
|||
|
|||
|
|||
def decode_false(x, f): |
|||
return (False, f + 1) |
|||
|
|||
|
|||
def decode_none(x, f): |
|||
return (None, f + 1) |
|||
|
|||
|
|||
decode_func = {} |
|||
decode_func[b'0'] = decode_string |
|||
decode_func[b'1'] = decode_string |
|||
decode_func[b'2'] = decode_string |
|||
decode_func[b'3'] = decode_string |
|||
decode_func[b'4'] = decode_string |
|||
decode_func[b'5'] = decode_string |
|||
decode_func[b'6'] = decode_string |
|||
decode_func[b'7'] = decode_string |
|||
decode_func[b'8'] = decode_string |
|||
decode_func[b'9'] = decode_string |
|||
decode_func[CHR_LIST] = decode_list |
|||
decode_func[CHR_DICT] = decode_dict |
|||
decode_func[CHR_INT] = decode_int |
|||
decode_func[CHR_INT1] = decode_intb |
|||
decode_func[CHR_INT2] = decode_inth |
|||
decode_func[CHR_INT4] = decode_intl |
|||
decode_func[CHR_INT8] = decode_intq |
|||
decode_func[CHR_FLOAT32] = decode_float32 |
|||
decode_func[CHR_FLOAT64] = decode_float64 |
|||
decode_func[CHR_TRUE] = decode_true |
|||
decode_func[CHR_FALSE] = decode_false |
|||
decode_func[CHR_NONE] = decode_none |
|||
|
|||
|
|||
def make_fixed_length_string_decoders(): |
|||
def make_decoder(slen): |
|||
def f(x, f): |
|||
s = x[f + 1:f + 1 + slen] |
|||
if _decode_utf8: |
|||
s = s.decode('utf8') |
|||
return (s, f + 1 + slen) |
|||
return f |
|||
for i in range(STR_FIXED_COUNT): |
|||
decode_func[int2byte(STR_FIXED_START + i)] = make_decoder(i) |
|||
|
|||
|
|||
make_fixed_length_string_decoders() |
|||
|
|||
|
|||
def make_fixed_length_list_decoders(): |
|||
def make_decoder(slen): |
|||
def f(x, f): |
|||
r, f = [], f + 1 |
|||
for _ in range(slen): |
|||
v, f = decode_func[x[f:f + 1]](x, f) |
|||
r.append(v) |
|||
return (tuple(r), f) |
|||
return f |
|||
for i in range(LIST_FIXED_COUNT): |
|||
decode_func[int2byte(LIST_FIXED_START + i)] = make_decoder(i) |
|||
|
|||
|
|||
make_fixed_length_list_decoders() |
|||
|
|||
|
|||
def make_fixed_length_int_decoders(): |
|||
def make_decoder(j): |
|||
def f(x, f): |
|||
return (j, f + 1) |
|||
return f |
|||
for i in range(INT_POS_FIXED_COUNT): |
|||
decode_func[int2byte(INT_POS_FIXED_START + i)] = make_decoder(i) |
|||
for i in range(INT_NEG_FIXED_COUNT): |
|||
decode_func[int2byte(INT_NEG_FIXED_START + i)] = make_decoder(-1 - i) |
|||
|
|||
|
|||
make_fixed_length_int_decoders() |
|||
|
|||
|
|||
def make_fixed_length_dict_decoders(): |
|||
def make_decoder(slen): |
|||
def f(x, f): |
|||
r, f = {}, f + 1 |
|||
for _ in range(slen): |
|||
k, f = decode_func[x[f:f + 1]](x, f) |
|||
r[k], f = decode_func[x[f:f + 1]](x, f) |
|||
return (r, f) |
|||
return f |
|||
for i in range(DICT_FIXED_COUNT): |
|||
decode_func[int2byte(DICT_FIXED_START + i)] = make_decoder(i) |
|||
|
|||
|
|||
make_fixed_length_dict_decoders() |
|||
|
|||
|
|||
def loads(x, decode_utf8=False): |
|||
global _decode_utf8 |
|||
_decode_utf8 = decode_utf8 |
|||
try: |
|||
r, l = decode_func[x[0:1]](x, 0) |
|||
except (IndexError, KeyError): |
|||
raise ValueError |
|||
if l != len(x): |
|||
raise ValueError |
|||
return r |
|||
|
|||
|
|||
def encode_int(x, r): |
|||
if 0 <= x < INT_POS_FIXED_COUNT: |
|||
r.append(int2byte(INT_POS_FIXED_START + x)) |
|||
elif -INT_NEG_FIXED_COUNT <= x < 0: |
|||
r.append(int2byte(INT_NEG_FIXED_START - 1 - x)) |
|||
elif -128 <= x < 128: |
|||
r.extend((CHR_INT1, struct.pack('!b', x))) |
|||
elif -32768 <= x < 32768: |
|||
r.extend((CHR_INT2, struct.pack('!h', x))) |
|||
elif -2147483648 <= x < 2147483648: |
|||
r.extend((CHR_INT4, struct.pack('!l', x))) |
|||
elif -9223372036854775808 <= x < 9223372036854775808: |
|||
r.extend((CHR_INT8, struct.pack('!q', x))) |
|||
else: |
|||
s = str(x) |
|||
if py3: |
|||
s = bytes(s, 'ascii') |
|||
|
|||
if len(s) >= MAX_INT_LENGTH: |
|||
raise ValueError('overflow') |
|||
r.extend((CHR_INT, s, CHR_TERM)) |
|||
|
|||
|
|||
def encode_float32(x, r): |
|||
r.extend((CHR_FLOAT32, struct.pack('!f', x))) |
|||
|
|||
|
|||
def encode_float64(x, r): |
|||
r.extend((CHR_FLOAT64, struct.pack('!d', x))) |
|||
|
|||
|
|||
def encode_bool(x, r): |
|||
r.append({False: CHR_FALSE, True: CHR_TRUE}[bool(x)]) |
|||
|
|||
|
|||
def encode_none(x, r): |
|||
r.append(CHR_NONE) |
|||
|
|||
|
|||
def encode_string(x, r): |
|||
if len(x) < STR_FIXED_COUNT: |
|||
r.extend((int2byte(STR_FIXED_START + len(x)), x)) |
|||
else: |
|||
s = str(len(x)) |
|||
if py3: |
|||
s = bytes(s, 'ascii') |
|||
r.extend((s, b':', x)) |
|||
|
|||
|
|||
def encode_unicode(x, r): |
|||
encode_string(x.encode('utf8'), r) |
|||
|
|||
|
|||
def encode_list(x, r): |
|||
if len(x) < LIST_FIXED_COUNT: |
|||
r.append(int2byte(LIST_FIXED_START + len(x))) |
|||
for i in x: |
|||
encode_func[type(i)](i, r) |
|||
else: |
|||
r.append(CHR_LIST) |
|||
for i in x: |
|||
encode_func[type(i)](i, r) |
|||
r.append(CHR_TERM) |
|||
|
|||
|
|||
def encode_dict(x, r): |
|||
if len(x) < DICT_FIXED_COUNT: |
|||
r.append(int2byte(DICT_FIXED_START + len(x))) |
|||
for k, v in x.items(): |
|||
encode_func[type(k)](k, r) |
|||
encode_func[type(v)](v, r) |
|||
else: |
|||
r.append(CHR_DICT) |
|||
for k, v in x.items(): |
|||
encode_func[type(k)](k, r) |
|||
encode_func[type(v)](v, r) |
|||
r.append(CHR_TERM) |
|||
|
|||
|
|||
encode_func = {} |
|||
encode_func[int] = encode_int |
|||
encode_func[long] = encode_int |
|||
encode_func[bytes] = encode_string |
|||
encode_func[list] = encode_list |
|||
encode_func[tuple] = encode_list |
|||
encode_func[dict] = encode_dict |
|||
encode_func[type(None)] = encode_none |
|||
encode_func[unicode] = encode_unicode |
|||
encode_func[bool] = encode_bool |
|||
|
|||
lock = Lock() |
|||
|
|||
|
|||
def dumps(x, float_bits=DEFAULT_FLOAT_BITS): |
|||
""" |
|||
Dump data structure to str. |
|||
|
|||
Here float_bits is either 32 or 64. |
|||
""" |
|||
with lock: |
|||
if float_bits == 32: |
|||
encode_func[float] = encode_float32 |
|||
elif float_bits == 64: |
|||
encode_func[float] = encode_float64 |
|||
else: |
|||
raise ValueError('Float bits (%d) is not 32 or 64' % float_bits) |
|||
r = [] |
|||
encode_func[type(x)](x, r) |
|||
return b''.join(r) |
|||
|
|||
|
|||
def test(): |
|||
f1 = struct.unpack('!f', struct.pack('!f', 25.5))[0] |
|||
f2 = struct.unpack('!f', struct.pack('!f', 29.3))[0] |
|||
f3 = struct.unpack('!f', struct.pack('!f', -0.6))[0] |
|||
ld = (({b'a': 15, b'bb': f1, b'ccc': f2, b'': (f3, (), False, True, b'')}, (b'a', 10**20), |
|||
tuple(range(-100000, 100000)), b'b' * 31, b'b' * 62, b'b' * 64, 2**30, 2**33, 2**62, |
|||
2**64, 2**30, 2**33, 2**62, 2**64, False, False, True, -1, 2, 0),) |
|||
assert loads(dumps(ld)) == ld |
|||
d = dict(zip(range(-100000, 100000), range(-100000, 100000))) |
|||
d.update({b'a': 20, 20: 40, 40: 41, f1: f2, f2: f3, f3: False, False: True, True: False}) |
|||
ld = (d, {}, {5: 6}, {7: 7, True: 8}, {9: 10, 22: 39, 49: 50, 44: b''}) |
|||
assert loads(dumps(ld)) == ld |
|||
ld = (b'', b'a' * 10, b'a' * 100, b'a' * 1000, b'a' * 10000, b'a' * 100000, b'a' * 1000000, b'a' * 10000000) |
|||
assert loads(dumps(ld)) == ld |
|||
ld = tuple([dict(zip(range(n), range(n))) for n in range(100)]) + (b'b',) |
|||
assert loads(dumps(ld)) == ld |
|||
ld = tuple([dict(zip(range(n), range(-n, 0))) for n in range(100)]) + (b'b',) |
|||
assert loads(dumps(ld)) == ld |
|||
ld = tuple([tuple(range(n)) for n in range(100)]) + (b'b',) |
|||
assert loads(dumps(ld)) == ld |
|||
ld = tuple([b'a' * n for n in range(1000)]) + (b'b',) |
|||
assert loads(dumps(ld)) == ld |
|||
ld = tuple([b'a' * n for n in range(1000)]) + (None, True, None) |
|||
assert loads(dumps(ld)) == ld |
|||
assert loads(dumps(None)) is None |
|||
assert loads(dumps({None: None})) == {None: None} |
|||
assert 1e-10 < abs(loads(dumps(1.1)) - 1.1) < 1e-6 |
|||
assert 1e-10 < abs(loads(dumps(1.1, 32)) - 1.1) < 1e-6 |
|||
assert abs(loads(dumps(1.1, 64)) - 1.1) < 1e-12 |
|||
assert loads(dumps('Hello World!!'), decode_utf8=True) |
|||
|
|||
|
|||
try: |
|||
import psyco |
|||
psyco.bind(dumps) |
|||
psyco.bind(loads) |
|||
except ImportError: |
|||
pass |
|||
|
|||
|
|||
if __name__ == '__main__': |
|||
test() |
@ -0,0 +1,65 @@ |
|||
import os |
|||
import sys |
|||
|
|||
import pytest |
|||
|
|||
from .client import DelugeRPCClient, RemoteException |
|||
|
|||
|
|||
if sys.version_info > (3,): |
|||
long = int |
|||
|
|||
|
|||
@pytest.fixture |
|||
def client(request): |
|||
if sys.platform.startswith('win'): |
|||
auth_path = os.path.join(os.getenv('APPDATA'), 'deluge', 'auth') |
|||
else: |
|||
auth_path = os.path.expanduser("~/.config/deluge/auth") |
|||
|
|||
with open(auth_path, 'rb') as f: |
|||
filedata = f.read().decode("utf-8").split('\n')[0].split(':') |
|||
|
|||
username, password = filedata[:2] |
|||
ip = '127.0.0.1' |
|||
port = 58846 |
|||
kwargs = {'decode_utf8': True} |
|||
if hasattr(request, 'param'): |
|||
kwargs.update(request.param) |
|||
client = DelugeRPCClient(ip, port, username, password, **kwargs) |
|||
client.connect() |
|||
|
|||
yield client |
|||
|
|||
try: |
|||
client.disconnect() |
|||
except: |
|||
pass |
|||
|
|||
|
|||
def test_connect(client): |
|||
assert client.connected |
|||
|
|||
|
|||
def test_call_method(client): |
|||
assert isinstance(client.call('core.get_free_space'), (int, long)) |
|||
|
|||
|
|||
def test_call_method_arguments(client): |
|||
assert isinstance(client.call('core.get_free_space', '/'), (int, long)) |
|||
|
|||
|
|||
@pytest.mark.parametrize('client', |
|||
[{'decode_utf8': True}, {'decode_utf8': False}], |
|||
ids=['decode_utf8_on', 'decode_utf8_off'], |
|||
indirect=True) |
|||
def test_call_method_exception(client): |
|||
with pytest.raises(RemoteException) as ex_info: |
|||
client.call('core.get_free_space', '1', '2') |
|||
assert ('takes at most 2 arguments' in str(ex_info.value) or |
|||
'takes from 1 to 2 positional arguments' in str(ex_info.value)) # deluge 2.0 |
|||
|
|||
|
|||
def test_attr_caller(client): |
|||
assert isinstance(client.core.get_free_space(), (int, long)) |
|||
assert isinstance(client.core.get_free_space('/'), (int, long)) |
Loading…
Reference in new issue