26 changed files with 1462 additions and 8 deletions
@ -0,0 +1,427 @@ |
|||||
|
from base64 import b16encode, b32decode, b64encode |
||||
|
from distutils.version import LooseVersion |
||||
|
from hashlib import sha1 |
||||
|
import httplib |
||||
|
import json |
||||
|
import os |
||||
|
import re |
||||
|
import urllib2 |
||||
|
|
||||
|
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList |
||||
|
from couchpotato.core.helpers.encoding import isInt, sp |
||||
|
from couchpotato.core.helpers.variable import cleanHost |
||||
|
from couchpotato.core.logger import CPLog |
||||
|
from bencode import bencode as benc, bdecode |
||||
|
|
||||
|
|
||||
|
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], api_key = self.conf('api_key')) |
||||
|
|
||||
|
return True |
||||
|
|
||||
|
def download(self, data = None, media = None, filedata = None): |
||||
|
""" Send a torrent/nzb file to the downloader |
||||
|
|
||||
|
:param data: dict returned from provider |
||||
|
Contains the release information |
||||
|
:param media: media dict with information |
||||
|
Used for creating the filename when possible |
||||
|
:param filedata: downloaded torrent/nzb filedata |
||||
|
The file gets downloaded in the searcher and send to this function |
||||
|
This is done to have failed checking before using the downloader, so the downloader |
||||
|
doesn't need to worry about that |
||||
|
:return: boolean |
||||
|
One faile returns false, but the downloaded should log his own errors |
||||
|
""" |
||||
|
|
||||
|
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: |
||||
|
info = bdecode(filedata)['info'] |
||||
|
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 |
||||
|
|
||||
|
# The minimum required version of Hadouken is 4.5.6. |
||||
|
if LooseVersion(version) >= LooseVersion('4.5.6'): |
||||
|
return True |
||||
|
|
||||
|
log.error('Hadouken v4.5.6 (or newer) required. Found v%s', version) |
||||
|
return False |
||||
|
|
||||
|
def getAllDownloadStatus(self, ids): |
||||
|
""" Get status of all active downloads |
||||
|
|
||||
|
:param ids: list of (mixed) downloader ids |
||||
|
Used to match the releases for this downloader as there could be |
||||
|
other downloaders active that it should ignore |
||||
|
:return: list of releases |
||||
|
""" |
||||
|
|
||||
|
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_filelist = self.hadouken_api.get_files_by_hash(torrent['InfoHash']) |
||||
|
torrent_files = [] |
||||
|
|
||||
|
save_path = torrent['SavePath'] |
||||
|
|
||||
|
# The 'Path' key for each file_item contains |
||||
|
# the full path to the single file relative to the |
||||
|
# torrents save path. |
||||
|
|
||||
|
# For a single file torrent the result would be, |
||||
|
# - Save path: "C:\Downloads" |
||||
|
# - file_item['Path'] = "file1.iso" |
||||
|
# Resulting path: "C:\Downloads\file1.iso" |
||||
|
|
||||
|
# For a multi file torrent the result would be, |
||||
|
# - Save path: "C:\Downloads" |
||||
|
# - file_item['Path'] = "dirname/file1.iso" |
||||
|
# Resulting path: "C:\Downloads\dirname/file1.iso" |
||||
|
|
||||
|
for file_item in torrent_filelist: |
||||
|
torrent_files.append(sp(os.path.join(save_path, file_item['Path']))) |
||||
|
|
||||
|
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(save_path if len(torrent_files == 1) else os.path.join(save_path, torrent['Name'])), |
||||
|
'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, api_key = None): |
||||
|
self.url = 'http://' + str(host) + ':' + str(port) |
||||
|
self.api_key = api_key |
||||
|
self.requestId = 0; |
||||
|
|
||||
|
self.opener = urllib2.build_opener() |
||||
|
self.opener.addheaders = [('User-agent', 'couchpotato-hadouken-client/1.0'), ('Accept', 'application/json')] |
||||
|
|
||||
|
if not api_key: |
||||
|
log.error('API key missing.') |
||||
|
|
||||
|
def add_file(self, filedata, torrent_params): |
||||
|
""" Add a file to Hadouken with the specified parameters. |
||||
|
|
||||
|
Keyword arguments: |
||||
|
filedata -- The binary torrent data. |
||||
|
torrent_params -- Additional parameters for the file. |
||||
|
""" |
||||
|
data = { |
||||
|
'method': 'torrents.addFile', |
||||
|
'params': [b64encode(filedata), torrent_params] |
||||
|
} |
||||
|
|
||||
|
return self._request(data) |
||||
|
|
||||
|
def add_magnet_link(self, magnetLink, torrent_params): |
||||
|
""" Add a magnet link to Hadouken with the specified parameters. |
||||
|
|
||||
|
Keyword arguments: |
||||
|
magnetLink -- The magnet link to send. |
||||
|
torrent_params -- Additional parameters for the magnet link. |
||||
|
""" |
||||
|
data = { |
||||
|
'method': 'torrents.addUrl', |
||||
|
'params': [magnetLink, torrent_params] |
||||
|
} |
||||
|
|
||||
|
return self._request(data) |
||||
|
|
||||
|
def get_by_hash_list(self, infoHashList): |
||||
|
""" Gets a list of torrents filtered by the given info hash list. |
||||
|
|
||||
|
Keyword arguments: |
||||
|
infoHashList -- A list of info hashes. |
||||
|
""" |
||||
|
data = { |
||||
|
'method': 'torrents.getByInfoHashList', |
||||
|
'params': [infoHashList] |
||||
|
} |
||||
|
|
||||
|
return self._request(data) |
||||
|
|
||||
|
def get_files_by_hash(self, infoHash): |
||||
|
""" Gets a list of files for the torrent identified by the |
||||
|
given info hash. |
||||
|
|
||||
|
Keyword arguments: |
||||
|
infoHash -- The info hash of the torrent to return files for. |
||||
|
""" |
||||
|
data = { |
||||
|
'method': 'torrents.getFiles', |
||||
|
'params': [infoHash] |
||||
|
} |
||||
|
|
||||
|
return self._request(data) |
||||
|
|
||||
|
def get_version(self): |
||||
|
""" Gets the version, commitish and build date of Hadouken. """ |
||||
|
data = { |
||||
|
'method': 'core.getVersion', |
||||
|
'params': None |
||||
|
} |
||||
|
|
||||
|
result = self._request(data) |
||||
|
|
||||
|
if not result: |
||||
|
return False |
||||
|
|
||||
|
return result['Version'] |
||||
|
|
||||
|
def pause(self, infoHash, pause): |
||||
|
""" Pauses/unpauses the torrent identified by the given info hash. |
||||
|
|
||||
|
Keyword arguments: |
||||
|
infoHash -- The info hash of the torrent to operate on. |
||||
|
pause -- If true, pauses the torrent. Otherwise resumes. |
||||
|
""" |
||||
|
data = { |
||||
|
'method': 'torrents.pause', |
||||
|
'params': [infoHash] |
||||
|
} |
||||
|
|
||||
|
if not pause: |
||||
|
data['method'] = 'torrents.resume' |
||||
|
|
||||
|
return self._request(data) |
||||
|
|
||||
|
def remove(self, infoHash, remove_data = False): |
||||
|
""" Removes the torrent identified by the given info hash and |
||||
|
optionally removes the data as well. |
||||
|
|
||||
|
Keyword arguments: |
||||
|
infoHash -- The info hash of the torrent to remove. |
||||
|
remove_data -- If true, removes the data associated with the torrent. |
||||
|
""" |
||||
|
data = { |
||||
|
'method': 'torrents.remove', |
||||
|
'params': [infoHash, remove_data] |
||||
|
} |
||||
|
|
||||
|
return self._request(data) |
||||
|
|
||||
|
|
||||
|
def _request(self, data): |
||||
|
self.requestId += 1 |
||||
|
|
||||
|
data['jsonrpc'] = '2.0' |
||||
|
data['id'] = self.requestId |
||||
|
|
||||
|
request = urllib2.Request(self.url + '/jsonrpc', data = json.dumps(data)) |
||||
|
request.add_header('Authorization', 'Token ' + self.api_key) |
||||
|
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.5.6) to download torrents.', |
||||
|
'wizard': True, |
||||
|
'options': [ |
||||
|
{ |
||||
|
'name': 'enabled', |
||||
|
'default': 0, |
||||
|
'type': 'enabler', |
||||
|
'radio_group': 'torrent' |
||||
|
}, |
||||
|
{ |
||||
|
'name': 'host', |
||||
|
'default': 'localhost:7890' |
||||
|
}, |
||||
|
{ |
||||
|
'name': 'api_key', |
||||
|
'label': 'API key', |
||||
|
'type': 'password' |
||||
|
}, |
||||
|
{ |
||||
|
'name': 'label', |
||||
|
'description': 'Label to add torrent as.' |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
}] |
@ -0,0 +1,63 @@ |
|||||
|
from .main import PutIO |
||||
|
|
||||
|
|
||||
|
def autoload(): |
||||
|
return PutIO() |
||||
|
|
||||
|
|
||||
|
config = [{ |
||||
|
'name': 'putio', |
||||
|
'groups': [ |
||||
|
{ |
||||
|
'tab': 'downloaders', |
||||
|
'list': 'download_providers', |
||||
|
'name': 'putio', |
||||
|
'label': 'put.io', |
||||
|
'description': 'This will start a torrent download on <a href="http://put.io">Put.io</a>.', |
||||
|
'wizard': True, |
||||
|
'options': [ |
||||
|
{ |
||||
|
'name': 'enabled', |
||||
|
'default': 0, |
||||
|
'type': 'enabler', |
||||
|
'radio_group': 'torrent', |
||||
|
}, |
||||
|
{ |
||||
|
'name': 'oauth_token', |
||||
|
'label': 'oauth_token', |
||||
|
'description': 'This is the OAUTH_TOKEN from your putio API', |
||||
|
'advanced': True, |
||||
|
}, |
||||
|
{ |
||||
|
'name': 'callback_host', |
||||
|
'description': 'External reachable url to CP so put.io can do it\'s thing', |
||||
|
}, |
||||
|
{ |
||||
|
'name': 'download', |
||||
|
'description': 'Set this to have CouchPotato download the file from Put.io', |
||||
|
'type': 'bool', |
||||
|
'default': 0, |
||||
|
}, |
||||
|
{ |
||||
|
'name': 'delete_file', |
||||
|
'description': ('Set this to remove the file from putio after sucessful download','Does nothing if you don\'t select download'), |
||||
|
'type': 'bool', |
||||
|
'default': 0, |
||||
|
}, |
||||
|
{ |
||||
|
'name': 'download_dir', |
||||
|
'type': 'directory', |
||||
|
'label': 'Download Directory', |
||||
|
'description': 'The Directory to download files to, does nothing if you don\'t select download', |
||||
|
}, |
||||
|
{ |
||||
|
'name': 'manual', |
||||
|
'default': 0, |
||||
|
'type': 'bool', |
||||
|
'advanced': True, |
||||
|
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', |
||||
|
}, |
||||
|
], |
||||
|
} |
||||
|
], |
||||
|
}] |
@ -0,0 +1,158 @@ |
|||||
|
from couchpotato.api import addApiView |
||||
|
from couchpotato.core.event import addEvent, fireEventAsync |
||||
|
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList |
||||
|
from couchpotato.core.helpers.variable import cleanHost |
||||
|
from couchpotato.core.logger import CPLog |
||||
|
from couchpotato.environment import Env |
||||
|
from pio import api as pio |
||||
|
import datetime |
||||
|
|
||||
|
log = CPLog(__name__) |
||||
|
|
||||
|
autoload = 'Putiodownload' |
||||
|
|
||||
|
|
||||
|
class PutIO(DownloaderBase): |
||||
|
|
||||
|
protocol = ['torrent', 'torrent_magnet'] |
||||
|
downloading_list = [] |
||||
|
oauth_authenticate = 'https://api.couchpota.to/authorize/putio/' |
||||
|
|
||||
|
def __init__(self): |
||||
|
addApiView('downloader.putio.getfrom', self.getFromPutio, docs = { |
||||
|
'desc': 'Allows you to download file from prom Put.io', |
||||
|
}) |
||||
|
addApiView('downloader.putio.auth_url', self.getAuthorizationUrl) |
||||
|
addApiView('downloader.putio.credentials', self.getCredentials) |
||||
|
addEvent('putio.download', self.putioDownloader) |
||||
|
|
||||
|
return super(PutIO, self).__init__() |
||||
|
|
||||
|
def download(self, data = None, media = None, filedata = None): |
||||
|
if not media: media = {} |
||||
|
if not data: data = {} |
||||
|
|
||||
|
log.info('Sending "%s" to put.io', data.get('name')) |
||||
|
url = data.get('url') |
||||
|
|
||||
|
client = pio.Client(self.conf('oauth_token')) |
||||
|
# It might be possible to call getFromPutio from the renamer if we can then we don't need to do this. |
||||
|
# Note callback_host is NOT our address, it's the internet host that putio can call too |
||||
|
callbackurl = None |
||||
|
if self.conf('download'): |
||||
|
callbackurl = 'http://' + self.conf('callback_host') + '/' + '%sdownloader.putio.getfrom/' %Env.get('api_base'.strip('/')) |
||||
|
resp = client.Transfer.add_url(url, callback_url = callbackurl) |
||||
|
log.debug('resp is %s', resp.id); |
||||
|
return self.downloadReturnId(resp.id) |
||||
|
|
||||
|
def test(self): |
||||
|
try: |
||||
|
client = pio.Client(self.conf('oauth_token')) |
||||
|
if client.File.list(): |
||||
|
return True |
||||
|
except: |
||||
|
log.info('Failed to get file listing, check OAUTH_TOKEN') |
||||
|
return False |
||||
|
|
||||
|
def getAuthorizationUrl(self, host = None, **kwargs): |
||||
|
|
||||
|
callback_url = cleanHost(host) + '%sdownloader.putio.credentials/' % (Env.get('api_base').lstrip('/')) |
||||
|
log.debug('callback_url is %s', callback_url) |
||||
|
|
||||
|
target_url = self.oauth_authenticate + "?target=" + callback_url |
||||
|
log.debug('target_url is %s', target_url) |
||||
|
|
||||
|
return { |
||||
|
'success': True, |
||||
|
'url': target_url, |
||||
|
} |
||||
|
|
||||
|
def getCredentials(self, **kwargs): |
||||
|
try: |
||||
|
oauth_token = kwargs.get('oauth') |
||||
|
except: |
||||
|
return 'redirect', Env.get('web_base') + 'settings/downloaders/' |
||||
|
log.debug('oauth_token is: %s', oauth_token) |
||||
|
self.conf('oauth_token', value = oauth_token); |
||||
|
return 'redirect', Env.get('web_base') + 'settings/downloaders/' |
||||
|
|
||||
|
def getAllDownloadStatus(self, ids): |
||||
|
|
||||
|
log.debug('Checking putio download status.') |
||||
|
client = pio.Client(self.conf('oauth_token')) |
||||
|
|
||||
|
transfers = client.Transfer.list() |
||||
|
|
||||
|
log.debug(transfers); |
||||
|
release_downloads = ReleaseDownloadList(self) |
||||
|
for t in transfers: |
||||
|
if t.id in ids: |
||||
|
|
||||
|
log.debug('downloading list is %s', self.downloading_list) |
||||
|
if t.status == "COMPLETED" and self.conf('download') == False : |
||||
|
status = 'completed' |
||||
|
|
||||
|
# So check if we are trying to download something |
||||
|
elif t.status == "COMPLETED" and self.conf('download') == True: |
||||
|
# Assume we are done |
||||
|
status = 'completed' |
||||
|
if not self.downloading_list: |
||||
|
now = datetime.datetime.utcnow() |
||||
|
date_time = datetime.datetime.strptime(t.finished_at,"%Y-%m-%dT%H:%M:%S") |
||||
|
# We need to make sure a race condition didn't happen |
||||
|
if (now - date_time) < datetime.timedelta(minutes=5): |
||||
|
# 5 minutes haven't passed so we wait |
||||
|
status = 'busy' |
||||
|
else: |
||||
|
# If we have the file_id in the downloading_list mark it as busy |
||||
|
if str(t.file_id) in self.downloading_list: |
||||
|
status = 'busy' |
||||
|
else: |
||||
|
status = 'busy' |
||||
|
release_downloads.append({ |
||||
|
'id' : t.id, |
||||
|
'name': t.name, |
||||
|
'status': status, |
||||
|
'timeleft': t.estimated_time, |
||||
|
}) |
||||
|
|
||||
|
return release_downloads |
||||
|
|
||||
|
def putioDownloader(self, fid): |
||||
|
|
||||
|
log.info('Put.io Real downloader called with file_id: %s',fid) |
||||
|
client = pio.Client(self.conf('oauth_token')) |
||||
|
|
||||
|
log.debug('About to get file List') |
||||
|
files = client.File.list() |
||||
|
downloaddir = self.conf('download_dir') |
||||
|
|
||||
|
for f in files: |
||||
|
if str(f.id) == str(fid): |
||||
|
client.File.download(f, dest = downloaddir, delete_after_download = self.conf('delete_file')) |
||||
|
# Once the download is complete we need to remove it from the running list. |
||||
|
self.downloading_list.remove(fid) |
||||
|
|
||||
|
return True |
||||
|
|
||||
|
def getFromPutio(self, **kwargs): |
||||
|
|
||||
|
try: |
||||
|
file_id = str(kwargs.get('file_id')) |
||||
|
except: |
||||
|
return { |
||||
|
'success' : False, |
||||
|
} |
||||
|
|
||||
|
log.info('Put.io Download has been called file_id is %s', file_id) |
||||
|
if file_id not in self.downloading_list: |
||||
|
self.downloading_list.append(file_id) |
||||
|
fireEventAsync('putio.download',fid = file_id) |
||||
|
return { |
||||
|
'success': True, |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
'success': False, |
||||
|
} |
||||
|
|
@ -0,0 +1,68 @@ |
|||||
|
var PutIODownloader = new Class({ |
||||
|
|
||||
|
initialize: function(){ |
||||
|
var self = this; |
||||
|
|
||||
|
App.addEvent('loadSettings', self.addRegisterButton.bind(self)); |
||||
|
}, |
||||
|
|
||||
|
addRegisterButton: function(){ |
||||
|
var self = this; |
||||
|
|
||||
|
var setting_page = App.getPage('Settings'); |
||||
|
setting_page.addEvent('create', function(){ |
||||
|
|
||||
|
var fieldset = setting_page.tabs.downloaders.groups.putio, |
||||
|
l = window.location; |
||||
|
|
||||
|
var putio_set = 0; |
||||
|
fieldset.getElements('input[type=text]').each(function(el){ |
||||
|
putio_set += +(el.get('value') != ''); |
||||
|
}); |
||||
|
|
||||
|
new Element('.ctrlHolder').adopt( |
||||
|
|
||||
|
// Unregister button
|
||||
|
(putio_set > 0) ? |
||||
|
[ |
||||
|
self.unregister = new Element('a.button.red', { |
||||
|
'text': 'Unregister "'+fieldset.getElement('input[name*=oauth_token]').get('value')+'"', |
||||
|
'events': { |
||||
|
'click': function(){ |
||||
|
fieldset.getElements('input[name*=oauth_token]').set('value', '').fireEvent('change'); |
||||
|
|
||||
|
self.unregister.destroy(); |
||||
|
self.unregister_or.destroy(); |
||||
|
} |
||||
|
} |
||||
|
}), |
||||
|
self.unregister_or = new Element('span[text=or]') |
||||
|
] |
||||
|
: null, |
||||
|
|
||||
|
// Register button
|
||||
|
new Element('a.button', { |
||||
|
'text': putio_set > 0 ? 'Register a different account' : 'Register your put.io account', |
||||
|
'events': { |
||||
|
'click': function(){ |
||||
|
Api.request('downloader.putio.auth_url', { |
||||
|
'data': { |
||||
|
'host': l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '') |
||||
|
}, |
||||
|
'onComplete': function(json){ |
||||
|
window.location = json.url; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
).inject(fieldset.getElement('.test_button'), 'before'); |
||||
|
}) |
||||
|
|
||||
|
} |
||||
|
|
||||
|
}); |
||||
|
|
||||
|
window.addEvent('domready', function(){ |
||||
|
new PutIODownloader(); |
||||
|
}); |
@ -0,0 +1,126 @@ |
|||||
|
import traceback |
||||
|
|
||||
|
from bs4 import BeautifulSoup |
||||
|
from couchpotato.core.helpers.variable import tryInt |
||||
|
from couchpotato.core.logger import CPLog |
||||
|
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider |
||||
|
import six |
||||
|
|
||||
|
|
||||
|
log = CPLog(__name__) |
||||
|
|
||||
|
|
||||
|
class Base(TorrentProvider): |
||||
|
|
||||
|
urls = { |
||||
|
'test': 'https://www.torrentleech.org/', |
||||
|
'login': 'https://www.torrentleech.org/user/account/login/', |
||||
|
'login_check': 'https://torrentleech.org/user/messages', |
||||
|
'detail': 'https://www.torrentleech.org/torrent/%s', |
||||
|
'search': 'https://www.torrentleech.org/torrents/browse/index/query/%s/categories/%d', |
||||
|
'download': 'https://www.torrentleech.org%s', |
||||
|
} |
||||
|
|
||||
|
http_time_between_calls = 1 # Seconds |
||||
|
cat_backup_id = None |
||||
|
|
||||
|
def _searchOnTitle(self, title, media, quality, results): |
||||
|
|
||||
|
url = self.urls['search'] % self.buildUrl(title, media, quality) |
||||
|
|
||||
|
data = self.getHTMLData(url) |
||||
|
|
||||
|
if data: |
||||
|
html = BeautifulSoup(data) |
||||
|
|
||||
|
try: |
||||
|
result_table = html.find('table', attrs = {'id': 'torrenttable'}) |
||||
|
if not result_table: |
||||
|
return |
||||
|
|
||||
|
entries = result_table.find_all('tr') |
||||
|
|
||||
|
for result in entries[1:]: |
||||
|
|
||||
|
link = result.find('td', attrs = {'class': 'name'}).find('a') |
||||
|
url = result.find('td', attrs = {'class': 'quickdownload'}).find('a') |
||||
|
details = result.find('td', attrs = {'class': 'name'}).find('a') |
||||
|
|
||||
|
results.append({ |
||||
|
'id': link['href'].replace('/torrent/', ''), |
||||
|
'name': six.text_type(link.string), |
||||
|
'url': self.urls['download'] % url['href'], |
||||
|
'detail_url': self.urls['download'] % details['href'], |
||||
|
'size': self.parseSize(result.find_all('td')[4].string), |
||||
|
'seeders': tryInt(result.find('td', attrs = {'class': 'seeders'}).string), |
||||
|
'leechers': tryInt(result.find('td', attrs = {'class': 'leechers'}).string), |
||||
|
}) |
||||
|
|
||||
|
except: |
||||
|
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc())) |
||||
|
|
||||
|
def getLoginParams(self): |
||||
|
return { |
||||
|
'username': self.conf('username'), |
||||
|
'password': self.conf('password'), |
||||
|
'remember_me': 'on', |
||||
|
'login': 'submit', |
||||
|
} |
||||
|
|
||||
|
def loginSuccess(self, output): |
||||
|
return '/user/account/logout' in output.lower() or 'welcome back' in output.lower() |
||||
|
|
||||
|
loginCheckSuccess = loginSuccess |
||||
|
|
||||
|
|
||||
|
config = [{ |
||||
|
'name': 'torrentleech', |
||||
|
'groups': [ |
||||
|
{ |
||||
|
'tab': 'searcher', |
||||
|
'list': 'torrent_providers', |
||||
|
'name': 'TorrentLeech', |
||||
|
'description': '<a href="http://torrentleech.org">TorrentLeech</a>', |
||||
|
'wizard': True, |
||||
|
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAACHUlEQVR4AZVSO48SYRSdGTCBEMKzILLAWiybkKAGMZRUUJEoDZX7B9zsbuQPYEEjNLTQkYgJDwsoSaxspEBsCITXjjNAIKi8AkzceXgmbHQ1NJ5iMufmO9/9zrmXlCSJ+B8o75J8Pp/NZj0eTzweBy0Wi4PBYD6f12o1r9ebTCZx+22HcrnMsuxms7m6urTZ7LPZDMVYLBZ8ZV3yo8aq9Pq0wzCMTqe77dDv9y8uLyAWBH6xWOyL0K/56fcb+rrPgPZ6PZfLRe1fsl6vCUmGKIqoqNXqdDr9Dbjps9znUV0uTqdTjuPkDoVCIfcuJ4gizjMMm8u9vW+1nr04czqdK56c37CbKY9j2+1WEARZ0Gq1RFHAz2q1qlQqXxoN69HRcDjUarW8ZD6QUigUOnY8uKYH8N1sNkul9yiGw+F6vS4Rxn8EsodEIqHRaOSnq9T7ajQazWQycEIR1AEBYDabSZJyHDucJyegwWBQr9ebTCaKvHd4cCQANUU9evwQ1Ofz4YvUKUI43GE8HouSiFiNRhOowWBIpVLyHITJkuW3PwgAEf3pgIwxF5r+OplMEsk3CPT5szCMnY7EwUdhwUh/CXiej0Qi3idPz89fdrpdbsfBzH7S3Q9K5pP4c0sAKpVKoVAQGO1ut+t0OoFAQHkH2Da/3/+but3uarWK0ZMQoNdyucRutdttmqZxMTzY7XaYxsrgtUjEZrNhkSwWyy/0NCatZumrNQAAAABJRU5ErkJggg==', |
||||
|
'options': [ |
||||
|
{ |
||||
|
'name': 'enabled', |
||||
|
'type': 'enabler', |
||||
|
'default': False, |
||||
|
}, |
||||
|
{ |
||||
|
'name': 'username', |
||||
|
'default': '', |
||||
|
}, |
||||
|
{ |
||||
|
'name': 'password', |
||||
|
'default': '', |
||||
|
'type': 'password', |
||||
|
}, |
||||
|
{ |
||||
|
'name': 'seed_ratio', |
||||
|
'label': 'Seed ratio', |
||||
|
'type': 'float', |
||||
|
'default': 1, |
||||
|
'description': 'Will not be (re)moved until this seed ratio is met.', |
||||
|
}, |
||||
|
{ |
||||
|
'name': 'seed_time', |
||||
|
'label': 'Seed time', |
||||
|
'type': 'int', |
||||
|
'default': 40, |
||||
|
'description': 'Will not be (re)moved until this seed time (in hours) is met.', |
||||
|
}, |
||||
|
{ |
||||
|
'name': 'extra_score', |
||||
|
'advanced': True, |
||||
|
'label': 'Extra Score', |
||||
|
'type': 'int', |
||||
|
'default': 20, |
||||
|
'description': 'Starting score for each release found via this provider.', |
||||
|
} |
||||
|
], |
||||
|
}, |
||||
|
], |
||||
|
}] |
@ -0,0 +1,27 @@ |
|||||
|
from couchpotato.core.helpers.encoding import tryUrlencode |
||||
|
from couchpotato.core.logger import CPLog |
||||
|
from couchpotato.core.media._base.providers.torrent.torrentleech import Base |
||||
|
from couchpotato.core.media.movie.providers.base import MovieProvider |
||||
|
|
||||
|
log = CPLog(__name__) |
||||
|
|
||||
|
autoload = 'TorrentLeech' |
||||
|
|
||||
|
|
||||
|
class TorrentLeech(MovieProvider, Base): |
||||
|
|
||||
|
cat_ids = [ |
||||
|
([13], ['720p', '1080p', 'bd50']), |
||||
|
([8], ['cam']), |
||||
|
([9], ['ts', 'tc']), |
||||
|
([10], ['r5', 'scr']), |
||||
|
([11], ['dvdrip']), |
||||
|
([14], ['brrip']), |
||||
|
([12], ['dvdr']), |
||||
|
] |
||||
|
|
||||
|
def buildUrl(self, title, media, quality): |
||||
|
return ( |
||||
|
tryUrlencode(title.replace(':', '')), |
||||
|
self.getCatId(quality)[0] |
||||
|
) |
@ -0,0 +1,271 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
|
||||
|
# Changed |
||||
|
# Removed iso8601 library requirement |
||||
|
# Added CP logging |
||||
|
|
||||
|
import os |
||||
|
import re |
||||
|
import json |
||||
|
import webbrowser |
||||
|
from urllib import urlencode |
||||
|
from couchpotato import CPLog |
||||
|
from dateutil.parser import parse |
||||
|
|
||||
|
import requests |
||||
|
|
||||
|
BASE_URL = 'https://api.put.io/v2' |
||||
|
ACCESS_TOKEN_URL = 'https://api.put.io/v2/oauth2/access_token' |
||||
|
AUTHENTICATION_URL = 'https://api.put.io/v2/oauth2/authenticate' |
||||
|
|
||||
|
log = CPLog(__name__) |
||||
|
|
||||
|
|
||||
|
class AuthHelper(object): |
||||
|
|
||||
|
def __init__(self, client_id, client_secret, redirect_uri, type='code'): |
||||
|
self.client_id = client_id |
||||
|
self.client_secret = client_secret |
||||
|
self.callback_url = redirect_uri |
||||
|
self.type = type |
||||
|
|
||||
|
@property |
||||
|
def authentication_url(self): |
||||
|
"""Redirect your users to here to authenticate them.""" |
||||
|
params = { |
||||
|
'client_id': self.client_id, |
||||
|
'response_type': self.type, |
||||
|
'redirect_uri': self.callback_url |
||||
|
} |
||||
|
return AUTHENTICATION_URL + "?" + urlencode(params) |
||||
|
|
||||
|
def open_authentication_url(self): |
||||
|
webbrowser.open(self.authentication_url) |
||||
|
|
||||
|
def get_access_token(self, code): |
||||
|
params = { |
||||
|
'client_id': self.client_id, |
||||
|
'client_secret': self.client_secret, |
||||
|
'grant_type': 'authorization_code', |
||||
|
'redirect_uri': self.callback_url, |
||||
|
'code': code |
||||
|
} |
||||
|
response = requests.get(ACCESS_TOKEN_URL, params=params) |
||||
|
log.debug(response) |
||||
|
assert response.status_code == 200 |
||||
|
return response.json()['access_token'] |
||||
|
|
||||
|
|
||||
|
class Client(object): |
||||
|
|
||||
|
def __init__(self, access_token): |
||||
|
self.access_token = access_token |
||||
|
self.session = requests.session() |
||||
|
|
||||
|
# Keep resource classes as attributes of client. |
||||
|
# Pass client to resource classes so resource object |
||||
|
# can use the client. |
||||
|
attributes = {'client': self} |
||||
|
self.File = type('File', (_File,), attributes) |
||||
|
self.Transfer = type('Transfer', (_Transfer,), attributes) |
||||
|
self.Account = type('Account', (_Account,), attributes) |
||||
|
|
||||
|
def request(self, path, method='GET', params=None, data=None, files=None, |
||||
|
headers=None, raw=False, stream=False): |
||||
|
""" |
||||
|
Wrapper around requests.request() |
||||
|
|
||||
|
Prepends BASE_URL to path. |
||||
|
Inserts oauth_token to query params. |
||||
|
Parses response as JSON and returns it. |
||||
|
|
||||
|
""" |
||||
|
if not params: |
||||
|
params = {} |
||||
|
|
||||
|
if not headers: |
||||
|
headers = {} |
||||
|
|
||||
|
# All requests must include oauth_token |
||||
|
params['oauth_token'] = self.access_token |
||||
|
|
||||
|
headers['Accept'] = 'application/json' |
||||
|
|
||||
|
url = BASE_URL + path |
||||
|
log.debug('url: %s', url) |
||||
|
|
||||
|
response = self.session.request( |
||||
|
method, url, params=params, data=data, files=files, |
||||
|
headers=headers, allow_redirects=True, stream=stream) |
||||
|
log.debug('response: %s', response) |
||||
|
if raw: |
||||
|
return response |
||||
|
|
||||
|
log.debug('content: %s', response.content) |
||||
|
try: |
||||
|
response = json.loads(response.content) |
||||
|
except ValueError: |
||||
|
raise Exception('Server didn\'t send valid JSON:\n%s\n%s' % ( |
||||
|
response, response.content)) |
||||
|
|
||||
|
if response['status'] == 'ERROR': |
||||
|
raise Exception(response['error_type']) |
||||
|
|
||||
|
return response |
||||
|
|
||||
|
|
||||
|
class _BaseResource(object): |
||||
|
|
||||
|
client = None |
||||
|
|
||||
|
def __init__(self, resource_dict): |
||||
|
"""Constructs the object from a dict.""" |
||||
|
# All resources must have id and name attributes |
||||
|
self.id = None |
||||
|
self.name = None |
||||
|
self.__dict__.update(resource_dict) |
||||
|
try: |
||||
|
self.created_at = parse(self.created_at) |
||||
|
except AttributeError: |
||||
|
self.created_at = None |
||||
|
|
||||
|
def __str__(self): |
||||
|
return self.name.encode('utf-8') |
||||
|
|
||||
|
def __repr__(self): |
||||
|
# shorten name for display |
||||
|
name = self.name[:17] + '...' if len(self.name) > 20 else self.name |
||||
|
return '<%s id=%r, name="%r">' % ( |
||||
|
self.__class__.__name__, self.id, name) |
||||
|
|
||||
|
|
||||
|
class _File(_BaseResource): |
||||
|
|
||||
|
@classmethod |
||||
|
def get(cls, id): |
||||
|
d = cls.client.request('/files/%i' % id, method='GET') |
||||
|
t = d['file'] |
||||
|
return cls(t) |
||||
|
|
||||
|
@classmethod |
||||
|
def list(cls, parent_id=0): |
||||
|
d = cls.client.request('/files/list', params={'parent_id': parent_id}) |
||||
|
files = d['files'] |
||||
|
return [cls(f) for f in files] |
||||
|
|
||||
|
@classmethod |
||||
|
def upload(cls, path, name=None): |
||||
|
with open(path) as f: |
||||
|
if name: |
||||
|
files = {'file': (name, f)} |
||||
|
else: |
||||
|
files = {'file': f} |
||||
|
d = cls.client.request('/files/upload', method='POST', files=files) |
||||
|
|
||||
|
f = d['file'] |
||||
|
return cls(f) |
||||
|
|
||||
|
def dir(self): |
||||
|
"""List the files under directory.""" |
||||
|
return self.list(parent_id=self.id) |
||||
|
|
||||
|
def download(self, dest='.', delete_after_download=False): |
||||
|
if self.content_type == 'application/x-directory': |
||||
|
self._download_directory(dest, delete_after_download) |
||||
|
else: |
||||
|
self._download_file(dest, delete_after_download) |
||||
|
|
||||
|
def _download_directory(self, dest='.', delete_after_download=False): |
||||
|
name = self.name |
||||
|
if isinstance(name, unicode): |
||||
|
name = name.encode('utf-8', 'replace') |
||||
|
|
||||
|
dest = os.path.join(dest, name) |
||||
|
if not os.path.exists(dest): |
||||
|
os.mkdir(dest) |
||||
|
|
||||
|
for sub_file in self.dir(): |
||||
|
sub_file.download(dest, delete_after_download) |
||||
|
|
||||
|
if delete_after_download: |
||||
|
self.delete() |
||||
|
|
||||
|
def _download_file(self, dest='.', delete_after_download=False): |
||||
|
response = self.client.request( |
||||
|
'/files/%s/download' % self.id, raw=True, stream=True) |
||||
|
|
||||
|
filename = re.match( |
||||
|
'attachment; filename=(.*)', |
||||
|
response.headers['content-disposition']).groups()[0] |
||||
|
# If file name has spaces, it must have quotes around. |
||||
|
filename = filename.strip('"') |
||||
|
|
||||
|
with open(os.path.join(dest, filename), 'wb') as f: |
||||
|
for chunk in response.iter_content(chunk_size=1024): |
||||
|
if chunk: # filter out keep-alive new chunks |
||||
|
f.write(chunk) |
||||
|
f.flush() |
||||
|
|
||||
|
if delete_after_download: |
||||
|
self.delete() |
||||
|
|
||||
|
def delete(self): |
||||
|
return self.client.request('/files/delete', method='POST', |
||||
|
data={'file_ids': str(self.id)}) |
||||
|
|
||||
|
def move(self, parent_id): |
||||
|
return self.client.request('/files/move', method='POST', |
||||
|
data={'file_ids': str(self.id), 'parent_id': str(parent_id)}) |
||||
|
|
||||
|
def rename(self, name): |
||||
|
return self.client.request('/files/rename', method='POST', |
||||
|
data={'file_id': str(self.id), 'name': str(name)}) |
||||
|
|
||||
|
|
||||
|
class _Transfer(_BaseResource): |
||||
|
|
||||
|
@classmethod |
||||
|
def list(cls): |
||||
|
d = cls.client.request('/transfers/list') |
||||
|
transfers = d['transfers'] |
||||
|
return [cls(t) for t in transfers] |
||||
|
|
||||
|
@classmethod |
||||
|
def get(cls, id): |
||||
|
d = cls.client.request('/transfers/%i' % id, method='GET') |
||||
|
t = d['transfer'] |
||||
|
return cls(t) |
||||
|
|
||||
|
@classmethod |
||||
|
def add_url(cls, url, parent_id=0, extract=False, callback_url=None): |
||||
|
d = cls.client.request('/transfers/add', method='POST', data=dict( |
||||
|
url=url, parent_id=parent_id, extract=extract, |
||||
|
callback_url=callback_url)) |
||||
|
t = d['transfer'] |
||||
|
return cls(t) |
||||
|
|
||||
|
@classmethod |
||||
|
def add_torrent(cls, path, parent_id=0, extract=False, callback_url=None): |
||||
|
with open(path) as f: |
||||
|
files = {'file': f} |
||||
|
d = cls.client.request('/files/upload', method='POST', files=files, |
||||
|
data=dict(parent_id=parent_id, |
||||
|
extract=extract, |
||||
|
callback_url=callback_url)) |
||||
|
t = d['transfer'] |
||||
|
return cls(t) |
||||
|
|
||||
|
@classmethod |
||||
|
def clean(cls): |
||||
|
return cls.client.request('/transfers/clean', method='POST') |
||||
|
|
||||
|
|
||||
|
class _Account(_BaseResource): |
||||
|
|
||||
|
@classmethod |
||||
|
def info(cls): |
||||
|
return cls.client.request('/account/info', method='GET') |
||||
|
|
||||
|
@classmethod |
||||
|
def settings(cls): |
||||
|
return cls.client.request('/account/settings', method='GET') |
Loading…
Reference in new issue