You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
421 lines
16 KiB
421 lines
16 KiB
12 years ago
|
from base64 import b16encode, b32decode
|
||
12 years ago
|
from bencode import bencode as benc, bdecode
|
||
12 years ago
|
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
||
12 years ago
|
from couchpotato.core.helpers.encoding import isInt, ss, sp
|
||
11 years ago
|
from couchpotato.core.helpers.variable import tryInt, tryFloat, cleanHost
|
||
13 years ago
|
from couchpotato.core.logger import CPLog
|
||
12 years ago
|
from datetime import timedelta
|
||
13 years ago
|
from hashlib import sha1
|
||
13 years ago
|
from multipartpost import MultipartPostHandler
|
||
13 years ago
|
import cookielib
|
||
13 years ago
|
import httplib
|
||
12 years ago
|
import json
|
||
12 years ago
|
import os
|
||
13 years ago
|
import re
|
||
12 years ago
|
import stat
|
||
13 years ago
|
import time
|
||
13 years ago
|
import urllib
|
||
|
import urllib2
|
||
|
|
||
|
log = CPLog(__name__)
|
||
|
|
||
11 years ago
|
autoload = 'uTorrent'
|
||
|
|
||
13 years ago
|
|
||
|
class uTorrent(Downloader):
|
||
|
|
||
12 years ago
|
protocol = ['torrent', 'torrent_magnet']
|
||
13 years ago
|
utorrent_api = None
|
||
11 years ago
|
status_flags = {
|
||
|
'STARTED' : 1,
|
||
|
'CHECKING' : 2,
|
||
|
'CHECK-START' : 4,
|
||
|
'CHECKED' : 8,
|
||
|
'ERROR' : 16,
|
||
|
'PAUSED' : 32,
|
||
|
'QUEUED' : 64,
|
||
|
'LOADED' : 128
|
||
|
}
|
||
13 years ago
|
|
||
12 years ago
|
def connect(self):
|
||
13 years ago
|
# Load host from config and split out port.
|
||
11 years ago
|
host = cleanHost(self.conf('host'), protocol = False).split(':')
|
||
13 years ago
|
if not isInt(host[1]):
|
||
|
log.error('Config properties are not filled in correctly, port is missing.')
|
||
|
return False
|
||
|
|
||
12 years ago
|
self.utorrent_api = uTorrentAPI(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
|
||
|
|
||
12 years ago
|
return self.utorrent_api
|
||
|
|
||
12 years ago
|
def download(self, data = None, media = None, filedata = None):
|
||
|
if not media: media = {}
|
||
12 years ago
|
if not data: data = {}
|
||
12 years ago
|
|
||
11 years ago
|
log.debug("Sending '%s' (%s) to uTorrent.", (data.get('name'), data.get('protocol')))
|
||
12 years ago
|
|
||
|
if not self.connect():
|
||
|
return False
|
||
|
|
||
|
settings = self.utorrent_api.get_settings()
|
||
|
if not settings:
|
||
|
return False
|
||
|
|
||
12 years ago
|
#Fix settings in case they are not set for CPS compatibility
|
||
|
new_settings = {}
|
||
12 years ago
|
if not (settings.get('seed_prio_limitul') == 0 and settings['seed_prio_limitul_flag']):
|
||
12 years ago
|
new_settings['seed_prio_limitul'] = 0
|
||
|
new_settings['seed_prio_limitul_flag'] = True
|
||
|
log.info('Updated uTorrent settings to set a torrent to complete after it the seeding requirements are met.')
|
||
12 years ago
|
|
||
11 years ago
|
if settings.get('bt.read_only_on_complete'): #This doesn't work as this option seems to be not available through the api. Mitigated with removeReadOnly function
|
||
12 years ago
|
new_settings['bt.read_only_on_complete'] = False
|
||
|
log.info('Updated uTorrent settings to not set the files to read only after completing.')
|
||
12 years ago
|
|
||
12 years ago
|
if new_settings:
|
||
|
self.utorrent_api.set_settings(new_settings)
|
||
|
|
||
13 years ago
|
torrent_params = {}
|
||
|
if self.conf('label'):
|
||
|
torrent_params['label'] = self.conf('label')
|
||
13 years ago
|
|
||
12 years ago
|
if not filedata and data.get('protocol') == 'torrent':
|
||
13 years ago
|
log.error('Failed sending torrent, no data')
|
||
|
return False
|
||
12 years ago
|
|
||
12 years ago
|
if data.get('protocol') == 'torrent_magnet':
|
||
13 years ago
|
torrent_hash = re.findall('urn:btih:([\w]{32,40})', data.get('url'))[0].upper()
|
||
13 years ago
|
torrent_params['trackers'] = '%0D%0A%0D%0A'.join(self.torrent_trackers)
|
||
13 years ago
|
else:
|
||
11 years ago
|
info = bdecode(filedata)['info']
|
||
12 years ago
|
torrent_hash = sha1(benc(info)).hexdigest().upper()
|
||
12 years ago
|
|
||
12 years ago
|
torrent_filename = self.createFileName(data, filedata, media)
|
||
13 years ago
|
|
||
12 years ago
|
if data.get('seed_ratio'):
|
||
12 years ago
|
torrent_params['seed_override'] = 1
|
||
12 years ago
|
torrent_params['seed_ratio'] = tryInt(tryFloat(data['seed_ratio']) * 1000)
|
||
12 years ago
|
|
||
12 years ago
|
if data.get('seed_time'):
|
||
12 years ago
|
torrent_params['seed_override'] = 1
|
||
12 years ago
|
torrent_params['seed_time'] = tryInt(data['seed_time']) * 3600
|
||
12 years ago
|
|
||
12 years ago
|
# Convert base 32 to hex
|
||
|
if len(torrent_hash) == 32:
|
||
|
torrent_hash = b16encode(b32decode(torrent_hash))
|
||
|
|
||
13 years ago
|
# Send request to uTorrent
|
||
12 years ago
|
if data.get('protocol') == 'torrent_magnet':
|
||
12 years ago
|
self.utorrent_api.add_torrent_uri(torrent_filename, data.get('url'))
|
||
12 years ago
|
else:
|
||
12 years ago
|
self.utorrent_api.add_torrent_file(torrent_filename, filedata)
|
||
13 years ago
|
|
||
12 years ago
|
# Change settings of added torrent
|
||
12 years ago
|
self.utorrent_api.set_torrent(torrent_hash, torrent_params)
|
||
|
if self.conf('paused', default = 0):
|
||
|
self.utorrent_api.pause_torrent(torrent_hash)
|
||
13 years ago
|
|
||
12 years ago
|
return self.downloadReturnId(torrent_hash)
|
||
13 years ago
|
|
||
11 years ago
|
def test(self):
|
||
|
if self.connect():
|
||
|
build_version = self.utorrent_api.get_build()
|
||
|
if not build_version:
|
||
|
return False
|
||
|
if build_version < 25406: # This build corresponds to version 3.0.0 stable
|
||
|
return False, 'Your uTorrent client is too old, please update to newest version.'
|
||
|
return True
|
||
|
|
||
|
return False
|
||
|
|
||
12 years ago
|
def getAllDownloadStatus(self, ids):
|
||
12 years ago
|
|
||
|
log.debug('Checking uTorrent download status.')
|
||
|
|
||
12 years ago
|
if not self.connect():
|
||
11 years ago
|
return []
|
||
12 years ago
|
|
||
12 years ago
|
release_downloads = ReleaseDownloadList(self)
|
||
12 years ago
|
|
||
12 years ago
|
data = self.utorrent_api.get_status()
|
||
|
if not data:
|
||
|
log.error('Error getting data from uTorrent')
|
||
11 years ago
|
return []
|
||
12 years ago
|
|
||
12 years ago
|
queue = json.loads(data)
|
||
|
if queue.get('error'):
|
||
|
log.error('Error getting data from uTorrent: %s', queue.get('error'))
|
||
11 years ago
|
return []
|
||
12 years ago
|
|
||
12 years ago
|
if not queue.get('torrents'):
|
||
12 years ago
|
log.debug('Nothing in queue')
|
||
11 years ago
|
return []
|
||
12 years ago
|
|
||
|
# Get torrents
|
||
12 years ago
|
for torrent in queue['torrents']:
|
||
12 years ago
|
if torrent[0] in ids:
|
||
|
|
||
|
#Get files of the torrent
|
||
|
torrent_files = []
|
||
|
try:
|
||
|
torrent_files = json.loads(self.utorrent_api.get_files(torrent[0]))
|
||
|
torrent_files = [sp(os.path.join(torrent[26], torrent_file[0])) for torrent_file in torrent_files['files'][1]]
|
||
|
except:
|
||
|
log.debug('Failed getting files from torrent: %s', torrent[2])
|
||
11 years ago
|
|
||
12 years ago
|
status = 'busy'
|
||
11 years ago
|
if (torrent[1] & self.status_flags['STARTED'] or torrent[1] & self.status_flags['QUEUED']) and torrent[4] == 1000:
|
||
12 years ago
|
status = 'seeding'
|
||
11 years ago
|
elif (torrent[1] & self.status_flags['ERROR']):
|
||
12 years ago
|
status = 'failed'
|
||
|
elif torrent[4] == 1000:
|
||
|
status = 'completed'
|
||
11 years ago
|
|
||
12 years ago
|
if not status == 'busy':
|
||
|
self.removeReadOnly(torrent_files)
|
||
11 years ago
|
|
||
12 years ago
|
release_downloads.append({
|
||
|
'id': torrent[0],
|
||
|
'name': torrent[2],
|
||
|
'status': status,
|
||
|
'seed_ratio': float(torrent[7]) / 1000,
|
||
|
'original_status': torrent[1],
|
||
|
'timeleft': str(timedelta(seconds = torrent[10])),
|
||
|
'folder': sp(torrent[26]),
|
||
|
'files': '|'.join(torrent_files)
|
||
|
})
|
||
12 years ago
|
|
||
12 years ago
|
return release_downloads
|
||
12 years ago
|
|
||
12 years ago
|
def pause(self, release_download, pause = True):
|
||
12 years ago
|
if not self.connect():
|
||
|
return False
|
||
12 years ago
|
return self.utorrent_api.pause_torrent(release_download['id'], pause)
|
||
12 years ago
|
|
||
12 years ago
|
def removeFailed(self, release_download):
|
||
|
log.info('%s failed downloading, deleting...', release_download['name'])
|
||
12 years ago
|
if not self.connect():
|
||
|
return False
|
||
12 years ago
|
return self.utorrent_api.remove_torrent(release_download['id'], remove_data = True)
|
||
12 years ago
|
|
||
12 years ago
|
def processComplete(self, release_download, delete_files = False):
|
||
|
log.debug('Requesting uTorrent to remove the torrent %s%s.', (release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||
12 years ago
|
if not self.connect():
|
||
|
return False
|
||
12 years ago
|
return self.utorrent_api.remove_torrent(release_download['id'], remove_data = delete_files)
|
||
12 years ago
|
|
||
12 years ago
|
def removeReadOnly(self, files):
|
||
12 years ago
|
#Removes all read-on ly flags in a for all files
|
||
12 years ago
|
for filepath in files:
|
||
|
if os.path.isfile(filepath):
|
||
|
#Windows only needs S_IWRITE, but we bitwise-or with current perms to preserve other permission bits on Linux
|
||
|
os.chmod(filepath, stat.S_IWRITE | os.stat(filepath).st_mode)
|
||
13 years ago
|
|
||
|
class uTorrentAPI(object):
|
||
|
|
||
|
def __init__(self, host = 'localhost', port = 8000, username = None, password = None):
|
||
|
|
||
|
super(uTorrentAPI, self).__init__()
|
||
|
|
||
13 years ago
|
self.url = 'http://' + str(host) + ':' + str(port) + '/gui/'
|
||
13 years ago
|
self.token = ''
|
||
|
self.last_time = time.time()
|
||
|
cookies = cookielib.CookieJar()
|
||
13 years ago
|
self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), MultipartPostHandler)
|
||
13 years ago
|
self.opener.addheaders = [('User-agent', 'couchpotato-utorrent-client/1.0')]
|
||
|
if username and password:
|
||
|
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
|
||
|
password_manager.add_password(realm = None, uri = self.url, user = username, passwd = password)
|
||
|
self.opener.add_handler(urllib2.HTTPBasicAuthHandler(password_manager))
|
||
|
self.opener.add_handler(urllib2.HTTPDigestAuthHandler(password_manager))
|
||
|
elif username or password:
|
||
|
log.debug('User or password missing, not using authentication.')
|
||
|
self.token = self.get_token()
|
||
|
|
||
13 years ago
|
def _request(self, action, data = None):
|
||
13 years ago
|
if time.time() > self.last_time + 1800:
|
||
|
self.last_time = time.time()
|
||
|
self.token = self.get_token()
|
||
11 years ago
|
request = urllib2.Request(self.url + '?token=' + self.token + '&' + action, data)
|
||
13 years ago
|
try:
|
||
|
open_request = self.opener.open(request)
|
||
|
response = open_request.read()
|
||
|
if response:
|
||
|
return response
|
||
|
else:
|
||
|
log.debug('Unknown failure sending command to uTorrent. Return text is: %s', response)
|
||
11 years ago
|
except httplib.InvalidURL as err:
|
||
13 years ago
|
log.error('Invalid uTorrent host, check your config %s', err)
|
||
11 years ago
|
except urllib2.HTTPError as err:
|
||
13 years ago
|
if err.code == 401:
|
||
|
log.error('Invalid uTorrent Username or Password, check your config')
|
||
|
else:
|
||
|
log.error('uTorrent HTTPError: %s', err)
|
||
11 years ago
|
except urllib2.URLError as err:
|
||
13 years ago
|
log.error('Unable to connect to uTorrent %s', err)
|
||
13 years ago
|
return False
|
||
13 years ago
|
|
||
|
def get_token(self):
|
||
11 years ago
|
request = self.opener.open(self.url + 'token.html')
|
||
|
token = re.findall('<div.*?>(.*?)</', request.read())[0]
|
||
13 years ago
|
return token
|
||
|
|
||
12 years ago
|
def add_torrent_uri(self, filename, torrent, add_folder = False):
|
||
11 years ago
|
action = 'action=add-url&s=%s' % urllib.quote(torrent)
|
||
12 years ago
|
if add_folder:
|
||
11 years ago
|
action += '&path=%s' % urllib.quote(filename)
|
||
13 years ago
|
return self._request(action)
|
||
|
|
||
12 years ago
|
def add_torrent_file(self, filename, filedata, add_folder = False):
|
||
11 years ago
|
action = 'action=add-file'
|
||
12 years ago
|
if add_folder:
|
||
11 years ago
|
action += '&path=%s' % urllib.quote(filename)
|
||
|
return self._request(action, {'torrent_file': (ss(filename), filedata)})
|
||
13 years ago
|
|
||
13 years ago
|
def set_torrent(self, hash, params):
|
||
11 years ago
|
action = 'action=setprops&hash=%s' % hash
|
||
11 years ago
|
for k, v in params.items():
|
||
11 years ago
|
action += '&s=%s&v=%s' % (k, v)
|
||
13 years ago
|
return self._request(action)
|
||
|
|
||
12 years ago
|
def pause_torrent(self, hash, pause = True):
|
||
|
if pause:
|
||
11 years ago
|
action = 'action=pause&hash=%s' % hash
|
||
12 years ago
|
else:
|
||
11 years ago
|
action = 'action=unpause&hash=%s' % hash
|
||
13 years ago
|
return self._request(action)
|
||
12 years ago
|
|
||
12 years ago
|
def stop_torrent(self, hash):
|
||
11 years ago
|
action = 'action=stop&hash=%s' % hash
|
||
12 years ago
|
return self._request(action)
|
||
12 years ago
|
|
||
12 years ago
|
def remove_torrent(self, hash, remove_data = False):
|
||
12 years ago
|
if remove_data:
|
||
11 years ago
|
action = 'action=removedata&hash=%s' % hash
|
||
12 years ago
|
else:
|
||
11 years ago
|
action = 'action=remove&hash=%s' % hash
|
||
12 years ago
|
return self._request(action)
|
||
12 years ago
|
|
||
12 years ago
|
def get_status(self):
|
||
11 years ago
|
action = 'list=1'
|
||
12 years ago
|
return self._request(action)
|
||
12 years ago
|
|
||
|
def get_settings(self):
|
||
11 years ago
|
action = 'action=getsettings'
|
||
12 years ago
|
settings_dict = {}
|
||
|
try:
|
||
|
utorrent_settings = json.loads(self._request(action))
|
||
|
|
||
|
# Create settings dict
|
||
12 years ago
|
for setting in utorrent_settings['settings']:
|
||
|
if setting[1] == 0: # int
|
||
|
settings_dict[setting[0]] = int(setting[2] if not setting[2].strip() == '' else '0')
|
||
|
elif setting[1] == 1: # bool
|
||
|
settings_dict[setting[0]] = True if setting[2] == 'true' else False
|
||
|
elif setting[1] == 2: # string
|
||
|
settings_dict[setting[0]] = setting[2]
|
||
12 years ago
|
|
||
|
#log.debug('uTorrent settings: %s', settings_dict)
|
||
|
|
||
11 years ago
|
except Exception as err:
|
||
12 years ago
|
log.error('Failed to get settings from uTorrent: %s', err)
|
||
|
|
||
|
return settings_dict
|
||
12 years ago
|
|
||
12 years ago
|
def set_settings(self, settings_dict = None):
|
||
|
if not settings_dict: settings_dict = {}
|
||
|
|
||
12 years ago
|
for key in settings_dict:
|
||
|
if isinstance(settings_dict[key], bool):
|
||
|
settings_dict[key] = 1 if settings_dict[key] else 0
|
||
|
|
||
12 years ago
|
action = 'action=setsetting' + ''.join(['&s=%s&v=%s' % (key, value) for (key, value) in settings_dict.items()])
|
||
12 years ago
|
return self._request(action)
|
||
12 years ago
|
|
||
|
def get_files(self, hash):
|
||
11 years ago
|
action = 'action=getfiles&hash=%s' % hash
|
||
12 years ago
|
return self._request(action)
|
||
11 years ago
|
|
||
|
def get_build(self):
|
||
|
data = self._request('')
|
||
|
if not data:
|
||
|
return False
|
||
|
response = json.loads(data)
|
||
|
return int(response.get('build'))
|
||
11 years ago
|
|
||
|
|
||
|
config = [{
|
||
|
'name': 'utorrent',
|
||
|
'groups': [
|
||
|
{
|
||
|
'tab': 'downloaders',
|
||
|
'list': 'download_providers',
|
||
|
'name': 'utorrent',
|
||
|
'label': 'uTorrent',
|
||
|
'description': 'Use <a href="http://www.utorrent.com/" target="_blank">uTorrent</a> (3.0+) to download torrents.',
|
||
|
'wizard': True,
|
||
|
'options': [
|
||
|
{
|
||
|
'name': 'enabled',
|
||
|
'default': 0,
|
||
|
'type': 'enabler',
|
||
|
'radio_group': 'torrent',
|
||
|
},
|
||
|
{
|
||
|
'name': 'host',
|
||
|
'default': 'localhost:8000',
|
||
|
'description': 'Port can be found in settings when enabling WebUI.',
|
||
|
},
|
||
|
{
|
||
|
'name': 'username',
|
||
|
},
|
||
|
{
|
||
|
'name': 'password',
|
||
|
'type': 'password',
|
||
|
},
|
||
|
{
|
||
|
'name': 'label',
|
||
|
'description': 'Label to add torrent as.',
|
||
|
},
|
||
|
{
|
||
|
'name': 'remove_complete',
|
||
|
'label': 'Remove torrent',
|
||
|
'default': True,
|
||
|
'advanced': True,
|
||
|
'type': 'bool',
|
||
|
'description': 'Remove the torrent from uTorrent after it finished seeding.',
|
||
|
},
|
||
|
{
|
||
|
'name': 'delete_files',
|
||
|
'label': 'Remove files',
|
||
|
'default': True,
|
||
|
'type': 'bool',
|
||
|
'advanced': True,
|
||
|
'description': 'Also remove the leftover files.',
|
||
|
},
|
||
|
{
|
||
|
'name': 'paused',
|
||
|
'type': 'bool',
|
||
|
'advanced': True,
|
||
|
'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.',
|
||
|
},
|
||
|
{
|
||
|
'name': 'delete_failed',
|
||
|
'default': True,
|
||
|
'advanced': True,
|
||
|
'type': 'bool',
|
||
|
'description': 'Delete a release after the download has failed.',
|
||
|
},
|
||
|
],
|
||
|
}
|
||
|
],
|
||
|
}]
|