From 9f193dba8090da2d7a551d6dbb1645db556ecefd Mon Sep 17 00:00:00 2001 From: Jason Cline Date: Thu, 5 May 2016 11:40:10 -0400 Subject: [PATCH] uTorrent Support --- mylar/bencode.py | 131 ++++++++++++++++++++++++++++ mylar/utorrent.py | 218 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 349 insertions(+) create mode 100644 mylar/bencode.py create mode 100644 mylar/utorrent.py diff --git a/mylar/bencode.py b/mylar/bencode.py new file mode 100644 index 00000000..f91ae02a --- /dev/null +++ b/mylar/bencode.py @@ -0,0 +1,131 @@ +# Got this from here: https://gist.github.com/1126793 + +# The contents of this file are subject to the Python Software Foundation +# License Version 2.3 (the License). You may not copy or use this file, in +# either source code or executable form, except in compliance with the License. +# You may obtain a copy of the License at http://www.python.org/license. +# +# Software distributed under the License is distributed on an AS IS basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. + +# Written by Petru Paler + +# Minor modifications made by Andrew Resch to replace the BTFailure errors with Exceptions + +def decode_int(x, f): + f += 1 + newf = x.index('e', f) + n = int(x[f:newf]) + if x[f] == '-': + if x[f + 1] == '0': + raise ValueError + elif x[f] == '0' and newf != f+1: + raise ValueError + return (n, newf+1) + +def decode_string(x, f): + colon = x.index(':', f) + n = int(x[f:colon]) + if x[f] == '0' and colon != f+1: + raise ValueError + colon += 1 + return (x[colon:colon+n], colon+n) + +def decode_list(x, f): + r, f = [], f+1 + while x[f] != 'e': + v, f = decode_func[x[f]](x, f) + r.append(v) + return (r, f + 1) + +def decode_dict(x, f): + r, f = {}, f+1 + while x[f] != 'e': + k, f = decode_string(x, f) + r[k], f = decode_func[x[f]](x, f) + return (r, f + 1) + +decode_func = {} +decode_func['l'] = decode_list +decode_func['d'] = decode_dict +decode_func['i'] = decode_int +decode_func['0'] = decode_string +decode_func['1'] = decode_string +decode_func['2'] = decode_string +decode_func['3'] = decode_string +decode_func['4'] = decode_string +decode_func['5'] = decode_string +decode_func['6'] = decode_string +decode_func['7'] = decode_string +decode_func['8'] = decode_string +decode_func['9'] = decode_string + +def bdecode(x): + try: + r, l = decode_func[x[0]](x, 0) + except (IndexError, KeyError, ValueError): + raise Exception("not a valid bencoded string") + + return r + +from types import StringType, IntType, LongType, DictType, ListType, TupleType + + +class Bencached(object): + + __slots__ = ['bencoded'] + + def __init__(self, s): + self.bencoded = s + +def encode_bencached(x,r): + r.append(x.bencoded) + +def encode_int(x, r): + r.extend(('i', str(x), 'e')) + +def encode_bool(x, r): + if x: + encode_int(1, r) + else: + encode_int(0, r) + +def encode_string(x, r): + r.extend((str(len(x)), ':', x)) + +def encode_list(x, r): + r.append('l') + for i in x: + encode_func[type(i)](i, r) + r.append('e') + +def encode_dict(x,r): + r.append('d') + ilist = x.items() + ilist.sort() + for k, v in ilist: + r.extend((str(len(k)), ':', k)) + encode_func[type(v)](v, r) + r.append('e') + +encode_func = {} +encode_func[Bencached] = encode_bencached +encode_func[IntType] = encode_int +encode_func[LongType] = encode_int +encode_func[StringType] = encode_string +encode_func[ListType] = encode_list +encode_func[TupleType] = encode_list +encode_func[DictType] = encode_dict + +try: + from types import BooleanType + encode_func[BooleanType] = encode_bool +except ImportError: + pass + +def bencode(x): + r = [] + encode_func[type(x)](x, r) + return ''.join(r) \ No newline at end of file diff --git a/mylar/utorrent.py b/mylar/utorrent.py new file mode 100644 index 00000000..fdf43496 --- /dev/null +++ b/mylar/utorrent.py @@ -0,0 +1,218 @@ +# This file is part of Mylar and is adapted from Headphones. + +import hashlib +import urllib +import json +import time +from collections import namedtuple +import urllib2 +import urlparse +import cookielib + +import re +import os +import mylar +from mylar import logger +from bencode import bencode, bdecode +from hashlib import sha1 + + +class utorrentclient(object): + TOKEN_REGEX = "" + UTSetting = namedtuple("UTSetting", ["name", "int", "str", "access"]) + + def __init__(self, base_url=None, username=None, password=None, ): + + host = mylar.UTORRENT_HOST + if not host.startswith('http'): + host = 'http://' + host + + if host.endswith('/'): + host = host[:-1] + + if host.endswith('/gui'): + host = host[:-4] + + self.base_url = host + self.username = mylar.UTORRENT_USERNAME + self.password = mylar.UTORRENT_PASSWORD + self.opener = self._make_opener('uTorrent', self.base_url, self.username, self.password) + self.token = self._get_token() + # TODO refresh token, when necessary + + def _make_opener(self, realm, base_url, username, password): + """uTorrent API need HTTP Basic Auth and cookie support for token verify.""" + auth = urllib2.HTTPBasicAuthHandler() + auth.add_password(realm=realm, uri=base_url, user=username, passwd=password) + opener = urllib2.build_opener(auth) + urllib2.install_opener(opener) + + cookie_jar = cookielib.CookieJar() + cookie_handler = urllib2.HTTPCookieProcessor(cookie_jar) + + handlers = [auth, cookie_handler] + opener = urllib2.build_opener(*handlers) + return opener + + def _get_token(self): + url = urlparse.urljoin(self.base_url, 'gui/token.html') + try: + response = self.opener.open(url) + except urllib2.HTTPError as err: + logger.debug('URL: ' + str(url)) + logger.debug('Error getting Token. uTorrent responded with error: ' + str(err)) + return + match = re.search(utorrentclient.TOKEN_REGEX, response.read()) + return match.group(1) + + def list(self, **kwargs): + params = [('list', '1')] + params += kwargs.items() + return self._action(params) + + def add_url(self, url): + # can receive magnet or normal .torrent link + params = [('action', 'add-url'), ('s', url)] + return self._action(params) + + def start(self, *hashes): + params = [('action', 'start'), ] + for hash in hashes: + params.append(('hash', hash)) + return self._action(params) + + def stop(self, *hashes): + params = [('action', 'stop'), ] + for hash in hashes: + params.append(('hash', hash)) + return self._action(params) + + def pause(self, *hashes): + params = [('action', 'pause'), ] + for hash in hashes: + params.append(('hash', hash)) + return self._action(params) + + def forcestart(self, *hashes): + params = [('action', 'forcestart'), ] + for hash in hashes: + params.append(('hash', hash)) + return self._action(params) + + def getfiles(self, hash): + params = [('action', 'getfiles'), ('hash', hash)] + return self._action(params) + + def getprops(self, hash): + params = [('action', 'getprops'), ('hash', hash)] + return self._action(params) + + def setprops(self, hash, s, val): + params = [('action', 'setprops'), ('hash', hash), ("s", s), ("v", val)] + logger.debug('Params: ' + str(params)) + return self._action(params) + + def setprio(self, hash, priority, *files): + params = [('action', 'setprio'), ('hash', hash), ('p', str(priority))] + for file_index in files: + params.append(('f', str(file_index))) + + return self._action(params) + + def get_settings(self, key=None): + params = [('action', 'getsettings'), ] + status, value = self._action(params) + settings = {} + for args in value['settings']: + settings[args[0]] = self.UTSetting(*args) + if key: + return settings[key] + return settings + + def remove(self, hash, remove_data=False): + if remove_data: + params = [('action', 'removedata'), ('hash', hash)] + else: + params = [('action', 'remove'), ('hash', hash)] + return self._action(params) + + def _action(self, params, body=None, content_type=None): + + if not self.token: + return + + url = self.base_url + '/gui/' + '?token=' + self.token + '&' + urllib.urlencode(params) + request = urllib2.Request(url) + + if body: + request.add_data(body) + request.add_header('Content-length', len(body)) + if content_type: + request.add_header('Content-type', content_type) + + try: + response = self.opener.open(request) + return response.code, json.loads(response.read()) + except urllib2.HTTPError as err: + logger.debug('URL: ' + str(url)) + logger.debug('uTorrent webUI raised the following error: ' + str(err)) + + +def labelTorrent(hash): + label = mylar.UTORRENT_LABEL + uTorrentClient = utorrentclient() + if label: + uTorrentClient.setprops(hash, 'label', str(label)) + + +def removeTorrent(hash, remove_data=False): + uTorrentClient = utorrentclient() + status, torrentList = uTorrentClient.list() + torrents = torrentList['torrents'] + for torrent in torrents: + if torrent[0].upper() == hash.upper(): + if torrent[21] == 'Finished': + logger.info('%s has finished seeding, removing torrent and data' % torrent[2]) + uTorrentClient.remove(hash, remove_data) + return True + else: + logger.info( + '%s has not finished seeding yet, torrent will not be removed, will try again on next run' % + torrent[2]) + return False + return False + + +def setSeedRatio(hash, ratio): + uTorrentClient = utorrentclient() + uTorrentClient.setprops(hash, 'seed_override', '1') + if ratio != 0: + uTorrentClient.setprops(hash, 'seed_ratio', ratio * 10) + else: + # TODO passing -1 should be unlimited + uTorrentClient.setprops(hash, 'seed_ratio', -10) + +def addTorrent(link): + uTorrentClient = utorrentclient() + uTorrentClient.add_url(link) + + +def calculate_torrent_hash(link, data=None): + """ + Calculate the torrent hash from a magnet link or data. Raises a ValueError + when it cannot create a torrent hash given the input data. + """ + + if link.startswith("magnet:"): + torrent_hash = re.findall("urn:btih:([\w]{32,40})", link)[0] + if len(torrent_hash) == 32: + torrent_hash = b16encode(b32decode(torrent_hash)).lower() + elif data: + info = bdecode(data)["info"] + torrent_hash = sha1(bencode(info)).hexdigest() + else: + raise ValueError("Cannot calculate torrent hash without magnet link " \ + "or data") + logger.debug("Torrent hash: " + torrent_hash) + return torrent_hash.upper() + \ No newline at end of file