PageRenderTime 70ms CodeModel.GetById 25ms RepoModel.GetById 1ms app.codeStats 0ms

/couchpotato/core/downloaders/hadouken.py

https://gitlab.com/132nd-etcher/CouchPotatoServer
Python | 427 lines | 400 code | 12 blank | 15 comment | 2 complexity | 2f843cdb9f04f842ec0da8942360e67c MD5 | raw file
  1. from base64 import b16encode, b32decode, b64encode
  2. from distutils.version import LooseVersion
  3. from hashlib import sha1
  4. import httplib
  5. import json
  6. import os
  7. import re
  8. import urllib2
  9. from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
  10. from couchpotato.core.helpers.encoding import isInt, sp
  11. from couchpotato.core.helpers.variable import cleanHost
  12. from couchpotato.core.logger import CPLog
  13. from bencode import bencode as benc, bdecode
  14. log = CPLog(__name__)
  15. autoload = 'Hadouken'
  16. class Hadouken(DownloaderBase):
  17. protocol = ['torrent', 'torrent_magnet']
  18. hadouken_api = None
  19. def connect(self):
  20. # Load host from config and split out port.
  21. host = cleanHost(self.conf('host'), protocol = False).split(':')
  22. if not isInt(host[1]):
  23. log.error('Config properties are not filled in correctly, port is missing.')
  24. return False
  25. if not self.conf('apikey'):
  26. log.error('Config properties are not filled in correctly, API key is missing.')
  27. return False
  28. self.hadouken_api = HadoukenAPI(host[0], port = host[1], api_key = self.conf('api_key'))
  29. return True
  30. def download(self, data = None, media = None, filedata = None):
  31. """ Send a torrent/nzb file to the downloader
  32. :param data: dict returned from provider
  33. Contains the release information
  34. :param media: media dict with information
  35. Used for creating the filename when possible
  36. :param filedata: downloaded torrent/nzb filedata
  37. The file gets downloaded in the searcher and send to this function
  38. This is done to have failed checking before using the downloader, so the downloader
  39. doesn't need to worry about that
  40. :return: boolean
  41. One faile returns false, but the downloaded should log his own errors
  42. """
  43. if not media: media = {}
  44. if not data: data = {}
  45. log.debug("Sending '%s' (%s) to Hadouken.", (data.get('name'), data.get('protocol')))
  46. if not self.connect():
  47. return False
  48. torrent_params = {}
  49. if self.conf('label'):
  50. torrent_params['label'] = self.conf('label')
  51. torrent_filename = self.createFileName(data, filedata, media)
  52. if data.get('protocol') == 'torrent_magnet':
  53. torrent_hash = re.findall('urn:btih:([\w]{32,40})', data.get('url'))[0].upper()
  54. torrent_params['trackers'] = self.torrent_trackers
  55. torrent_params['name'] = torrent_filename
  56. else:
  57. info = bdecode(filedata)['info']
  58. torrent_hash = sha1(benc(info)).hexdigest().upper()
  59. # Convert base 32 to hex
  60. if len(torrent_hash) == 32:
  61. torrent_hash = b16encode(b32decode(torrent_hash))
  62. # Send request to Hadouken
  63. if data.get('protocol') == 'torrent_magnet':
  64. self.hadouken_api.add_magnet_link(data.get('url'), torrent_params)
  65. else:
  66. self.hadouken_api.add_file(filedata, torrent_params)
  67. return self.downloadReturnId(torrent_hash)
  68. def test(self):
  69. """ Tests the given host:port and API key """
  70. if not self.connect():
  71. return False
  72. version = self.hadouken_api.get_version()
  73. if not version:
  74. log.error('Could not get Hadouken version.')
  75. return False
  76. # The minimum required version of Hadouken is 4.5.6.
  77. if LooseVersion(version) >= LooseVersion('4.5.6'):
  78. return True
  79. log.error('Hadouken v4.5.6 (or newer) required. Found v%s', version)
  80. return False
  81. def getAllDownloadStatus(self, ids):
  82. """ Get status of all active downloads
  83. :param ids: list of (mixed) downloader ids
  84. Used to match the releases for this downloader as there could be
  85. other downloaders active that it should ignore
  86. :return: list of releases
  87. """
  88. log.debug('Checking Hadouken download status.')
  89. if not self.connect():
  90. return []
  91. release_downloads = ReleaseDownloadList(self)
  92. queue = self.hadouken_api.get_by_hash_list(ids)
  93. if not queue:
  94. return []
  95. for torrent in queue:
  96. if torrent is None:
  97. continue
  98. torrent_filelist = self.hadouken_api.get_files_by_hash(torrent['InfoHash'])
  99. torrent_files = []
  100. save_path = torrent['SavePath']
  101. # The 'Path' key for each file_item contains
  102. # the full path to the single file relative to the
  103. # torrents save path.
  104. # For a single file torrent the result would be,
  105. # - Save path: "C:\Downloads"
  106. # - file_item['Path'] = "file1.iso"
  107. # Resulting path: "C:\Downloads\file1.iso"
  108. # For a multi file torrent the result would be,
  109. # - Save path: "C:\Downloads"
  110. # - file_item['Path'] = "dirname/file1.iso"
  111. # Resulting path: "C:\Downloads\dirname/file1.iso"
  112. for file_item in torrent_filelist:
  113. torrent_files.append(sp(os.path.join(save_path, file_item['Path'])))
  114. release_downloads.append({
  115. 'id': torrent['InfoHash'].upper(),
  116. 'name': torrent['Name'],
  117. 'status': self.get_torrent_status(torrent),
  118. 'seed_ratio': self.get_seed_ratio(torrent),
  119. 'original_status': torrent['State'],
  120. 'timeleft': -1,
  121. 'folder': sp(save_path if len(torrent_files == 1) else os.path.join(save_path, torrent['Name'])),
  122. 'files': torrent_files
  123. })
  124. return release_downloads
  125. def get_seed_ratio(self, torrent):
  126. """ Returns the seed ratio for a given torrent.
  127. Keyword arguments:
  128. torrent -- The torrent to calculate seed ratio for.
  129. """
  130. up = torrent['TotalUploadedBytes']
  131. down = torrent['TotalDownloadedBytes']
  132. if up > 0 and down > 0:
  133. return up / down
  134. return 0
  135. def get_torrent_status(self, torrent):
  136. """ Returns the CouchPotato status for a given torrent.
  137. Keyword arguments:
  138. torrent -- The torrent to translate status for.
  139. """
  140. if torrent['IsSeeding'] and torrent['IsFinished'] and torrent['Paused']:
  141. return 'completed'
  142. if torrent['IsSeeding']:
  143. return 'seeding'
  144. return 'busy'
  145. def pause(self, release_download, pause = True):
  146. """ Pauses or resumes the torrent specified by the ID field
  147. in release_download.
  148. Keyword arguments:
  149. release_download -- The CouchPotato release_download to pause/resume.
  150. pause -- Boolean indicating whether to pause or resume.
  151. """
  152. if not self.connect():
  153. return False
  154. return self.hadouken_api.pause(release_download['id'], pause)
  155. def removeFailed(self, release_download):
  156. """ Removes a failed torrent and also remove the data associated with it.
  157. Keyword arguments:
  158. release_download -- The CouchPotato release_download to remove.
  159. """
  160. log.info('%s failed downloading, deleting...', release_download['name'])
  161. if not self.connect():
  162. return False
  163. return self.hadouken_api.remove(release_download['id'], remove_data = True)
  164. def processComplete(self, release_download, delete_files = False):
  165. """ Removes the completed torrent from Hadouken and optionally removes the data
  166. associated with it.
  167. Keyword arguments:
  168. release_download -- The CouchPotato release_download to remove.
  169. delete_files: Boolean indicating whether to remove the associated data.
  170. """
  171. log.debug('Requesting Hadouken to remove the torrent %s%s.',
  172. (release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
  173. if not self.connect():
  174. return False
  175. return self.hadouken_api.remove(release_download['id'], remove_data = delete_files)
  176. class HadoukenAPI(object):
  177. def __init__(self, host = 'localhost', port = 7890, api_key = None):
  178. self.url = 'http://' + str(host) + ':' + str(port)
  179. self.api_key = api_key
  180. self.requestId = 0;
  181. self.opener = urllib2.build_opener()
  182. self.opener.addheaders = [('User-agent', 'couchpotato-hadouken-client/1.0'), ('Accept', 'application/json')]
  183. if not api_key:
  184. log.error('API key missing.')
  185. def add_file(self, filedata, torrent_params):
  186. """ Add a file to Hadouken with the specified parameters.
  187. Keyword arguments:
  188. filedata -- The binary torrent data.
  189. torrent_params -- Additional parameters for the file.
  190. """
  191. data = {
  192. 'method': 'torrents.addFile',
  193. 'params': [b64encode(filedata), torrent_params]
  194. }
  195. return self._request(data)
  196. def add_magnet_link(self, magnetLink, torrent_params):
  197. """ Add a magnet link to Hadouken with the specified parameters.
  198. Keyword arguments:
  199. magnetLink -- The magnet link to send.
  200. torrent_params -- Additional parameters for the magnet link.
  201. """
  202. data = {
  203. 'method': 'torrents.addUrl',
  204. 'params': [magnetLink, torrent_params]
  205. }
  206. return self._request(data)
  207. def get_by_hash_list(self, infoHashList):
  208. """ Gets a list of torrents filtered by the given info hash list.
  209. Keyword arguments:
  210. infoHashList -- A list of info hashes.
  211. """
  212. data = {
  213. 'method': 'torrents.getByInfoHashList',
  214. 'params': [infoHashList]
  215. }
  216. return self._request(data)
  217. def get_files_by_hash(self, infoHash):
  218. """ Gets a list of files for the torrent identified by the
  219. given info hash.
  220. Keyword arguments:
  221. infoHash -- The info hash of the torrent to return files for.
  222. """
  223. data = {
  224. 'method': 'torrents.getFiles',
  225. 'params': [infoHash]
  226. }
  227. return self._request(data)
  228. def get_version(self):
  229. """ Gets the version, commitish and build date of Hadouken. """
  230. data = {
  231. 'method': 'core.getVersion',
  232. 'params': None
  233. }
  234. result = self._request(data)
  235. if not result:
  236. return False
  237. return result['Version']
  238. def pause(self, infoHash, pause):
  239. """ Pauses/unpauses the torrent identified by the given info hash.
  240. Keyword arguments:
  241. infoHash -- The info hash of the torrent to operate on.
  242. pause -- If true, pauses the torrent. Otherwise resumes.
  243. """
  244. data = {
  245. 'method': 'torrents.pause',
  246. 'params': [infoHash]
  247. }
  248. if not pause:
  249. data['method'] = 'torrents.resume'
  250. return self._request(data)
  251. def remove(self, infoHash, remove_data = False):
  252. """ Removes the torrent identified by the given info hash and
  253. optionally removes the data as well.
  254. Keyword arguments:
  255. infoHash -- The info hash of the torrent to remove.
  256. remove_data -- If true, removes the data associated with the torrent.
  257. """
  258. data = {
  259. 'method': 'torrents.remove',
  260. 'params': [infoHash, remove_data]
  261. }
  262. return self._request(data)
  263. def _request(self, data):
  264. self.requestId += 1
  265. data['jsonrpc'] = '2.0'
  266. data['id'] = self.requestId
  267. request = urllib2.Request(self.url + '/jsonrpc', data = json.dumps(data))
  268. request.add_header('Authorization', 'Token ' + self.api_key)
  269. request.add_header('Content-Type', 'application/json')
  270. try:
  271. f = self.opener.open(request)
  272. response = f.read()
  273. f.close()
  274. obj = json.loads(response)
  275. if not 'error' in obj.keys():
  276. return obj['result']
  277. log.error('JSONRPC error, %s: %s', obj['error']['code'], obj['error']['message'])
  278. except httplib.InvalidURL as err:
  279. log.error('Invalid Hadouken host, check your config %s', err)
  280. except urllib2.HTTPError as err:
  281. if err.code == 401:
  282. log.error('Invalid Hadouken API key, check your config')
  283. else:
  284. log.error('Hadouken HTTPError: %s', err)
  285. except urllib2.URLError as err:
  286. log.error('Unable to connect to Hadouken %s', err)
  287. return False
  288. config = [{
  289. 'name': 'hadouken',
  290. 'groups': [
  291. {
  292. 'tab': 'downloaders',
  293. 'list': 'download_providers',
  294. 'name': 'hadouken',
  295. 'label': 'Hadouken',
  296. 'description': 'Use <a href="http://www.hdkn.net">Hadouken</a> (>= v4.5.6) to download torrents.',
  297. 'wizard': True,
  298. 'options': [
  299. {
  300. 'name': 'enabled',
  301. 'default': 0,
  302. 'type': 'enabler',
  303. 'radio_group': 'torrent'
  304. },
  305. {
  306. 'name': 'host',
  307. 'default': 'localhost:7890'
  308. },
  309. {
  310. 'name': 'api_key',
  311. 'label': 'API key',
  312. 'type': 'password'
  313. },
  314. {
  315. 'name': 'label',
  316. 'description': 'Label to add torrent as.'
  317. }
  318. ]
  319. }
  320. ]
  321. }]