mirror of https://github.com/evilhero/mylar
Add Deluge for Torrent downloads
This commit is contained in:
parent
8a10181d32
commit
7cb640500b
|
@ -407,6 +407,7 @@
|
|||
<input type="radio" name="torrent_downloader" id="torrent_downloader_utorrent" value="1" ${config['torrent_downloader_utorrent']}> uTorrent
|
||||
<input type="radio" name="torrent_downloader" id="torrent_downloader_rtorrent" value="2" ${config['torrent_downloader_rtorrent']}> rTorrent
|
||||
<input type="radio" name="torrent_downloader" id="torrent_downloader_transmission" value="3" ${config['torrent_downloader_transmission']}> Transmission
|
||||
<input type="radio" name="torrent_downloader" id="torrent_downloader_deluge" value="4" ${config['torrent_downloader_deluge']}> Deluge
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset id="watchlist_options">
|
||||
|
@ -534,11 +535,30 @@
|
|||
<label>Transmission Password</label>
|
||||
<input type="password" name="transmission_password" value="${config['transmission_password']| h}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Transmission Directory</label>
|
||||
<input type="text" name="transmission_directory" value="${config['transmission_directory']}" size="30"><br/>
|
||||
<small>Folder path where torrent download will be assigned</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Transmission Directory</label>
|
||||
<input type="text" name="transmission_directory" value="${config['transmission_directory']}" size="30"><br/>
|
||||
<small>Folder path where torrent download will be assigned</small>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset id="deluge_options">
|
||||
<div class="row">
|
||||
<label>Deluge Host:Port </label>
|
||||
<input type="text" name="deluge_host" value="${config['deluge_host']}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Deluge Username</label>
|
||||
<input type="text" name="deluge_username" value="${config['deluge_username']}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Deluge Password</label>
|
||||
<input type="password" name="deluge_password" value="${config['deluge_password']}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Deluge Label</label>
|
||||
<input type="text" name="deluge_label" value="${config['deluge_label']}" size="30"><br/>
|
||||
<small>Label to be used on the torrents</small>
|
||||
</div>
|
||||
</fieldset>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -1500,25 +1520,29 @@
|
|||
|
||||
if ($("#torrent_downloader_watchlist").is(":checked"))
|
||||
{
|
||||
$("#utorrent_options,#rtorrent_options,#transmission_options").hide();
|
||||
$("#utorrent_options,#rtorrent_options,#transmission_options,#deluge_options").hide();
|
||||
$("#watchlist_options").show();
|
||||
}
|
||||
if ($("#torrent_downloader_utorrent").is(":checked"))
|
||||
{
|
||||
$("#watchlist_options,#rtorrent_options,#transmission_options").hide();
|
||||
$("#watchlist_options,#rtorrent_options,#transmission_options,#deluge_options").hide();
|
||||
$("#utorrent_options").show();
|
||||
}
|
||||
if ($("#torrent_downloader_rtorrent").is(":checked"))
|
||||
{
|
||||
$("#utorrent_options,#watchlist_options,#transmission_options").hide();
|
||||
$("#utorrent_options,#watchlist_options,#transmission_options,#deluge_options").hide();
|
||||
$("#rtorrent_options").show();
|
||||
}
|
||||
if ($("#torrent_downloader_transmission").is(":checked"))
|
||||
{
|
||||
$("#utorrent_options,#rtorrent_options,#watchlist_options").hide();
|
||||
$("#utorrent_options,#rtorrent_options,#watchlist_options,#deluge_options").hide();
|
||||
$("#transmission_options").show();
|
||||
}
|
||||
|
||||
if ($("#torrent_downloader_deluge").is(":checked"))
|
||||
{
|
||||
$("#utorrent_options,#rtorrent_options,#watchlist_options,#transmission_options").hide();
|
||||
$("#deluge_options").show();
|
||||
}
|
||||
|
||||
$('input[type=radio]').change(function(){
|
||||
if ($("#nzb_downloader_sabnzbd").is(":checked"))
|
||||
|
@ -1535,19 +1559,23 @@
|
|||
}
|
||||
if ($("#torrent_downloader_watchlist").is(":checked"))
|
||||
{
|
||||
$("#utorrent_options,#rtorrent_options,#transmission_options").fadeOut("fast", function() { $("#watchlist_options").fadeIn() });
|
||||
$("#utorrent_options,#rtorrent_options,#transmission_options,#deluge_options").fadeOut("fast", function() { $("#watchlist_options").fadeIn() });
|
||||
}
|
||||
if ($("#torrent_downloader_utorrent").is(":checked"))
|
||||
{
|
||||
$("#watchlist_options,#rtorrent_options,#transmission_options").fadeOut("fast", function() { $("#utorrent_options").fadeIn() });
|
||||
$("#watchlist_options,#rtorrent_options,#transmission_options,#deluge_options").fadeOut("fast", function() { $("#utorrent_options").fadeIn() });
|
||||
}
|
||||
if ($("#torrent_downloader_rtorrent").is(":checked"))
|
||||
{
|
||||
$("#utorrent_options,#watchlist_options,#transmission_options").fadeOut("fast", function() { $("#rtorrent_options").fadeIn() });
|
||||
$("#utorrent_options,#watchlist_options,#transmission_options,#deluge_options").fadeOut("fast", function() { $("#rtorrent_options").fadeIn() });
|
||||
}
|
||||
if ($("#torrent_downloader_transmission").is(":checked"))
|
||||
{
|
||||
$("#utorrent_options,#rtorrent_options,#watchlist_options").fadeOut("fast", function() { $("#transmission_options").fadeIn() });
|
||||
$("#utorrent_options,#rtorrent_options,#watchlist_options,#deluge_options").fadeOut("fast", function() { $("#transmission_options").fadeIn() });
|
||||
}
|
||||
if ($("#torrent_downloader_deluge").is(":checked"))
|
||||
{
|
||||
$("#utorrent_options,#rtorrent_options,#watchlist_options,#transmission_options").fadeOut("fast", function() { $("#deluge_options").fadeIn() });
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
Copyright (C) 2015 Anders Jensen
|
||||
|
||||
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.
|
|
@ -0,0 +1 @@
|
|||
from .client import DelugeRPCClient
|
|
@ -0,0 +1,111 @@
|
|||
import logging
|
||||
import socket
|
||||
import ssl
|
||||
import struct
|
||||
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 ConnectionLostException(Exception):
|
||||
pass
|
||||
|
||||
class CallTimeoutException(Exception):
|
||||
pass
|
||||
|
||||
class DelugeRPCClient(object):
|
||||
timeout = 20
|
||||
|
||||
def __init__(self, host, port, username, password):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
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
|
||||
"""
|
||||
logger.info('Connecting to %s:%s' % (self.host, self.port))
|
||||
try:
|
||||
self._socket.connect((self.host, self.port))
|
||||
except ssl.SSLError as e:
|
||||
if e.reason != 'UNSUPPORTED_PROTOCOL' or not hasattr(ssl, 'PROTOCOL_SSLv3'):
|
||||
raise
|
||||
|
||||
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))
|
||||
|
||||
logger.debug('Connected to Deluge, logging in')
|
||||
result = self.call('daemon.login', self.username, self.password)
|
||||
logger.debug('Logged in with value %r' % result)
|
||||
self.connected = True
|
||||
|
||||
def disconnect(self):
|
||||
"""
|
||||
Disconnect from deluge
|
||||
"""
|
||||
if self.connected:
|
||||
self._socket.close()
|
||||
|
||||
def call(self, method, *args, **kwargs):
|
||||
"""
|
||||
Calls an RPC function
|
||||
"""
|
||||
self.request_id += 1
|
||||
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))
|
||||
|
||||
#self._socket.send('D' + struct.pack("!i", len(req))) # seems to be for the future !
|
||||
self._socket.send(req)
|
||||
|
||||
data = b''
|
||||
while True:
|
||||
try:
|
||||
d = self._socket.recv(READ_SIZE)
|
||||
except ssl.SSLError:
|
||||
raise CallTimeoutException()
|
||||
|
||||
data += d
|
||||
try:
|
||||
data = zlib.decompress(data)
|
||||
except zlib.error:
|
||||
if not d:
|
||||
raise ConnectionLostException()
|
||||
continue
|
||||
break
|
||||
|
||||
data = list(loads(data))
|
||||
msg_type = data.pop(0)
|
||||
request_id = data.pop(0)
|
||||
|
||||
if msg_type == RPC_ERROR:
|
||||
exception_type, exception_msg, traceback = data[0]
|
||||
exception = type(str(exception_type), (Exception, ), {})
|
||||
exception_msg = '%s\n\n%s' % (exception_msg, traceback)
|
||||
raise exception(exception_msg)
|
||||
elif msg_type == RPC_RESPONSE:
|
||||
retval = data[0]
|
||||
return retval
|
|
@ -0,0 +1,436 @@
|
|||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
__version__ = '1.0.2'
|
||||
__all__ = ['dumps', 'loads']
|
||||
|
||||
# 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).
|
||||
#
|
||||
|
||||
import sys
|
||||
|
||||
py3 = False
|
||||
if sys.version_info >= (3, 0):
|
||||
py3 = True
|
||||
long = int
|
||||
unicode = str
|
||||
|
||||
def int2byte(c):
|
||||
if py3:
|
||||
return bytes([c])
|
||||
else:
|
||||
return chr(c)
|
||||
|
||||
import struct
|
||||
from threading import Lock
|
||||
|
||||
# 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 i 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 j 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.
|
||||
"""
|
||||
lock.acquire()
|
||||
try:
|
||||
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)
|
||||
finally:
|
||||
lock.release()
|
||||
|
||||
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]
|
||||
L = (({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(L)) == L
|
||||
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})
|
||||
L = (d, {}, {5:6}, {7:7,True:8}, {9:10, 22:39, 49:50, 44: b''})
|
||||
assert loads(dumps(L)) == L
|
||||
L = (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(L)) == L
|
||||
L = tuple([dict(zip(range(n),range(n))) for n in range(100)]) + (b'b',)
|
||||
assert loads(dumps(L)) == L
|
||||
L = tuple([dict(zip(range(n),range(-n,0))) for n in range(100)]) + (b'b',)
|
||||
assert loads(dumps(L)) == L
|
||||
L = tuple([tuple(range(n)) for n in range(100)]) + (b'b',)
|
||||
assert loads(dumps(L)) == L
|
||||
L = tuple([b'a'*n for n in range(1000)]) + (b'b',)
|
||||
assert loads(dumps(L)) == L
|
||||
L = tuple([b'a'*n for n in range(1000)]) + (None,True,None)
|
||||
assert loads(dumps(L)) == L
|
||||
assert loads(dumps(None)) == 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,41 @@
|
|||
import os
|
||||
|
||||
from unittest import TestCase
|
||||
|
||||
from .client import DelugeRPCClient
|
||||
|
||||
class TestDelugeClient(TestCase):
|
||||
def setUp(self):
|
||||
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(':')
|
||||
|
||||
self.username, self.password = filedata[:2]
|
||||
self.ip = '127.0.0.1'
|
||||
self.port = 58846
|
||||
self.client = DelugeRPCClient(self.ip, self.port, self.username, self.password)
|
||||
|
||||
def tearDown(self):
|
||||
try:
|
||||
self.client.disconnect()
|
||||
except:
|
||||
pass
|
||||
|
||||
def test_connect(self):
|
||||
self.client.connect()
|
||||
|
||||
def test_call_method(self):
|
||||
self.client.connect()
|
||||
self.assertIsInstance(self.client.call('core.get_free_space'), int)
|
||||
|
||||
def test_call_method_arguments(self):
|
||||
self.client.connect()
|
||||
self.assertIsInstance(self.client.call('core.get_free_space', '/'), int)
|
||||
|
||||
def test_call_method_exception(self):
|
||||
self.client.connect()
|
||||
try:
|
||||
self.client.call('core.get_free_space', '1', '2')
|
||||
except Exception as e:
|
||||
self.assertEqual('deluge_client.client', e.__module__)
|
|
@ -423,6 +423,13 @@ TRANSMISSION_USERNAME = None
|
|||
TRANSMISSION_PASSWORD = None
|
||||
TRANSMISSION_DIRECTORY = None
|
||||
|
||||
USE_DELUGE = False
|
||||
DELUGE_HOST = None
|
||||
DELUGE_USERNAME = None
|
||||
DELUGE_PASSWORD = None
|
||||
DELUGE_LABEL = None
|
||||
|
||||
|
||||
def CheckSection(sec):
|
||||
""" Check if INI section exists, if not create it """
|
||||
try:
|
||||
|
@ -482,7 +489,7 @@ def initialize():
|
|||
NEWZNAB, NEWZNAB_NAME, NEWZNAB_HOST, NEWZNAB_APIKEY, NEWZNAB_VERIFY, NEWZNAB_UID, NEWZNAB_ENABLED, EXTRA_NEWZNABS, NEWZNAB_EXTRA, \
|
||||
ENABLE_TORZNAB, TORZNAB_NAME, TORZNAB_HOST, TORZNAB_APIKEY, TORZNAB_CATEGORY, TORZNAB_VERIFY, EXPERIMENTAL, ALTEXPERIMENTAL, \
|
||||
USE_RTORRENT, RTORRENT_HOST, RTORRENT_AUTHENTICATION, RTORRENT_RPC_URL, RTORRENT_SSL, RTORRENT_VERIFY, RTORRENT_CA_BUNDLE, RTORRENT_USERNAME, RTORRENT_PASSWORD, RTORRENT_STARTONLOAD, RTORRENT_LABEL, RTORRENT_DIRECTORY, \
|
||||
USE_UTORRENT, UTORRENT_HOST, UTORRENT_USERNAME, UTORRENT_PASSWORD, UTORRENT_LABEL, USE_TRANSMISSION, TRANSMISSION_HOST, TRANSMISSION_USERNAME, TRANSMISSION_PASSWORD, TRANSMISSION_DIRECTORY, \
|
||||
USE_UTORRENT, UTORRENT_HOST, UTORRENT_USERNAME, UTORRENT_PASSWORD, UTORRENT_LABEL, USE_TRANSMISSION, TRANSMISSION_HOST, TRANSMISSION_USERNAME, TRANSMISSION_PASSWORD, TRANSMISSION_DIRECTORY, USE_DELUGE, DELUGE_HOST, DELUGE_USERNAME, DELUGE_PASSWORD, DELUGE_LABEL, \
|
||||
ENABLE_META, CMTAGGER_PATH, CBR2CBZ_ONLY, CT_TAG_CR, CT_TAG_CBL, CT_CBZ_OVERWRITE, UNRAR_CMD, CT_SETTINGSPATH, CMTAG_START_YEAR_AS_VOLUME, UPDATE_ENDED, INDIE_PUB, BIGGIE_PUB, IGNORE_HAVETOTAL, SNATCHED_HAVETOTAL, PROVIDER_ORDER, TMP_PROV, \
|
||||
dbUpdateScheduler, searchScheduler, RSSScheduler, WeeklyScheduler, VersionScheduler, FolderMonitorScheduler, \
|
||||
ALLOW_PACKS, ENABLE_TORRENTS, TORRENT_DOWNLOADER, MINSEEDS, USE_WATCHDIR, TORRENT_LOCAL, LOCAL_WATCHDIR, TORRENT_SEEDBOX, SEEDBOX_HOST, SEEDBOX_PORT, SEEDBOX_USER, SEEDBOX_PASS, SEEDBOX_WATCHDIR, \
|
||||
|
@ -836,6 +843,9 @@ def initialize():
|
|||
elif TORRENT_DOWNLOADER == 3:
|
||||
TORRENT_LOCAL = False
|
||||
USE_TRANSMISSION = True
|
||||
elif TORRENT_DOWNLOADER == 4:
|
||||
TORRENT_LOCAL = False
|
||||
USE_DELUGE = True
|
||||
else:
|
||||
TORRENT_DOWNLOADER = 0
|
||||
USE_WATCHDIR = True
|
||||
|
@ -851,6 +861,11 @@ def initialize():
|
|||
TRANSMISSION_PASSWORD = check_setting_str(CFG, 'Transmission', 'transmission_password', '')
|
||||
TRANSMISSION_DIRECTORY = check_setting_str(CFG, 'Transmission', 'transmission_directory', '')
|
||||
|
||||
DELUGE_HOST = check_setting_str(CFG, 'Deluge', 'deluge_host', '')
|
||||
DELUGE_USERNAME = check_setting_str(CFG, 'Deluge', 'deluge_username', '')
|
||||
DELUGE_PASSWORD = check_setting_str(CFG, 'Deluge', 'deluge_password', '')
|
||||
DELUGE_LABEL = check_setting_str(CFG, 'Deluge', 'deluge_label', '')
|
||||
|
||||
#add torrents to provider counter.
|
||||
if ENABLE_TORRENT_SEARCH:
|
||||
if ENABLE_32P:
|
||||
|
@ -1598,6 +1613,12 @@ def config_write():
|
|||
new_config['Transmission']['transmission_password'] = TRANSMISSION_PASSWORD
|
||||
new_config['Transmission']['transmission_directory'] = TRANSMISSION_DIRECTORY
|
||||
|
||||
new_config['Deluge'] = {}
|
||||
new_config['Deluge']['deluge_host'] = DELUGE_HOST
|
||||
new_config['Deluge']['deluge_username'] = DELUGE_USERNAME
|
||||
new_config['Deluge']['deluge_password'] = DELUGE_PASSWORD
|
||||
new_config['Deluge']['deluge_label'] = DELUGE_LABEL
|
||||
|
||||
# Need to unpack the extra newznabs for saving in config.ini
|
||||
flattened_newznabs = []
|
||||
for newznab in EXTRA_NEWZNABS:
|
||||
|
|
|
@ -15,7 +15,7 @@ from StringIO import StringIO
|
|||
import mylar
|
||||
from mylar import db, logger, ftpsshup, helpers, auth32p, utorrent
|
||||
import torrent.clients.transmission as transmission
|
||||
|
||||
import torrent.clients.deluge as deluge
|
||||
|
||||
def _start_newznab_attr(self, attrsD):
|
||||
context = self._getContext()
|
||||
|
@ -764,7 +764,7 @@ def torsend2client(seriesname, issue, seriesyear, linkit, site):
|
|||
|
||||
if linkit[-7:] != "torrent":
|
||||
filename += ".torrent"
|
||||
if any([mylar.USE_UTORRENT, mylar.USE_RTORRENT, mylar.USE_TRANSMISSION]):
|
||||
if any([mylar.USE_UTORRENT, mylar.USE_RTORRENT, mylar.USE_TRANSMISSION,mylar.USE_DELUGE]):
|
||||
filepath = os.path.join(mylar.CACHE_DIR, filename)
|
||||
logger.fdebug('filename for torrent set to : ' + filepath)
|
||||
|
||||
|
@ -1008,6 +1008,24 @@ def torsend2client(seriesname, issue, seriesyear, linkit, site):
|
|||
logger.error(e)
|
||||
return "fail"
|
||||
|
||||
elif mylar.USE_DELUGE:
|
||||
try:
|
||||
dc = deluge.TorrentClient()
|
||||
if not dc.connect(mylar.DELUGE_HOST, mylar.DELUGE_USERNAME, mylar.DELUGE_PASSWORD):
|
||||
return "fail"
|
||||
logger.info('Not connected to Deluge! (rsscheck)')
|
||||
else:
|
||||
logger.info('Connected to Deluge! Will try to add torrent now! (rsscheck)')
|
||||
if dc.load_torrent(filepath):
|
||||
return "pass"
|
||||
else:
|
||||
return "fail"
|
||||
logger.info('Unable to connect to Deluge (rsscheck)')
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
return "fail"
|
||||
|
||||
|
||||
elif mylar.USE_WATCHDIR:
|
||||
if mylar.TORRENT_LOCAL:
|
||||
return "pass"
|
||||
|
|
|
@ -2182,6 +2182,8 @@ def searcher(nzbprov, nzbname, comicinfo, link, IssueID, ComicID, tmpprov, direc
|
|||
sent_to = "your rTorrent client"
|
||||
elif mylar.USE_TRANSMISSION:
|
||||
sent_to = "your Transmission client"
|
||||
elif mylar.USE_DELUGE:
|
||||
sent_to = "your Deluge client"
|
||||
#end torrents
|
||||
|
||||
else:
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
import os
|
||||
import mylar
|
||||
import base64
|
||||
from mylar import logger, helpers
|
||||
|
||||
from deluge_client import DelugeRPCClient
|
||||
|
||||
class TorrentClient(object):
|
||||
def __init__(self):
|
||||
self.conn = None
|
||||
|
||||
def connect(self, host, username, password):
|
||||
if self.conn is not None:
|
||||
return self.connect
|
||||
|
||||
if not host:
|
||||
return False
|
||||
|
||||
# Get port from the config
|
||||
host,portnr = host.split(':')
|
||||
|
||||
|
||||
#if username and password:
|
||||
logger.info('Connecting to ' + host + ':' + portnr + ' Username: ' + username + ' Password: ' + password )
|
||||
self.client = DelugeRPCClient(host,int(portnr),username,password)
|
||||
self.client.connect()
|
||||
|
||||
logger.info('connected? ' + str(self.client.connected))
|
||||
|
||||
return self.client
|
||||
|
||||
|
||||
def find_torrent(self, hash):
|
||||
#To be Added
|
||||
return False
|
||||
|
||||
def get_torrent(self, torrent):
|
||||
# To be Added
|
||||
return False
|
||||
|
||||
def start_torrent(self, torrent):
|
||||
return torrent.start()
|
||||
|
||||
def stop_torrent(self, torrent):
|
||||
return torrent.stop()
|
||||
|
||||
def load_torrent(self, filepath):
|
||||
logger.info('filepath to torrent file set to : ' + filepath)
|
||||
torrent_id = False
|
||||
|
||||
if self.client.connected is True:
|
||||
logger.info('Trying to send to deluge now')
|
||||
# Open torrent to endcode
|
||||
|
||||
torrentfile = open(filepath, 'rb')
|
||||
torrentcontent = torrentfile.read()
|
||||
torrentencode = base64.encodestring(torrentcontent)
|
||||
|
||||
# Send to Deluge and return torrent_id
|
||||
logger.info('FileName: ' + str(os.path.basename(filepath)))
|
||||
|
||||
|
||||
torrent_id = self.client.call('core.add_torrent_file', str(os.path.basename(filepath)), torrentencode, '')
|
||||
|
||||
if not torrent_id:
|
||||
logger.error('Torrent not added')
|
||||
return False
|
||||
else:
|
||||
logger.info('TorrentID: ' + torrent_id)
|
||||
|
||||
# If label enabled put label on torrent in Deluge
|
||||
logger.info('Label: ' + mylar.DELUGE_LABEL)
|
||||
if torrent_id and mylar.DELUGE_LABEL:
|
||||
logger.info ('Setting label to ' + mylar.DELUGE_LABEL)
|
||||
self.client.call('label.set_torrent', torrent_id, mylar.DELUGE_LABEL)
|
||||
return True
|
||||
|
||||
|
||||
def delete_torrent(self, torrent):
|
||||
# To be Added
|
||||
return False
|
||||
|
||||
|
|
@ -3960,6 +3960,7 @@ class WebInterface(object):
|
|||
"torrent_downloader_utorrent": helpers.radio(mylar.TORRENT_DOWNLOADER, 1),
|
||||
"torrent_downloader_rtorrent": helpers.radio(mylar.TORRENT_DOWNLOADER, 2),
|
||||
"torrent_downloader_transmission": helpers.radio(mylar.TORRENT_DOWNLOADER, 3),
|
||||
"torrent_downloader_deluge": helpers.radio(mylar.TORRENT_DOWNLOADER, 4),
|
||||
"utorrent_host": mylar.UTORRENT_HOST,
|
||||
"utorrent_username": mylar.UTORRENT_USERNAME,
|
||||
"utorrent_password": mylar.UTORRENT_PASSWORD,
|
||||
|
@ -3978,6 +3979,10 @@ class WebInterface(object):
|
|||
"transmission_username": mylar.TRANSMISSION_USERNAME,
|
||||
"transmission_password": mylar.TRANSMISSION_PASSWORD,
|
||||
"transmission_directory": mylar.TRANSMISSION_DIRECTORY,
|
||||
"deluge_host": mylar.DELUGE_HOST,
|
||||
"deluge_username": mylar.DELUGE_USERNAME,
|
||||
"deluge_password": mylar.DELUGE_PASSWORD,
|
||||
"deluge_label": mylar.DELUGE_LABEL,
|
||||
"blackhole_dir": mylar.BLACKHOLE_DIR,
|
||||
"usenet_retention": mylar.USENET_RETENTION,
|
||||
"use_nzbsu": helpers.checked(mylar.NZBSU),
|
||||
|
@ -4320,7 +4325,7 @@ class WebInterface(object):
|
|||
enable_torrents=0, minseeds=0, local_watchdir=None, seedbox_watchdir=None, seedbox_user=None, seedbox_pass=None, seedbox_host=None, seedbox_port=None,
|
||||
prowl_enabled=0, prowl_onsnatch=0, prowl_keys=None, prowl_priority=None, nma_enabled=0, nma_apikey=None, nma_priority=0, nma_onsnatch=0, pushover_enabled=0, pushover_onsnatch=0, pushover_apikey=None, pushover_userkey=None, pushover_priority=None, boxcar_enabled=0, boxcar_onsnatch=0, boxcar_token=None,
|
||||
pushbullet_enabled=0, pushbullet_apikey=None, pushbullet_deviceid=None, pushbullet_onsnatch=0, telegram_enabled=0, telegram_token=None, telegram_userid=None, telegram_onsnatch=0, torrent_downloader=0, torrent_local=0, torrent_seedbox=0, utorrent_host=None, utorrent_username=None, utorrent_password=None, utorrent_label=None,
|
||||
rtorrent_host=None, rtorrent_ssl=0, rtorrent_verify=0, rtorrent_authentication='basic', rtorrent_rpc_url=None, rtorrent_username=None, rtorrent_password=None, rtorrent_directory=None, rtorrent_label=None, rtorrent_startonload=0, transmission_host=None, transmission_username=None, transmission_password=None, transmission_directory=None,
|
||||
rtorrent_host=None, rtorrent_ssl=0, rtorrent_verify=0, rtorrent_authentication='basic', rtorrent_rpc_url=None, rtorrent_username=None, rtorrent_password=None, rtorrent_directory=None, rtorrent_label=None, rtorrent_startonload=0, transmission_host=None, transmission_username=None, transmission_password=None, transmission_directory=None,deluge_host=None, deluge_username=None, deluge_password=None, deluge_label=None,
|
||||
preferred_quality=0, move_files=0, rename_files=0, add_to_csv=1, cvinfo=0, lowercase_filenames=0, folder_format=None, file_format=None, enable_extra_scripts=0, extra_scripts=None, enable_pre_scripts=0, pre_scripts=None, post_processing=0, file_opts=None, syno_fix=0, search_delay=None, enforce_perms=1, chmod_dir=0777, chmod_file=0660, chowner=None, chgroup=None,
|
||||
tsab=None, destination_dir=None, create_folders=1, replace_spaces=0, replace_char=None, use_minsize=0, minsize=None, use_maxsize=0, maxsize=None, autowant_all=0, autowant_upcoming=0, comic_cover_local=0, zero_level=0, zero_level_n=None, interface=None, dupeconstraint=None, ddump=0, duplicate_dump=None, **kwargs):
|
||||
mylar.COMICVINE_API = comicvine_api
|
||||
|
@ -4412,6 +4417,10 @@ class WebInterface(object):
|
|||
mylar.TRANSMISSION_USERNAME = transmission_username
|
||||
mylar.TRANSMISSION_PASSWORD = transmission_password
|
||||
mylar.TRANSMISSION_DIRECTORY = transmission_directory
|
||||
mylar.DELUGE_HOST = deluge_host
|
||||
mylar.DELUGE_USERNAME = deluge_username
|
||||
mylar.DELUGE_PASSWORD = deluge_password
|
||||
mylar.DELUGE_LABEL = deluge_label
|
||||
mylar.ENABLE_TORRENT_SEARCH = int(enable_torrent_search)
|
||||
mylar.ENABLE_TPSE = int(enable_tpse)
|
||||
mylar.ENABLE_32P = int(enable_32p)
|
||||
|
|
Loading…
Reference in New Issue