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.
337 lines
10 KiB
337 lines
10 KiB
11 years ago
|
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
|
||
|
from couchpotato.core.helpers.encoding import isInt, ss, sp
|
||
|
from couchpotato.core.helpers.variable import tryInt, tryFloat, cleanHost
|
||
|
from couchpotato.core.logger import CPLog
|
||
|
|
||
|
from base64 import b16encode, b32decode, b64encode
|
||
|
from distutils.version import LooseVersion
|
||
|
import httplib
|
||
|
import json
|
||
|
import os
|
||
|
import re
|
||
|
import urllib
|
||
|
import urllib2
|
||
|
|
||
|
log = CPLog(__name__)
|
||
|
|
||
|
autoload = 'Hadouken'
|
||
|
|
||
|
class Hadouken(DownloaderBase):
|
||
|
protocol = ['torrent', 'torrent_magnet']
|
||
|
hadouken_api = None
|
||
|
|
||
|
def connect(self):
|
||
|
# Load host from config and split out port.
|
||
|
host = cleanHost(self.conf('host'), protocol = False).split(':')
|
||
|
|
||
|
if not isInt(host[1]):
|
||
|
log.error('Config properties are not filled in correctly, port is missing.')
|
||
|
return False
|
||
|
|
||
|
if not self.conf('apikey'):
|
||
|
log.error('Config properties are not filled in correctly, API key is missing.')
|
||
|
return False
|
||
|
|
||
|
self.hadouken_api = HadoukenAPI(host[0], port = host[1], apiKey = self.conf('apikey'))
|
||
|
|
||
|
return True
|
||
|
|
||
|
def download(self, data = None, media = None, filedata = None):
|
||
|
if not media: media = {}
|
||
|
if not data: data = {}
|
||
|
|
||
|
log.debug("Sending '%s' (%s) to Hadouken.", (data.get('name'), data.get('protocol')))
|
||
|
|
||
|
if not self.connect():
|
||
|
return False
|
||
|
|
||
|
torrent_params = {}
|
||
|
|
||
|
if self.conf('label'):
|
||
|
torrent_params['label'] = self.conf('label')
|
||
|
|
||
|
torrent_filename = self.createFileName(data, filedata, media)
|
||
|
|
||
|
if data.get('protocol') == 'torrent_magnet':
|
||
|
torrent_hash = re.findall('urn:btih:([\w]{32,40})', data.get('url'))[0].upper()
|
||
|
torrent_params['trackers'] = self.torrent_trackers
|
||
|
torrent_params['name'] = torrent_filename
|
||
|
else:
|
||
|
torrent_hash = sha1(benc(info)).hexdigest().upper()
|
||
|
|
||
|
# Convert base 32 to hex
|
||
|
if len(torrent_hash) == 32:
|
||
|
torrent_hash = b16encode(b32decode(torrent_hash))
|
||
|
|
||
|
# Send request to Hadouken
|
||
|
if data.get('protocol') == 'torrent_magnet':
|
||
|
self.hadouken_api.add_magnet_link(data.get('url'), torrent_params)
|
||
|
else:
|
||
|
self.hadouken_api.add_file(filedata, torrent_params)
|
||
|
|
||
|
return self.downloadReturnId(torrent_hash)
|
||
|
|
||
|
def test(self):
|
||
|
""" Tests the given host:port and API key """
|
||
|
|
||
|
if not self.connect():
|
||
|
return False
|
||
|
|
||
|
version = self.hadouken_api.get_version()
|
||
|
|
||
|
if not version:
|
||
|
log.error('Could not get Hadouken version.')
|
||
|
return False
|
||
|
|
||
|
if LooseVersion(version) >= LooseVersion('4.4.1'):
|
||
|
return True
|
||
|
|
||
|
log.error('Hadouken v4.1.1 (or newer) required. Found v%s', version)
|
||
|
return False
|
||
|
|
||
|
def getAllDownloadStatus(self, ids):
|
||
|
log.debug('Checking Hadouken download status.')
|
||
|
|
||
|
if not self.connect():
|
||
|
return []
|
||
|
|
||
|
release_downloads = ReleaseDownloadList(self)
|
||
|
queue = self.hadouken_api.get_by_hash_list(ids)
|
||
|
|
||
|
if not queue:
|
||
|
return []
|
||
|
|
||
|
for torrent in queue:
|
||
|
if torrent is None:
|
||
|
continue
|
||
|
|
||
|
torrent_dir = os.path.join(torrent['SavePath'], torrent['Name'])
|
||
|
torrent_files = []
|
||
|
status = 'busy'
|
||
|
|
||
|
for torrent_file in torrent['Files']:
|
||
|
torrent_files.append(sp(os.path.join(torrent['SavePath'], torrent_file['Path'])))
|
||
|
|
||
|
if os.path.isdir(torrent_dir):
|
||
|
torrent['SavePath'] = torrent_dir
|
||
|
|
||
|
release_downloads.append({
|
||
|
'id': torrent['InfoHash'].upper(),
|
||
|
'name': torrent['Name'],
|
||
|
'status': self.get_torrent_status(torrent),
|
||
|
'seed_ratio': self.get_seed_ratio(torrent),
|
||
|
'original_status': torrent['State'],
|
||
|
'timeleft': -1,
|
||
|
'folder': sp(torrent['SavePath']),
|
||
|
'files': torrent_files
|
||
|
})
|
||
|
|
||
|
return release_downloads
|
||
|
|
||
|
def get_seed_ratio(self, torrent):
|
||
|
""" Returns the seed ratio for a given torrent.
|
||
|
|
||
|
Keyword arguments:
|
||
|
torrent -- The torrent to calculate seed ratio for.
|
||
|
"""
|
||
|
|
||
|
up = torrent['TotalUploadedBytes']
|
||
|
down = torrent['TotalDownloadedBytes']
|
||
|
|
||
|
if up > 0 and down > 0:
|
||
|
return up / down
|
||
|
|
||
|
return 0
|
||
|
|
||
|
def get_torrent_status(self, torrent):
|
||
|
""" Returns the CouchPotato status for a given torrent.
|
||
|
|
||
|
Keyword arguments:
|
||
|
torrent -- The torrent to translate status for.
|
||
|
"""
|
||
|
|
||
|
if torrent['IsSeeding'] and torrent['IsFinished'] and torrent['Paused']:
|
||
|
return 'completed'
|
||
|
|
||
|
if torrent['IsSeeding']:
|
||
|
return 'seeding'
|
||
|
|
||
|
return 'busy'
|
||
|
|
||
|
def pause(self, release_download, pause = True):
|
||
|
""" Pauses or resumes the torrent specified by the ID field
|
||
|
in release_download.
|
||
|
|
||
|
Keyword arguments:
|
||
|
release_download -- The CouchPotato release_download to pause/resume.
|
||
|
pause -- Boolean indicating whether to pause or resume.
|
||
|
"""
|
||
|
|
||
|
if not self.connect():
|
||
|
return False
|
||
|
|
||
|
return self.hadouken_api.pause(release_download['id'], pause)
|
||
|
|
||
|
def removeFailed(self, release_download):
|
||
|
""" Removes a failed torrent and also remove the data associated with it.
|
||
|
|
||
|
Keyword arguments:
|
||
|
release_download -- The CouchPotato release_download to remove.
|
||
|
"""
|
||
|
|
||
|
log.info('%s failed downloading, deleting...', release_download['name'])
|
||
|
|
||
|
if not self.connect():
|
||
|
return False
|
||
|
|
||
|
return self.hadouken_api.remove(release_download['id'], remove_data = True)
|
||
|
|
||
|
def processComplete(self, release_download, delete_files = False):
|
||
|
""" Removes the completed torrent from Hadouken and optionally removes the data
|
||
|
associated with it.
|
||
|
|
||
|
Keyword arguments:
|
||
|
release_download -- The CouchPotato release_download to remove.
|
||
|
delete_files: Boolean indicating whether to remove the associated data.
|
||
|
"""
|
||
|
|
||
|
log.debug('Requesting Hadouken to remove the torrent %s%s.', (release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||
|
|
||
|
if not self.connect():
|
||
|
return False
|
||
|
|
||
|
return self.hadouken_api.remove(release_download['id'], remove_data = delete_files)
|
||
|
|
||
|
class HadoukenAPI(object):
|
||
|
def __init__(self, host = 'localhost', port = 7890, apiKey = None):
|
||
|
self.url = 'http://' + str(host) + ':' + str(port)
|
||
|
self.apiKey = apiKey
|
||
|
self.requestId = 0;
|
||
|
|
||
|
self.opener = urllib2.build_opener()
|
||
|
self.opener.addheaders = [('User-agent', 'couchpotato-hadouken-client/1.0'), ('Accept', 'application/json')]
|
||
|
|
||
|
if not apiKey:
|
||
|
log.error('API key missing.')
|
||
|
|
||
|
def add_file(self, filedata, torrent_params):
|
||
|
data = {
|
||
|
'method': 'torrents.addFile',
|
||
|
'params': [ b64encode(filedata), torrent_params ]
|
||
|
}
|
||
|
|
||
|
return self._request('/jsonrpc', data)
|
||
|
|
||
|
def add_magnet_link(self, magnetLink, torrent_params):
|
||
|
data = {
|
||
|
'method': 'torrents.addUrl',
|
||
|
'params': [ magnetLink, torrent_params ]
|
||
|
}
|
||
|
|
||
|
return self._request('/jsonrpc', data)
|
||
|
|
||
|
def get_by_hash_list(self, infoHashList):
|
||
|
data = {
|
||
|
'method': 'torrents.getByInfoHashList',
|
||
|
'params': [ infoHashList ]
|
||
|
}
|
||
|
|
||
|
return self._request('/jsonrpc', data)
|
||
|
|
||
|
def get_version(self):
|
||
|
data = {
|
||
|
'method': 'core.getVersion',
|
||
|
'params': None
|
||
|
}
|
||
|
|
||
|
result = self._request('/jsonrpc', data)
|
||
|
|
||
|
if not result:
|
||
|
return False
|
||
|
|
||
|
return result['Version']
|
||
|
|
||
|
def pause(self, id, pause):
|
||
|
data = {
|
||
|
'method': 'torrents.pause',
|
||
|
'params': [ id ]
|
||
|
}
|
||
|
|
||
|
if not pause:
|
||
|
data['method'] = 'torrents.resume'
|
||
|
|
||
|
return self._request('/jsonrpc', data)
|
||
|
|
||
|
def remove(self, id, remove_data = False):
|
||
|
data = {
|
||
|
'method': 'torrents.remove',
|
||
|
'params': [ id, remove_data ]
|
||
|
}
|
||
|
|
||
|
return self._request('/jsonrpc', data)
|
||
|
|
||
|
|
||
|
def _request(self, url, data):
|
||
|
self.requestId += 1
|
||
|
|
||
|
data['jsonrpc'] = '2.0'
|
||
|
data['id'] = self.requestId
|
||
|
|
||
|
request = urllib2.Request(self.url + url, data = json.dumps(data))
|
||
|
request.add_header('Authorization', 'Token ' + self.apiKey)
|
||
|
request.add_header('Content-Type', 'application/json')
|
||
|
|
||
|
try:
|
||
|
f = self.opener.open(request)
|
||
|
response = f.read()
|
||
|
f.close()
|
||
|
|
||
|
obj = json.loads(response)
|
||
|
|
||
|
if not 'error' in obj.keys():
|
||
|
return obj['result']
|
||
|
|
||
|
log.error('JSONRPC error, %s: %s', obj['error']['code'], obj['error']['message'])
|
||
|
except httplib.InvalidURL as err:
|
||
|
log.error('Invalid Hadouken host, check your config %s', err)
|
||
|
except urllib2.HTTPError as err:
|
||
|
if err.code == 401:
|
||
|
log.error('Invalid Hadouken API key, check your config')
|
||
|
else:
|
||
|
log.error('Hadouken HTTPError: %s', err)
|
||
|
except urllib2.URLError as err:
|
||
|
log.error('Unable to connect to Hadouken %s', err)
|
||
|
|
||
|
return False
|
||
|
|
||
|
|
||
|
config = [{
|
||
|
'name': 'hadouken',
|
||
|
'groups': [
|
||
|
{
|
||
|
'tab': 'downloaders',
|
||
|
'list': 'download_providers',
|
||
|
'name': 'hadouken',
|
||
|
'label': 'Hadouken',
|
||
|
'description': 'Use <a href="http://www.hdkn.net">Hadouken</a> (>= v4.4.1) to download torrents.',
|
||
|
'wizard': True,
|
||
|
'options': [
|
||
|
{
|
||
|
'name': 'enabled',
|
||
|
'default': 0,
|
||
|
'type': 'enabler',
|
||
|
'radio_group': 'torrent'
|
||
|
},
|
||
|
{
|
||
|
'name': 'host',
|
||
|
'default': 'localhost:7890'
|
||
|
},
|
||
|
{
|
||
|
'name': 'apikey',
|
||
|
'label': 'API key',
|
||
|
'type': 'password'
|
||
|
}
|
||
|
]
|
||
|
}
|
||
|
]
|
||
|
}]
|