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();
+});