diff --git a/couchpotato/core/downloaders/putio.py b/couchpotato/core/downloaders/putio.py deleted file mode 100644 index bc27a66..0000000 --- a/couchpotato/core/downloaders/putio.py +++ /dev/null @@ -1,131 +0,0 @@ -from __future__ import with_statement -import os -import traceback -import putio -import shutil - -from couchpotato.api import addApiView -from couchpotato.core.event import addEvent -from couchpotato.core._base.downloader.main import DownloaderBase -from couchpotato.core.helpers.encoding import sp -from couchpotato.core.helpers.variable import getDownloadDir -from couchpotato.core.logger import CPLog -from couchpotato.environment import Env - -log = CPLog(__name__) - -autoload = 'Putiodownload' - - -class Putiodownload(DownloaderBase): - - protocol = ['torrent', 'torrent_magnet'] - status_support = False - - def __init__(self): - addApiView('putiodownload.getfrom', self.getFromPutio, docs = { - 'desc': 'Allows you to download file from prom Put.io', - }) - return super(Putiodownload,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') - OAUTH_TOKEN = self.conf('oauth_token') - client = putio.Client(OAUTH_TOKEN) - # Need to constuct a the API url a better way. - callbackurl = None - if self.conf('download'): - callbackurl = 'http://'+self.conf('callback_host')+'/'+self.conf('url_base', section='core')+'/api/'+self.conf('api_key', section='core')+'/putiodownload.getfrom/' - client.Transfer.add_url(url,callback_url=callbackurl) - return True - - def test(self): - OAUTH_TOKEN = self.conf('oauth_token') - try: - client = putio.Client(OAUTH_TOKEN) - if client.File.list(): - return True - except: - log.info('Failed to get file listing, check OAUTH_TOKEN') - return False - - def getFromPutio(self, **kwargs): - log.info('Put.io Download has been called') - OAUTH_TOKEN = self.conf('oauth_token') - client = putio.Client(OAUTH_TOKEN) - files = client.File.list() - delete = self.conf('detele_file') - downloaddir = self.conf('download_dir') - tempdownloaddir = self.conf('tempdownload_dir') - for f in files: - if str(f.id) == str(kwargs.get('file_id')): - # Need to read this in from somewhere - client.File.download(f, dest=tempdownloaddir, delete_after_download=delete) - shutil.move(tempdownloaddir+"/"+str(f.name),downloaddir) - return True - -config = [{ - 'name': 'putiodownload', - 'groups': [ - { - 'tab': 'downloaders', - 'list': 'download_providers', - 'name': 'putiodownload', - 'label': 'put.io Download', - 'description': 'This will start a torrent download on Put.io.
Note: you must have a putio account and API', - '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', - }, - { - 'name': 'callback_host', - 'description': 'This is used to generate the callback url', - }, - { - 'name': 'download', - 'description': 'Set this to have CouchPotato download the file from Put.io', - 'type': 'bool', - 'default': 0, - }, - { - 'name': 'detele_file', - 'description': 'Set this to remove the file from putio after sucessful download Note: does nothing if you don\'t select download', - 'type': 'bool', - 'default': 0, - }, - { - 'name': 'download_dir', - 'label': 'Download Directory', - 'description': 'The Directory to download files to, does nothing if you don\'t select download', - 'default': '/', - }, - { - 'name': 'tempdownload_dir', - 'label': 'Temporary Download Directory', - 'description': 'The Temporary Directory to download files to, does nothing if you don\'t select download', - 'default': '/', - }, - { - 'name': 'manual', - 'default': 0, - 'type': 'bool', - 'advanced': True, - 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', - }, - ], - } - ], -}] diff --git a/couchpotato/core/downloaders/putio/__init__.py b/couchpotato/core/downloaders/putio/__init__.py new file mode 100644 index 0000000..1f4865e --- /dev/null +++ b/couchpotato/core/downloaders/putio/__init__.py @@ -0,0 +1,69 @@ +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 Put.io.', + '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': 'This is used to generate the callback url', + }, + { + '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 Note: 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': 'tempdownload_dir', + 'type': 'directory', + 'label': 'Temporary Download Directory', + 'description': 'The Temporary 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.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/downloaders/putio/api.py b/couchpotato/core/downloaders/putio/api.py new file mode 100644 index 0000000..0f2a2c6 --- /dev/null +++ b/couchpotato/core/downloaders/putio/api.py @@ -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') diff --git a/couchpotato/core/downloaders/putio/main.py b/couchpotato/core/downloaders/putio/main.py new file mode 100644 index 0000000..10e6aa1 --- /dev/null +++ b/couchpotato/core/downloaders/putio/main.py @@ -0,0 +1,87 @@ +import shutil + +from couchpotato.api import addApiView +from couchpotato.core._base.downloader.main import DownloaderBase +from couchpotato.core.logger import CPLog +import api as pio + +log = CPLog(__name__) + +autoload = 'Putiodownload' + + +class PutIO(DownloaderBase): + protocol = ['torrent', 'torrent_magnet'] + status_support = False + + 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) + + 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')) + + # Need to constuct a the API url a better way. + callbackurl = None + if self.conf('download'): + callbackurl = 'http://' + self.conf('callback_host') + '/' + self.conf('url_base', + section = 'core') + '/api/' + self.conf( + 'api_key', section = 'core') + '/downloader.putiodownload.getfrom/' + client.Transfer.add_url(url, callback_url = callbackurl) + + return True + + 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): + # See notification/twitter + pass + + def getCredentials(self): + # Save oauth_token here to settings + pass + + def getAllDownloadStatus(self, ids): + # See other downloaders for examples + + # Check putio for status + + # Check "getFromPutio" progress + pass + + def getFromPutio(self, **kwargs): + + log.info('Put.io Download has been called') + client = pio.Client(self.conf('oauth_token')) + files = client.File.list() + + tempdownloaddir = self.conf('tempdownload_dir') + downloaddir = self.conf('download_dir') + + for f in files: + if str(f.id) == str(kwargs.get('file_id')): + # Need to read this in from somewhere + client.File.download(f, dest = tempdownloaddir, delete_after_download = self.conf('delete_file')) + shutil.move(tempdownloaddir + "/" + str(f.name), downloaddir) + + # Mark status of file_id as "done" here for getAllDownloadStatus + + return True diff --git a/couchpotato/core/downloaders/putio/static/putio.js b/couchpotato/core/downloaders/putio/static/putio.js new file mode 100644 index 0000000..1b71c26 --- /dev/null +++ b/couchpotato/core/downloaders/putio/static/putio.js @@ -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*=screen_name]').get('value')+'"', + 'events': { + 'click': function(){ + fieldset.getElements('input[type=text]').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(); +});