diff --git a/CouchPotato.py b/CouchPotato.py index e777f9bf..375a1d41 100755 --- a/CouchPotato.py +++ b/CouchPotato.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +from __future__ import print_function from logging import handlers from os.path import dirname import logging @@ -132,14 +133,15 @@ if __name__ == '__main__': pass except SystemExit: raise - except socket.error as (nr, msg): + except socket.error as e: # log when socket receives SIGINT, but continue. # previous code would have skipped over other types of IO errors too. + nr, msg = e if nr != 4: try: l.log.critical(traceback.format_exc()) except: - print traceback.format_exc() + print(traceback.format_exc()) raise except: try: @@ -148,7 +150,7 @@ if __name__ == '__main__': if l: l.log.critical(traceback.format_exc()) else: - print traceback.format_exc() + print(traceback.format_exc()) except: - print traceback.format_exc() + print(traceback.format_exc()) raise diff --git a/contributing.md b/contributing.md index ef8546f0..d5db0b42 100644 --- a/contributing.md +++ b/contributing.md @@ -1,15 +1,25 @@ -#So you feel like posting a bug, sending me a pull request or just telling me how awesome I am. No problem! +## Got a issue/feature request or submitting a pull request? -##Just make sure you think of the following things: +Make sure you think of the following things: - * Search through the existing (and closed) issues first. See if you can get your answer there. +## Issue + * Search through the existing (and closed) issues first, see if you can get your answer there. * Double check the result manually, because it could be an external issue. * Post logs! Without seeing what is going on, I can't reproduce the error. - * What is the movie + quality you are searching for. - * What are you settings for the specific problem. - * What providers are you using. (While your logs include these, scanning through hundred of lines of log isn't my hobby). - * Give me a short step by step of how to reproduce. + * Also check the logs before submitting, obvious errors like permission or http errors are often not related to CP. + * What is the movie + quality you are searching for? + * What are you're settings for the specific problem? + * What providers are you using? (While you're logs include these, scanning through hundred of lines of log isn't our hobby) + * Post the logs from config directory, please do not copy paste the UI. Use pastebin to store these logs! + * Give a short step by step of how to reproduce the error. * What hardware / OS are you using and what are the limits? NAS can be slow and maybe have a different python installed then when you use CP on OSX or Windows for example. - * I will mark issues with the "can't reproduce" tag. Don't go asking me "why closed" if it clearly says the issue in the tag ;) + * I will mark issues with the "can't reproduce" tag. Don't go asking "why closed" if it clearly says the issue in the tag ;) + * If you're running on a NAS (QNAP, Austor etc..) with pre-made packages, make sure these are setup to use our source repo (RuudBurger/CouchPotatoServer) and nothing else!! -**If I don't get enough info, the chance of the issue getting closed is a lot bigger ;)** +## Pull Request + * Make sure you're pull request is made for develop branch (or relevant feature branch) + * Have you tested your PR? If not, why? + * Are there any limitations of your PR we should know of? + * Make sure to keep you're PR up-to-date with the branch you're trying to push into. + +**If we don't get enough info, the chance of the issue getting closed is a lot bigger ;)** diff --git a/couchpotato/__init__.py b/couchpotato/__init__.py index b8aa3ab9..6b8cfd36 100644 --- a/couchpotato/__init__.py +++ b/couchpotato/__init__.py @@ -9,13 +9,12 @@ import os import time import traceback -log = CPLog(__name__) +log = CPLog(__name__) views = {} template_loader = template.Loader(os.path.join(os.path.dirname(__file__), 'templates')) - class BaseHandler(RequestHandler): def get_current_user(self): @@ -24,9 +23,10 @@ class BaseHandler(RequestHandler): if username and password: return self.get_secure_cookie('user') - else: # Login when no username or password are set + else: # Login when no username or password are set return True + # Main web handler class WebHandler(BaseHandler): @@ -43,11 +43,13 @@ class WebHandler(BaseHandler): log.error("Failed doing web request '%s': %s", (route, traceback.format_exc())) self.write({'success': False, 'error': 'Failed returning results'}) + def addView(route, func, static = False): views[route] = func -def get_session(engine = None): - return Env.getSession(engine) + +def get_session(): + return Env.getSession() # Web view @@ -55,12 +57,10 @@ def index(): return template_loader.load('index.html').generate(sep = os.sep, fireEvent = fireEvent, Env = Env) addView('', index) + # API docs def apiDocs(): - routes = [] - - for route in api.iterkeys(): - routes.append(route) + routes = list(api.keys()) if api_docs.get(''): del api_docs[''] @@ -70,21 +70,22 @@ def apiDocs(): addView('docs', apiDocs) + # Make non basic auth option to get api key class KeyHandler(RequestHandler): def get(self, *args, **kwargs): - api = None + api_key = None try: username = Env.setting('username') password = Env.setting('password') if (self.get_argument('u') == md5(username) or not username) and (self.get_argument('p') == password or not password): - api = Env.setting('api_key') + api_key = Env.setting('api_key') self.write({ - 'success': api is not None, - 'api_key': api + 'success': api_key is not None, + 'api_key': api_key }) except: log.error('Failed doing key request: %s', (traceback.format_exc())) @@ -102,20 +103,21 @@ class LoginHandler(BaseHandler): def post(self, *args, **kwargs): - api = None + api_key = None username = Env.setting('username') password = Env.setting('password') if (self.get_argument('username') == username or not username) and (md5(self.get_argument('password')) == password or not password): - api = Env.setting('api_key') + api_key = Env.setting('api_key') - if api: + if api_key: remember_me = tryInt(self.get_argument('remember_me', default = 0)) - self.set_secure_cookie('user', api, expires_days = 30 if remember_me > 0 else None) + self.set_secure_cookie('user', api_key, expires_days = 30 if remember_me > 0 else None) self.redirect(Env.get('web_base')) + class LogoutHandler(BaseHandler): def get(self, *args, **kwargs): @@ -136,4 +138,3 @@ def page_not_found(rh): rh.set_status(404) rh.write('Wrong API key used') - diff --git a/couchpotato/api.py b/couchpotato/api.py index e86b127f..ba7f7b69 100644 --- a/couchpotato/api.py +++ b/couchpotato/api.py @@ -20,6 +20,7 @@ api_nonblock = {} api_docs = {} api_docs_missing = [] + def run_async(func): @wraps(func) def async_func(*args, **kwargs): @@ -29,6 +30,7 @@ def run_async(func): return async_func + # NonBlock API handler class NonBlockHandler(RequestHandler): @@ -61,6 +63,7 @@ class NonBlockHandler(RequestHandler): self.stopper = None + def addNonBlockApiView(route, func_tuple, docs = None, **kwargs): api_nonblock[route] = func_tuple @@ -69,6 +72,7 @@ def addNonBlockApiView(route, func_tuple, docs = None, **kwargs): else: api_docs_missing.append(route) + # Blocking API handler class ApiHandler(RequestHandler): @@ -98,11 +102,12 @@ class ApiHandler(RequestHandler): @run_async def run_handler(callback): try: - result = api[route](**kwargs) - callback(result) + res = api[route](**kwargs) + callback(res) except: log.error('Failed doing api request "%s": %s', (route, traceback.format_exc())) callback({'success': False, 'error': 'Failed returning results'}) + result = yield tornado.gen.Task(run_handler) # Check JSONP callback @@ -122,6 +127,7 @@ class ApiHandler(RequestHandler): api_locks[route].release() + def addApiView(route, func, static = False, docs = None, **kwargs): if static: func(route) diff --git a/couchpotato/core/_base/_core/__init__.py b/couchpotato/core/_base/_core/__init__.py index 4d1a6840..58965bbb 100644 --- a/couchpotato/core/_base/_core/__init__.py +++ b/couchpotato/core/_base/_core/__init__.py @@ -1,6 +1,7 @@ from .main import Core from uuid import uuid4 + def start(): return Core() diff --git a/couchpotato/core/_base/_core/main.py b/couchpotato/core/_base/_core/main.py index 803ac5a3..02e21f2d 100644 --- a/couchpotato/core/_base/_core/main.py +++ b/couchpotato/core/_base/_core/main.py @@ -55,6 +55,10 @@ class Core(Plugin): if not Env.get('desktop'): self.signalHandler() + # Set default urlopen timeout + import socket + socket.setdefaulttimeout(30) + def md5Password(self, value): return md5(value) if value else '' @@ -113,7 +117,7 @@ class Core(Plugin): if len(still_running) == 0: break - elif starttime < time.time() - 30: # Always force break after 30s wait + elif starttime < time.time() - 30: # Always force break after 30s wait break running = list(set(still_running) - set(self.ignore_restart)) diff --git a/couchpotato/core/_base/clientscript/__init__.py b/couchpotato/core/_base/clientscript/__init__.py index 8490eae7..8070044e 100644 --- a/couchpotato/core/_base/clientscript/__init__.py +++ b/couchpotato/core/_base/clientscript/__init__.py @@ -1,5 +1,6 @@ from .main import ClientScript + def start(): return ClientScript() diff --git a/couchpotato/core/_base/clientscript/main.py b/couchpotato/core/_base/clientscript/main.py index 1b7f1636..c1be7e73 100644 --- a/couchpotato/core/_base/clientscript/main.py +++ b/couchpotato/core/_base/clientscript/main.py @@ -34,6 +34,8 @@ class ClientScript(Plugin): 'scripts/library/question.js', 'scripts/library/scrollspy.js', 'scripts/library/spin.js', + 'scripts/library/Array.stableSort.js', + 'scripts/library/async.js', 'scripts/couchpotato.js', 'scripts/api.js', 'scripts/library/history.js', @@ -47,13 +49,14 @@ class ClientScript(Plugin): 'scripts/page/settings.js', 'scripts/page/about.js', 'scripts/page/manage.js', + 'scripts/misc/downloaders.js', ], } - urls = {'style': {}, 'script': {}, } - minified = {'style': {}, 'script': {}, } - paths = {'style': {}, 'script': {}, } + urls = {'style': {}, 'script': {}} + minified = {'style': {}, 'script': {}} + paths = {'style': {}, 'script': {}} comment = { 'style': '/*** %s:%d ***/\n', 'script': '// %s:%d\n' diff --git a/couchpotato/core/_base/desktop/__init__.py b/couchpotato/core/_base/desktop/__init__.py index 064492f2..e59ca523 100644 --- a/couchpotato/core/_base/desktop/__init__.py +++ b/couchpotato/core/_base/desktop/__init__.py @@ -1,5 +1,6 @@ from .main import Desktop + def start(): return Desktop() diff --git a/couchpotato/core/_base/scheduler/__init__.py b/couchpotato/core/_base/scheduler/__init__.py index aa1c5c90..abfc2305 100644 --- a/couchpotato/core/_base/scheduler/__init__.py +++ b/couchpotato/core/_base/scheduler/__init__.py @@ -1,5 +1,6 @@ from .main import Scheduler + def start(): return Scheduler() diff --git a/couchpotato/core/_base/scheduler/main.py b/couchpotato/core/_base/scheduler/main.py index 2c97e1b4..3d835f94 100644 --- a/couchpotato/core/_base/scheduler/main.py +++ b/couchpotato/core/_base/scheduler/main.py @@ -17,6 +17,7 @@ class Scheduler(Plugin): addEvent('schedule.cron', self.cron) addEvent('schedule.interval', self.interval) addEvent('schedule.remove', self.remove) + addEvent('schedule.queue', self.queue) self.sched = Sched(misfire_grace_time = 60) self.sched.start() @@ -31,13 +32,13 @@ class Scheduler(Plugin): pass def doShutdown(self): - super(Scheduler, self).doShutdown() self.stop() + return super(Scheduler, self).doShutdown() def stop(self): if self.started: log.debug('Stopping scheduler') - self.sched.shutdown() + self.sched.shutdown(wait = False) log.debug('Scheduler stopped') self.started = False @@ -64,3 +65,14 @@ class Scheduler(Plugin): 'seconds': seconds, 'job': self.sched.add_interval_job(handle, hours = hours, minutes = minutes, seconds = seconds) } + + def queue(self, handlers = None): + if not handlers: handlers = [] + + for h in handlers: + h() + + if self.shuttingDown(): + break + + return True diff --git a/couchpotato/core/_base/updater/__init__.py b/couchpotato/core/_base/updater/__init__.py index a304f9e7..7ad30d27 100644 --- a/couchpotato/core/_base/updater/__init__.py +++ b/couchpotato/core/_base/updater/__init__.py @@ -2,6 +2,7 @@ from .main import Updater from couchpotato.environment import Env import os + def start(): return Updater() diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py index aecf0c4f..ef595ad7 100644 --- a/couchpotato/core/_base/updater/main.py +++ b/couchpotato/core/_base/updater/main.py @@ -15,6 +15,7 @@ import time import traceback import version import zipfile +from six.moves import filter log = CPLog(__name__) @@ -32,6 +33,7 @@ class Updater(Plugin): else: self.updater = SourceUpdater() + addEvent('app.load', self.logVersion, priority = 10000) addEvent('app.load', self.setCrons) addEvent('updater.info', self.info) @@ -53,12 +55,16 @@ class Updater(Plugin): addEvent('setting.save.updater.enabled.after', self.setCrons) + def logVersion(self): + info = self.info() + log.info('=== VERSION %s, using %s ===', (info.get('version', {}).get('repr', 'UNKNOWN'), self.updater.getName())) + def setCrons(self): fireEvent('schedule.remove', 'updater.check', single = True) if self.isEnabled(): fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6) - self.autoUpdate() # Check after enabling + self.autoUpdate() # Check after enabling def autoUpdate(self): if self.isEnabled() and self.check() and self.conf('automatic') and not self.updater.update_failed: @@ -146,6 +152,9 @@ class BaseUpdater(Plugin): 'branch': self.branch, } + def getVersion(self): + pass + def check(self): pass @@ -174,7 +183,6 @@ class BaseUpdater(Plugin): log.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc())) - class GitUpdater(BaseUpdater): def __init__(self, git_command): @@ -183,9 +191,6 @@ class GitUpdater(BaseUpdater): def doUpdate(self): try: - log.debug('Stashing local changes') - self.repo.saveStash() - log.info('Updating to latest version') self.repo.pull() @@ -204,14 +209,15 @@ class GitUpdater(BaseUpdater): if not self.version: try: - output = self.repo.getHead() # Yes, please + output = self.repo.getHead() # Yes, please log.debug('Git version output: %s', output.hash) self.version = { + 'repr': 'git:(%s:%s % s) %s (%s)' % (self.repo_user, self.repo_name, self.branch, output.hash[:8], datetime.fromtimestamp(output.getDate())), 'hash': output.hash[:8], 'date': output.getDate(), 'type': 'git', } - except Exception, e: + except Exception as e: log.error('Failed using GIT updater, running from source, you need to have GIT installed. %s', e) return 'No GIT' @@ -234,7 +240,7 @@ class GitUpdater(BaseUpdater): local = self.repo.getHead() remote = branch.getHead() - log.info('Versions, local:%s, remote:%s', (local.hash[:8], remote.hash[:8])) + log.debug('Versions, local:%s, remote:%s', (local.hash[:8], remote.hash[:8])) if local.getDate() < remote.getDate(): self.update_version = { @@ -247,7 +253,6 @@ class GitUpdater(BaseUpdater): return False - class SourceUpdater(BaseUpdater): def __init__(self): @@ -273,9 +278,9 @@ class SourceUpdater(BaseUpdater): # Extract if download_data.get('type') == 'zip': - zip = zipfile.ZipFile(destination) - zip.extractall(extracted_path) - zip.close() + zip_file = zipfile.ZipFile(destination) + zip_file.extractall(extracted_path) + zip_file.close() else: tar = tarfile.open(destination) tar.extractall(path = extracted_path) @@ -342,13 +347,12 @@ class SourceUpdater(BaseUpdater): return True - def removeDir(self, path): try: if os.path.isdir(path): shutil.rmtree(path) - except OSError, inst: - os.chmod(inst.filename, 0777) + except OSError as inst: + os.chmod(inst.filename, 0o777) self.removeDir(path) def getVersion(self): @@ -362,7 +366,8 @@ class SourceUpdater(BaseUpdater): log.debug('Source version output: %s', output) self.version = output self.version['type'] = 'source' - except Exception, e: + self.version['repr'] = 'source:(%s:%s % s) %s (%s)' % (self.repo_user, self.repo_name, self.branch, output.get('hash', '')[:8], datetime.fromtimestamp(output.get('date', 0))) + except Exception as e: log.error('Failed using source updater. %s', e) return {} @@ -392,7 +397,7 @@ class SourceUpdater(BaseUpdater): return { 'hash': commit['sha'], - 'date': int(time.mktime(parse(commit['commit']['committer']['date']).timetuple())), + 'date': int(time.mktime(parse(commit['commit']['committer']['date']).timetuple())), } except: log.error('Failed getting latest request from github: %s', traceback.format_exc()) @@ -437,7 +442,7 @@ class DesktopUpdater(BaseUpdater): if latest and latest != current_version.get('hash'): self.update_version = { 'hash': latest, - 'date': None, + 'date': None, 'changelog': self.desktop._changelogURL, } @@ -449,6 +454,7 @@ class DesktopUpdater(BaseUpdater): def getVersion(self): return { + 'repr': 'desktop: %s' % self.desktop._esky.active_version, 'hash': self.desktop._esky.active_version, 'date': None, 'type': 'desktop', diff --git a/couchpotato/core/_base/updater/static/updater.js b/couchpotato/core/_base/updater/static/updater.js index 0577c783..be436ed2 100644 --- a/couchpotato/core/_base/updater/static/updater.js +++ b/couchpotato/core/_base/updater/static/updater.js @@ -24,7 +24,7 @@ var UpdaterBase = new Class({ self.doUpdate(); else { App.unBlockPage(); - App.fireEvent('message', 'No updates available'); + App.trigger('message', ['No updates available']); } } }) diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index 9e24d914..3bcf1f31 100644 --- a/couchpotato/core/downloaders/base.py +++ b/couchpotato/core/downloaders/base.py @@ -1,4 +1,5 @@ from base64 import b32decode, b16encode +from couchpotato.api import addApiView from couchpotato.core.event import addEvent from couchpotato.core.helpers.variable import mergeDicts from couchpotato.core.logger import CPLog @@ -13,6 +14,7 @@ class Downloader(Provider): protocol = [] http_time_between_calls = 0 + status_support = True torrent_sources = [ 'http://torrage.com/torrent/%s.torrent', @@ -41,6 +43,7 @@ class Downloader(Provider): addEvent('download.remove_failed', self._removeFailed) addEvent('download.pause', self._pause) addEvent('download.process_complete', self._processComplete) + addApiView('download.%s.test' % self.getName().lower(), self._test) def getEnabledProtocol(self): for download_protocol in self.protocol: @@ -49,22 +52,27 @@ class Downloader(Provider): return [] - def _download(self, data = None, movie = None, manual = False, filedata = None): - if not movie: movie = {} + def _download(self, data = None, media = None, manual = False, filedata = None): + if not media: media = {} if not data: data = {} if self.isDisabled(manual, data): return - return self.download(data = data, movie = movie, filedata = filedata) + return self.download(data = data, media = media, filedata = filedata) - def _getAllDownloadStatus(self): + def _getAllDownloadStatus(self, download_ids): if self.isDisabled(manual = True, data = {}): return - return self.getAllDownloadStatus() + ids = [download_id['id'] for download_id in download_ids if download_id['downloader'] == self.getName()] - def getAllDownloadStatus(self): - return + if ids: + return self.getAllDownloadStatus(ids) + else: + return + + def getAllDownloadStatus(self, ids): + return [] def _removeFailed(self, release_download): if self.isDisabled(manual = True, data = {}): @@ -128,6 +136,7 @@ class Downloader(Provider): def downloadReturnId(self, download_id): return { 'downloader': self.getName(), + 'status_support': self.status_support, 'id': download_id } @@ -151,6 +160,15 @@ class Downloader(Provider): (d_manual and manual or d_manual is False) and \ (not data or self.isCorrectProtocol(data.get('protocol'))) + def _test(self): + t = self.test() + if isinstance(t, tuple): + return {'success': t[0], 'msg': t[1]} + return {'success': t} + + def test(self): + return False + def _pause(self, release_download, pause = True): if self.isDisabled(manual = True, data = {}): return diff --git a/couchpotato/core/downloaders/blackhole/__init__.py b/couchpotato/core/downloaders/blackhole/__init__.py index 6b5279a1..92d18e7f 100644 --- a/couchpotato/core/downloaders/blackhole/__init__.py +++ b/couchpotato/core/downloaders/blackhole/__init__.py @@ -1,6 +1,7 @@ from .main import Blackhole from couchpotato.core.helpers.variable import getDownloadDir + def start(): return Blackhole() @@ -13,7 +14,7 @@ config = [{ 'list': 'download_providers', 'name': 'blackhole', 'label': 'Black hole', - 'description': 'Download the NZB/Torrent to a specific folder.', + 'description': 'Download the NZB/Torrent to a specific folder. Note: Seeding and copying/linking features do not work with Black hole.', 'wizard': True, 'options': [ { diff --git a/couchpotato/core/downloaders/blackhole/main.py b/couchpotato/core/downloaders/blackhole/main.py index 854860cd..9a018354 100644 --- a/couchpotato/core/downloaders/blackhole/main.py +++ b/couchpotato/core/downloaders/blackhole/main.py @@ -1,5 +1,6 @@ from __future__ import with_statement from couchpotato.core.downloaders.base import Downloader +from couchpotato.core.helpers.encoding import sp from couchpotato.core.logger import CPLog from couchpotato.environment import Env import os @@ -11,9 +12,10 @@ log = CPLog(__name__) class Blackhole(Downloader): protocol = ['nzb', 'torrent', 'torrent_magnet'] + status_support = False - def download(self, data = None, movie = None, filedata = None): - if not movie: movie = {} + def download(self, data = None, media = None, filedata = None): + if not media: media = {} if not data: data = {} directory = self.conf('directory') @@ -33,7 +35,7 @@ class Blackhole(Downloader): log.error('No nzb/torrent available: %s', data.get('url')) return False - file_name = self.createFileName(data, filedata, movie) + file_name = self.createFileName(data, filedata, media) full_path = os.path.join(directory, file_name) if self.conf('create_subdir'): @@ -51,10 +53,10 @@ class Blackhole(Downloader): with open(full_path, 'wb') as f: f.write(filedata) os.chmod(full_path, Env.getPermission('file')) - return True + return self.downloadReturnId('') else: log.info('File %s already exists.', full_path) - return True + return self.downloadReturnId('') except: log.error('Failed to download to blackhole %s', traceback.format_exc()) @@ -66,6 +68,20 @@ class Blackhole(Downloader): return False + def test(self): + directory = self.conf('directory') + if directory and os.path.isdir(directory): + + test_file = sp(os.path.join(directory, 'couchpotato_test.txt')) + + # Check if folder is writable + self.createFile(test_file, 'This is a test file') + if os.path.isfile(test_file): + os.remove(test_file) + return True + + return False + def getEnabledProtocol(self): if self.conf('use_for') == 'both': return super(Blackhole, self).getEnabledProtocol() diff --git a/couchpotato/core/downloaders/deluge/__init__.py b/couchpotato/core/downloaders/deluge/__init__.py index c7aa26e6..09fae751 100644 --- a/couchpotato/core/downloaders/deluge/__init__.py +++ b/couchpotato/core/downloaders/deluge/__init__.py @@ -1,5 +1,6 @@ from .main import Deluge + def start(): return Deluge() diff --git a/couchpotato/core/downloaders/deluge/main.py b/couchpotato/core/downloaders/deluge/main.py index f3a1238f..59300958 100644 --- a/couchpotato/core/downloaders/deluge/main.py +++ b/couchpotato/core/downloaders/deluge/main.py @@ -2,7 +2,7 @@ from base64 import b64encode, b16encode, b32decode from bencode import bencode as benc, bdecode from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList from couchpotato.core.helpers.encoding import isInt, sp -from couchpotato.core.helpers.variable import tryFloat +from couchpotato.core.helpers.variable import tryFloat, cleanHost from couchpotato.core.logger import CPLog from datetime import timedelta from hashlib import sha1 @@ -20,19 +20,22 @@ class Deluge(Downloader): log = CPLog(__name__) drpc = None - def connect(self): + def connect(self, reconnect = False): # Load host from config and split out port. - host = self.conf('host').split(':') + 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.drpc: + if not self.drpc or reconnect: self.drpc = DelugeRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password')) return self.drpc - def download(self, data, movie, filedata = None): + def download(self, data = None, media = None, filedata = None): + if not media: media = {} + if not data: data = {} + log.info('Sending "%s" (%s) to Deluge.', (data.get('name'), data.get('protocol'))) if not self.connect(): @@ -73,7 +76,7 @@ class Deluge(Downloader): if data.get('protocol') == 'torrent_magnet': remote_torrent = self.drpc.add_torrent_magnet(data.get('url'), options) else: - filename = self.createFileName(data, filedata, movie) + filename = self.createFileName(data, filedata, media) remote_torrent = self.drpc.add_torrent_file(filename, filedata, options) if not remote_torrent: @@ -83,24 +86,34 @@ class Deluge(Downloader): log.info('Torrent sent to Deluge successfully.') return self.downloadReturnId(remote_torrent) - def getAllDownloadStatus(self): + def test(self): + if self.connect(True) and self.drpc.test(): + return True + return False + + def getAllDownloadStatus(self, ids): log.debug('Checking Deluge download status.') if not self.connect(): - return False + return [] release_downloads = ReleaseDownloadList(self) - queue = self.drpc.get_alltorrents() + queue = self.drpc.get_alltorrents(ids) if not queue: log.debug('Nothing in queue or error') - return False + return [] for torrent_id in queue: torrent = queue[torrent_id] - log.debug('name=%s / id=%s / save_path=%s / move_completed_path=%s / hash=%s / progress=%s / state=%s / eta=%s / ratio=%s / stop_ratio=%s / is_seed=%s / is_finished=%s / paused=%s', (torrent['name'], torrent['hash'], torrent['save_path'], torrent['move_completed_path'], torrent['hash'], torrent['progress'], torrent['state'], torrent['eta'], torrent['ratio'], torrent['stop_ratio'], torrent['is_seed'], torrent['is_finished'], torrent['paused'])) + + if not 'hash' in torrent: + # When given a list of ids, deluge will return an empty item for a non-existant torrent. + continue + + log.debug('name=%s / id=%s / save_path=%s / move_on_completed=%s / move_completed_path=%s / hash=%s / progress=%s / state=%s / eta=%s / ratio=%s / stop_ratio=%s / is_seed=%s / is_finished=%s / paused=%s', (torrent['name'], torrent['hash'], torrent['save_path'], torrent['move_on_completed'], torrent['move_completed_path'], torrent['hash'], torrent['progress'], torrent['state'], torrent['eta'], torrent['ratio'], torrent['stop_ratio'], torrent['is_seed'], torrent['is_finished'], torrent['paused'])) # Deluge has no easy way to work out if a torrent is stalled or failing. #status = 'failed' @@ -149,6 +162,7 @@ class Deluge(Downloader): log.debug('Requesting Deluge to remove the torrent %s%s.', (release_download['name'], ' and cleanup the downloaded files' if delete_files else '')) return self.drpc.remove_torrent(release_download['id'], remove_local_data = delete_files) + class DelugeRPC(object): host = 'localhost' @@ -169,6 +183,13 @@ class DelugeRPC(object): self.client = DelugeClient() self.client.connect(self.host, int(self.port), self.username, self.password) + def test(self): + try: + self.connect() + except: + return False + return True + def add_torrent_magnet(self, torrent, options): torrent_id = False try: @@ -179,7 +200,7 @@ class DelugeRPC(object): if torrent_id and options['label']: self.client.label.set_torrent(torrent_id, options['label']).get() - except Exception, err: + except Exception as err: log.error('Failed to add torrent magnet %s: %s %s', (torrent, err, traceback.format_exc())) finally: if self.client: @@ -197,7 +218,7 @@ class DelugeRPC(object): if torrent_id and options['label']: self.client.label.set_torrent(torrent_id, options['label']).get() - except Exception, err: + except Exception as err: log.error('Failed to add torrent file %s: %s %s', (filename, err, traceback.format_exc())) finally: if self.client: @@ -205,12 +226,12 @@ class DelugeRPC(object): return torrent_id - def get_alltorrents(self): + def get_alltorrents(self, ids): ret = False try: self.connect() - ret = self.client.core.get_torrents_status({}, {}).get() - except Exception, err: + ret = self.client.core.get_torrents_status({'id': ids}, ('name', 'hash', 'save_path', 'move_completed_path', 'progress', 'state', 'eta', 'ratio', 'stop_ratio', 'is_seed', 'is_finished', 'paused', 'move_on_completed', 'files')).get() + except Exception as err: log.error('Failed to get all torrents: %s %s', (err, traceback.format_exc())) finally: if self.client: @@ -221,7 +242,7 @@ class DelugeRPC(object): try: self.connect() self.client.core.pause_torrent(torrent_ids).get() - except Exception, err: + except Exception as err: log.error('Failed to pause torrent: %s %s', (err, traceback.format_exc())) finally: if self.client: @@ -231,7 +252,7 @@ class DelugeRPC(object): try: self.connect() self.client.core.resume_torrent(torrent_ids).get() - except Exception, err: + except Exception as err: log.error('Failed to resume torrent: %s %s', (err, traceback.format_exc())) finally: if self.client: @@ -242,7 +263,7 @@ class DelugeRPC(object): try: self.connect() ret = self.client.core.remove_torrent(torrent_id, remove_local_data).get() - except Exception, err: + except Exception as err: log.error('Failed to remove torrent: %s %s', (err, traceback.format_exc())) finally: if self.client: diff --git a/couchpotato/core/downloaders/nzbget/__init__.py b/couchpotato/core/downloaders/nzbget/__init__.py index 00763cfb..551eb42c 100644 --- a/couchpotato/core/downloaders/nzbget/__init__.py +++ b/couchpotato/core/downloaders/nzbget/__init__.py @@ -1,5 +1,6 @@ from .main import NZBGet + def start(): return NZBGet() @@ -25,6 +26,13 @@ config = [{ 'default': 'localhost:6789', 'description': 'Hostname with port. Usually localhost:6789', }, + { + 'name': 'ssl', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Use HyperText Transfer Protocol Secure, or https', + }, { 'name': 'username', 'default': 'nzbget', diff --git a/couchpotato/core/downloaders/nzbget/main.py b/couchpotato/core/downloaders/nzbget/main.py index f8506134..3dad8670 100644 --- a/couchpotato/core/downloaders/nzbget/main.py +++ b/couchpotato/core/downloaders/nzbget/main.py @@ -1,7 +1,7 @@ from base64 import standard_b64encode from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList from couchpotato.core.helpers.encoding import ss, sp -from couchpotato.core.helpers.variable import tryInt, md5 +from couchpotato.core.helpers.variable import tryInt, md5, cleanHost from couchpotato.core.logger import CPLog from datetime import timedelta import re @@ -16,11 +16,10 @@ log = CPLog(__name__) class NZBGet(Downloader): protocol = ['nzb'] + rpc = 'xmlrpc' - url = 'http://%(username)s:%(password)s@%(host)s/xmlrpc' - - def download(self, data = None, movie = None, filedata = None): - if not movie: movie = {} + def download(self, data = None, media = None, filedata = None): + if not media: media = {} if not data: data = {} if not filedata: @@ -29,10 +28,10 @@ class NZBGet(Downloader): log.info('Sending "%s" to NZBGet.', data.get('name')) - url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')} - nzb_name = ss('%s.nzb' % self.createNzbName(data, movie)) + nzb_name = ss('%s.nzb' % self.createNzbName(data, media)) + + rpc = self.getRPC() - rpc = xmlrpclib.ServerProxy(url) try: if rpc.writelog('INFO', 'CouchPotato connected to drop off %s.' % nzb_name): log.debug('Successfully connected to NZBGet') @@ -41,7 +40,7 @@ class NZBGet(Downloader): except socket.error: log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.') return False - except xmlrpclib.ProtocolError, e: + except xmlrpclib.ProtocolError as e: if e.errcode == 401: log.error('Password is incorrect.') else: @@ -55,7 +54,7 @@ class NZBGet(Downloader): if xml_response: log.info('NZB sent successfully to NZBGet') - nzb_id = md5(data['url']) # about as unique as they come ;) + nzb_id = md5(data['url']) # about as unique as they come ;) couchpotato_id = "couchpotato=" + nzb_id groups = rpc.listgroups() file_id = [item['LastID'] for item in groups if item['NZBFilename'] == nzb_name] @@ -67,13 +66,32 @@ class NZBGet(Downloader): log.error('NZBGet could not add %s to the queue.', nzb_name) return False - def getAllDownloadStatus(self): + def test(self): + rpc = self.getRPC() + + try: + if rpc.writelog('INFO', 'CouchPotato connected to test connection'): + log.debug('Successfully connected to NZBGet') + else: + log.info('Successfully connected to NZBGet, but unable to send a message') + except socket.error: + log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.') + return False + except xmlrpclib.ProtocolError as e: + if e.errcode == 401: + log.error('Password is incorrect.') + else: + log.error('Protocol Error: %s', e) + return False + + return True + + def getAllDownloadStatus(self, ids): log.debug('Checking NZBGet download status.') - url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')} + rpc = self.getRPC() - rpc = xmlrpclib.ServerProxy(url) try: if rpc.writelog('INFO', 'CouchPotato connected to check status'): log.debug('Successfully connected to NZBGet') @@ -81,13 +99,13 @@ class NZBGet(Downloader): log.info('Successfully connected to NZBGet, but unable to send a message') except socket.error: log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.') - return False - except xmlrpclib.ProtocolError, e: + return [] + except xmlrpclib.ProtocolError as e: if e.errcode == 401: log.error('Password is incorrect.') else: log.error('Protocol Error: %s', e) - return False + return [] # Get NZBGet data try: @@ -97,56 +115,59 @@ class NZBGet(Downloader): history = rpc.history() except: log.error('Failed getting data: %s', traceback.format_exc(1)) - return False + return [] release_downloads = ReleaseDownloadList(self) for nzb in groups: - log.debug('Found %s in NZBGet download queue', nzb['NZBFilename']) try: nzb_id = [param['Value'] for param in nzb['Parameters'] if param['Name'] == 'couchpotato'][0] except: nzb_id = nzb['NZBID'] + if nzb_id in ids: + log.debug('Found %s in NZBGet download queue', nzb['NZBFilename']) + timeleft = -1 + try: + if nzb['ActiveDownloads'] > 0 and nzb['DownloadRate'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']): + timeleft = str(timedelta(seconds = nzb['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20)) + except: + pass - timeleft = -1 - try: - if nzb['ActiveDownloads'] > 0 and nzb['DownloadRate'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']): - timeleft = str(timedelta(seconds = nzb['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20)) - except: - pass - - release_downloads.append({ - 'id': nzb_id, - 'name': nzb['NZBFilename'], - 'original_status': 'DOWNLOADING' if nzb['ActiveDownloads'] > 0 else 'QUEUED', - # Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item - 'timeleft': timeleft, - }) + release_downloads.append({ + 'id': nzb_id, + 'name': nzb['NZBFilename'], + 'original_status': 'DOWNLOADING' if nzb['ActiveDownloads'] > 0 else 'QUEUED', + # Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item + 'timeleft': timeleft, + }) for nzb in queue: # 'Parameters' is not passed in rpc.postqueue - log.debug('Found %s in NZBGet postprocessing queue', nzb['NZBFilename']) - release_downloads.append({ - 'id': nzb['NZBID'], - 'name': nzb['NZBFilename'], - 'original_status': nzb['Stage'], - 'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1, - }) + if nzb['NZBID'] in ids: + log.debug('Found %s in NZBGet postprocessing queue', nzb['NZBFilename']) + release_downloads.append({ + 'id': nzb['NZBID'], + 'name': nzb['NZBFilename'], + 'original_status': nzb['Stage'], + 'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1, + }) for nzb in history: - log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (nzb['NZBFilename'] , nzb['ParStatus'], nzb['ScriptStatus'] , nzb['Log'])) try: nzb_id = [param['Value'] for param in nzb['Parameters'] if param['Name'] == 'couchpotato'][0] except: nzb_id = nzb['NZBID'] - release_downloads.append({ - 'id': nzb_id, - 'name': nzb['NZBFilename'], - 'status': 'completed' if nzb['ParStatus'] in ['SUCCESS', 'NONE'] and nzb['ScriptStatus'] in ['SUCCESS', 'NONE'] else 'failed', - 'original_status': nzb['ParStatus'] + ', ' + nzb['ScriptStatus'], - 'timeleft': str(timedelta(seconds = 0)), - 'folder': sp(nzb['DestDir']) - }) + + if nzb_id in ids: + log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (nzb['NZBFilename'] , nzb['ParStatus'], nzb['ScriptStatus'] , nzb['Log'])) + release_downloads.append({ + 'id': nzb_id, + 'name': nzb['NZBFilename'], + 'status': 'completed' if nzb['ParStatus'] in ['SUCCESS', 'NONE'] and nzb['ScriptStatus'] in ['SUCCESS', 'NONE'] else 'failed', + 'original_status': nzb['ParStatus'] + ', ' + nzb['ScriptStatus'], + 'timeleft': str(timedelta(seconds = 0)), + 'folder': sp(nzb['DestDir']) + }) return release_downloads @@ -154,9 +175,8 @@ class NZBGet(Downloader): log.info('%s failed downloading, deleting...', release_download['name']) - url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')} + rpc = self.getRPC() - rpc = xmlrpclib.ServerProxy(url) try: if rpc.writelog('INFO', 'CouchPotato connected to delete some history'): log.debug('Successfully connected to NZBGet') @@ -165,7 +185,7 @@ class NZBGet(Downloader): except socket.error: log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.') return False - except xmlrpclib.ProtocolError, e: + except xmlrpclib.ProtocolError as e: if e.errcode == 401: log.error('Password is incorrect.') else: @@ -190,3 +210,7 @@ class NZBGet(Downloader): return False return True + + def getRPC(self): + url = cleanHost(host = self.conf('host'), ssl = self.conf('ssl'), username = self.conf('username'), password = self.conf('password')) + self.rpc + return xmlrpclib.ServerProxy(url) diff --git a/couchpotato/core/downloaders/nzbvortex/__init__.py b/couchpotato/core/downloaders/nzbvortex/__init__.py index 3b95698e..1c2d699e 100644 --- a/couchpotato/core/downloaders/nzbvortex/__init__.py +++ b/couchpotato/core/downloaders/nzbvortex/__init__.py @@ -1,5 +1,6 @@ from .main import NZBVortex + def start(): return NZBVortex() @@ -22,7 +23,15 @@ config = [{ }, { 'name': 'host', - 'default': 'https://localhost:4321', + 'default': 'localhost:4321', + 'description': 'Hostname with port. Usually localhost:4321', + }, + { + 'name': 'ssl', + 'default': 1, + 'type': 'bool', + 'advanced': True, + 'description': 'Use HyperText Transfer Protocol Secure, or https', }, { 'name': 'api_key', diff --git a/couchpotato/core/downloaders/nzbvortex/main.py b/couchpotato/core/downloaders/nzbvortex/main.py index f4e233be..d1525c89 100644 --- a/couchpotato/core/downloaders/nzbvortex/main.py +++ b/couchpotato/core/downloaders/nzbvortex/main.py @@ -8,9 +8,11 @@ from uuid import uuid4 import hashlib import httplib import json +import os import socket import ssl import sys +import time import traceback import urllib2 @@ -23,44 +25,54 @@ class NZBVortex(Downloader): api_level = None session_id = None - def download(self, data = None, movie = None, filedata = None): - if not movie: movie = {} + def download(self, data = None, media = None, filedata = None): + if not media: media = {} if not data: data = {} # Send the nzb try: - nzb_filename = self.createFileName(data, filedata, movie) - self.call('nzb/add', params = {'file': (nzb_filename, filedata)}, multipart = True) + nzb_filename = self.createFileName(data, filedata, media) + self.call('nzb/add', files = {'file': (nzb_filename, filedata)}) + time.sleep(10) raw_statuses = self.call('nzb') - nzb_id = [nzb['id'] for nzb in raw_statuses.get('nzbs', []) if nzb['name'] == nzb_filename][0] + nzb_id = [nzb['id'] for nzb in raw_statuses.get('nzbs', []) if os.path.basename(nzb['nzbFileName']) == nzb_filename][0] return self.downloadReturnId(nzb_id) except: log.error('Something went wrong sending the NZB file: %s', traceback.format_exc()) return False - def getAllDownloadStatus(self): + def test(self): + try: + login_result = self.login() + except: + return False + + return login_result + + def getAllDownloadStatus(self, ids): raw_statuses = self.call('nzb') release_downloads = ReleaseDownloadList(self) for nzb in raw_statuses.get('nzbs', []): + if nzb['id'] in ids: - # Check status - status = 'busy' - if nzb['state'] == 20: - status = 'completed' - elif nzb['state'] in [21, 22, 24]: - status = 'failed' + # Check status + status = 'busy' + if nzb['state'] == 20: + status = 'completed' + elif nzb['state'] in [21, 22, 24]: + status = 'failed' - release_downloads.append({ - 'id': nzb['id'], - 'name': nzb['uiTitle'], - 'status': status, - 'original_status': nzb['state'], - 'timeleft':-1, - 'folder': sp(nzb['destinationPath']), - }) + release_downloads.append({ + 'id': nzb['id'], + 'name': nzb['uiTitle'], + 'status': status, + 'original_status': nzb['state'], + 'timeleft': -1, + 'folder': sp(nzb['destinationPath']), + }) return release_downloads @@ -98,7 +110,6 @@ class NZBVortex(Downloader): log.error('Login failed, please check you api-key') return False - def call(self, call, parameters = None, repeat = False, auth = True, *args, **kwargs): # Login first @@ -112,15 +123,14 @@ class NZBVortex(Downloader): params = tryUrlencode(parameters) - url = cleanHost(self.conf('host')) + 'api/' + call - url_opener = urllib2.build_opener(HTTPSHandler()) + url = cleanHost(self.conf('host'), ssl = self.conf('ssl')) + 'api/' + call try: - data = self.urlopen('%s?%s' % (url, params), opener = url_opener, *args, **kwargs) + data = self.urlopen('%s?%s' % (url, params), *args, **kwargs) if data: return json.loads(data) - except URLError, e: + except URLError as e: if hasattr(e, 'code') and e.code == 403: # Try login and do again if not repeat: @@ -138,12 +148,11 @@ class NZBVortex(Downloader): if not self.api_level: url = cleanHost(self.conf('host')) + 'api/app/apilevel' - url_opener = urllib2.build_opener(HTTPSHandler()) try: - data = self.urlopen(url, opener = url_opener, show_error = False) + data = self.urlopen(url, show_error = False) self.api_level = float(json.loads(data).get('apilevel')) - except URLError, e: + except URLError as e: if hasattr(e, 'code') and e.code == 403: log.error('This version of NZBVortex isn\'t supported. Please update to 2.8.6 or higher') else: @@ -173,6 +182,7 @@ class HTTPSConnection(httplib.HTTPSConnection): self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_version = ssl.PROTOCOL_TLSv1) + class HTTPSHandler(urllib2.HTTPSHandler): def https_open(self, req): return self.do_open(HTTPSConnection, req) diff --git a/couchpotato/core/downloaders/pneumatic/__init__.py b/couchpotato/core/downloaders/pneumatic/__init__.py index 96574a7a..698643fb 100644 --- a/couchpotato/core/downloaders/pneumatic/__init__.py +++ b/couchpotato/core/downloaders/pneumatic/__init__.py @@ -1,5 +1,6 @@ from .main import Pneumatic + def start(): return Pneumatic() diff --git a/couchpotato/core/downloaders/pneumatic/main.py b/couchpotato/core/downloaders/pneumatic/main.py index 643350e1..bc1f6d04 100644 --- a/couchpotato/core/downloaders/pneumatic/main.py +++ b/couchpotato/core/downloaders/pneumatic/main.py @@ -1,5 +1,6 @@ from __future__ import with_statement from couchpotato.core.downloaders.base import Downloader +from couchpotato.core.helpers.encoding import sp from couchpotato.core.logger import CPLog import os import traceback @@ -11,9 +12,10 @@ class Pneumatic(Downloader): protocol = ['nzb'] strm_syntax = 'plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb=%s&nzbname=%s' + status_support = False - def download(self, data = None, movie = None, filedata = None): - if not movie: movie = {} + def download(self, data = None, media = None, filedata = None): + if not media: media = {} if not data: data = {} directory = self.conf('directory') @@ -25,27 +27,27 @@ class Pneumatic(Downloader): log.error('No nzb available!') return False - fullPath = os.path.join(directory, self.createFileName(data, filedata, movie)) + full_path = os.path.join(directory, self.createFileName(data, filedata, media)) try: - if not os.path.isfile(fullPath): - log.info('Downloading %s to %s.', (data.get('protocol'), fullPath)) - with open(fullPath, 'wb') as f: + if not os.path.isfile(full_path): + log.info('Downloading %s to %s.', (data.get('protocol'), full_path)) + with open(full_path, 'wb') as f: f.write(filedata) - nzb_name = self.createNzbName(data, movie) + nzb_name = self.createNzbName(data, media) strm_path = os.path.join(directory, nzb_name) strm_file = open(strm_path + '.strm', 'wb') - strmContent = self.strm_syntax % (fullPath, nzb_name) + strmContent = self.strm_syntax % (full_path, nzb_name) strm_file.write(strmContent) strm_file.close() - return True + return self.downloadReturnId('') else: - log.info('File %s already exists.', fullPath) - return True + log.info('File %s already exists.', full_path) + return self.downloadReturnId('') except: log.error('Failed to download .strm: %s', traceback.format_exc()) @@ -55,3 +57,17 @@ class Pneumatic(Downloader): log.info('Failed to download file %s: %s', (data.get('name'), traceback.format_exc())) return False return False + + def test(self): + directory = self.conf('directory') + if directory and os.path.isdir(directory): + + test_file = sp(os.path.join(directory, 'couchpotato_test.txt')) + + # Check if folder is writable + self.createFile(test_file, 'This is a test file') + if os.path.isfile(test_file): + os.remove(test_file) + return True + + return False diff --git a/couchpotato/core/downloaders/rtorrent/__init__.py b/couchpotato/core/downloaders/rtorrent/__init__.py index 026a56c6..f793cad1 100755 --- a/couchpotato/core/downloaders/rtorrent/__init__.py +++ b/couchpotato/core/downloaders/rtorrent/__init__.py @@ -1,5 +1,6 @@ from .main import rTorrent + def start(): return rTorrent() @@ -20,11 +21,32 @@ config = [{ 'type': 'enabler', 'radio_group': 'torrent', }, +# @RuudBurger: How do I migrate this? +# { +# 'name': 'url', +# 'default': 'http://localhost:80/RPC2', +# 'description': 'XML-RPC Endpoint URI. Usually scgi://localhost:5000 ' +# 'or http://localhost:80/RPC2' +# }, { - 'name': 'url', - 'default': 'http://localhost:80/RPC2', - 'description': 'XML-RPC Endpoint URI. Usually scgi://localhost:5000 ' - 'or http://localhost:80/RPC2' + 'name': 'host', + 'default': 'localhost:80', + 'description': 'RPC Communication URI. Usually scgi://localhost:5000, ' + 'httprpc://localhost/rutorrent or localhost:80' + }, + { + 'name': 'ssl', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Use HyperText Transfer Protocol Secure, or https', + }, + { + 'name': 'rpc_url', + 'type': 'string', + 'default': 'RPC2', + 'advanced': True, + 'description': 'Change if your RPC mount is at a different path.', }, { 'name': 'username', @@ -58,14 +80,6 @@ config = [{ 'advanced': True, 'description': 'Also remove the leftover files.', }, - { - 'name': 'append_label', - 'label': 'Append Label', - 'default': False, - 'advanced': True, - 'type': 'bool', - 'description': 'Append label to download location. Requires you to set the download location above.', - }, { 'name': 'paused', 'type': 'bool', diff --git a/couchpotato/core/downloaders/rtorrent/main.py b/couchpotato/core/downloaders/rtorrent/main.py index d7ae589f..08d34213 100755 --- a/couchpotato/core/downloaders/rtorrent/main.py +++ b/couchpotato/core/downloaders/rtorrent/main.py @@ -1,12 +1,15 @@ +from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList +from couchpotato.core.event import fireEvent, addEvent +from couchpotato.core.helpers.encoding import sp +from couchpotato.core.helpers.variable import cleanHost, splitString +from couchpotato.core.logger import CPLog from base64 import b16encode, b32decode from bencode import bencode, bdecode -from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList -from couchpotato.core.helpers.encoding import sp -from couchpotato.core.logger import CPLog from datetime import timedelta from hashlib import sha1 from rtorrent import RTorrent from rtorrent.err import MethodError +from urlparse import urlparse import os log = CPLog(__name__) @@ -16,29 +19,75 @@ class rTorrent(Downloader): protocol = ['torrent', 'torrent_magnet'] rt = None + error_msg = '' - def connect(self): + # Migration url to host options + def __init__(self): + super(rTorrent, self).__init__() + + addEvent('app.load', self.migrate) + addEvent('setting.save.rtorrent.*.after', self.settingsChanged) + + def migrate(self): + + url = self.conf('url') + if url: + host_split = splitString(url.split('://')[-1], split_on = '/') + + self.conf('ssl', value = url.startswith('https')) + self.conf('host', value = host_split[0].strip()) + self.conf('rpc_url', value = '/'.join(host_split[1:])) + + self.deleteConf('url') + + def settingsChanged(self): + # Reset active connection if settings have changed + if self.rt: + log.debug('Settings have changed, closing active connection') + + self.rt = None + return True + + def connect(self, reconnect = False): # Already connected? - if self.rt is not None: + if not reconnect and self.rt is not None: return self.rt - # Ensure url is set - if not self.conf('url'): - log.error('Config properties are not filled in correctly, url is missing.') - return False + url = cleanHost(self.conf('host'), protocol = True, ssl = self.conf('ssl')) + parsed = urlparse(url) + + # rpc_url is only used on http/https scgi pass-through + if parsed.scheme in ['http', 'https']: + url += self.conf('rpc_url') if self.conf('username') and self.conf('password'): self.rt = RTorrent( - self.conf('url'), + url, self.conf('username'), self.conf('password') ) else: - self.rt = RTorrent(self.conf('url')) + self.rt = RTorrent(url) + + self.error_msg = '' + try: + self.rt._verify_conn() + except AssertionError as e: + self.error_msg = e.message + self.rt = None return self.rt - def _update_provider_group(self, name, data): + def test(self): + if self.connect(True): + return True + + if self.error_msg: + return False, 'Connection failed: ' + self.error_msg + + return False + + def updateProviderGroup(self, name, data): if data.get('seed_time'): log.info('seeding time ignored, not supported') @@ -70,28 +119,30 @@ class rTorrent(Downloader): # Reset group action and disable it group.set_command() group.disable() - except MethodError, err: + except MethodError as err: log.error('Unable to set group options: %s', err.msg) return False return True - def download(self, data, movie, filedata = None): + def download(self, data = None, media = None, filedata = None): + if not media: media = {} + if not data: data = {} + log.debug('Sending "%s" to rTorrent.', (data.get('name'))) if not self.connect(): return False group_name = 'cp_' + data.get('provider').lower() - if not self._update_provider_group(group_name, data): + if not self.updateProviderGroup(group_name, data): return False torrent_params = {} if self.conf('label'): torrent_params['label'] = self.conf('label') - if not filedata and data.get('protocol') == 'torrent': log.error('Failed sending torrent, no data') return False @@ -115,7 +166,7 @@ class rTorrent(Downloader): # Send request to rTorrent try: # Send torrent to rTorrent - torrent = self.rt.load_torrent(filedata) + torrent = self.rt.load_torrent(filedata, verify_retries=10) if not torrent: log.error('Unable to find the torrent, did it fail to load?') @@ -125,9 +176,7 @@ class rTorrent(Downloader): if self.conf('label'): torrent.set_custom(1, self.conf('label')) - if self.conf('directory') and self.conf('append_label'): - torrent.set_directory(os.path.join(self.conf('directory'), self.conf('label'))) - elif self.conf('directory'): + if self.conf('directory'): torrent.set_directory(self.conf('directory')) # Set Ratio Group @@ -138,15 +187,30 @@ class rTorrent(Downloader): torrent.start() return self.downloadReturnId(torrent_hash) - except Exception, err: + except Exception as err: log.error('Failed to send torrent to rTorrent: %s', err) return False - def getAllDownloadStatus(self): + def getTorrentStatus(self, torrent): + if torrent.hashing or torrent.hash_checking or torrent.message: + return 'busy' + + if not torrent.complete: + return 'busy' + + if not torrent.open: + return 'completed' + + if torrent.state and torrent.active: + return 'seeding' + + return 'busy' + + def getAllDownloadStatus(self, ids): log.debug('Checking rTorrent download status.') if not self.connect(): - return False + return [] try: torrents = self.rt.get_torrents() @@ -154,33 +218,34 @@ class rTorrent(Downloader): release_downloads = ReleaseDownloadList(self) for torrent in torrents: - torrent_files = [] - for file_item in torrent.get_files(): - torrent_files.append(sp(os.path.join(torrent.directory, file_item.path))) + if torrent.info_hash in ids: + torrent_directory = os.path.normpath(torrent.directory) + torrent_files = [] - status = 'busy' - if torrent.complete: - if torrent.active: - status = 'seeding' - else: - status = 'completed' + for file in torrent.get_files(): + if not os.path.normpath(file.path).startswith(torrent_directory): + file_path = os.path.join(torrent_directory, file.path.lstrip('/')) + else: + file_path = file.path - release_downloads.append({ - 'id': torrent.info_hash, - 'name': torrent.name, - 'status': status, - 'seed_ratio': torrent.ratio, - 'original_status': torrent.state, - 'timeleft': str(timedelta(seconds = float(torrent.left_bytes) / torrent.down_rate)) if torrent.down_rate > 0 else -1, - 'folder': sp(torrent.directory), - 'files': '|'.join(torrent_files) - }) + torrent_files.append(sp(file_path)) + + release_downloads.append({ + 'id': torrent.info_hash, + 'name': torrent.name, + 'status': self.getTorrentStatus(torrent), + 'seed_ratio': torrent.ratio, + 'original_status': torrent.state, + 'timeleft': str(timedelta(seconds = float(torrent.left_bytes) / torrent.down_rate)) if torrent.down_rate > 0 else -1, + 'folder': sp(torrent.directory), + 'files': '|'.join(torrent_files) + }) return release_downloads - except Exception, err: + except Exception as err: log.error('Failed to get status from rTorrent: %s', err) - return False + return [] def pause(self, release_download, pause = True): if not self.connect(): diff --git a/couchpotato/core/downloaders/sabnzbd/__init__.py b/couchpotato/core/downloaders/sabnzbd/__init__.py index 48692dae..2990078a 100644 --- a/couchpotato/core/downloaders/sabnzbd/__init__.py +++ b/couchpotato/core/downloaders/sabnzbd/__init__.py @@ -1,5 +1,6 @@ from .main import Sabnzbd + def start(): return Sabnzbd() @@ -24,6 +25,13 @@ config = [{ 'name': 'host', 'default': 'localhost:8080', }, + { + 'name': 'ssl', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Use HyperText Transfer Protocol Secure, or https', + }, { 'name': 'api_key', 'label': 'Api Key', diff --git a/couchpotato/core/downloaders/sabnzbd/main.py b/couchpotato/core/downloaders/sabnzbd/main.py index aba21231..ba58c090 100644 --- a/couchpotato/core/downloaders/sabnzbd/main.py +++ b/couchpotato/core/downloaders/sabnzbd/main.py @@ -16,8 +16,8 @@ class Sabnzbd(Downloader): protocol = ['nzb'] - def download(self, data = None, movie = None, filedata = None): - if not movie: movie = {} + def download(self, data = None, media = None, filedata = None): + if not media: media = {} if not data: data = {} log.info('Sending "%s" to SABnzbd.', data.get('name')) @@ -25,7 +25,7 @@ class Sabnzbd(Downloader): req_params = { 'cat': self.conf('category'), 'mode': 'addurl', - 'nzbname': self.createNzbName(data, movie), + 'nzbname': self.createNzbName(data, media), 'priority': self.conf('priority'), } @@ -36,14 +36,14 @@ class Sabnzbd(Downloader): return False # If it's a .rar, it adds the .rar extension, otherwise it stays .nzb - nzb_filename = self.createFileName(data, filedata, movie) + nzb_filename = self.createFileName(data, filedata, media) req_params['mode'] = 'addfile' else: req_params['name'] = data.get('url') try: if nzb_filename and req_params.get('mode') is 'addfile': - sab_data = self.call(req_params, params = {'nzbfile': (ss(nzb_filename), filedata)}, multipart = True) + sab_data = self.call(req_params, files = {'nzbfile': (ss(nzb_filename), filedata)}) else: sab_data = self.call(req_params) except URLError: @@ -64,7 +64,27 @@ class Sabnzbd(Downloader): log.error('Error getting data from SABNZBd: %s', sab_data) return False - def getAllDownloadStatus(self): + def test(self): + try: + sab_data = self.call({ + 'mode': 'version', + }) + v = sab_data.split('.') + if int(v[0]) == 0 and int(v[1]) < 7: + return False, 'Your Sabnzbd client is too old, please update to newest version.' + + # the version check will work even with wrong api key, so we need the next check as well + sab_data = self.call({ + 'mode': 'qstatus', + }) + if not sab_data: + return False + except: + return False + + return True + + def getAllDownloadStatus(self, ids): log.debug('Checking SABnzbd download status.') @@ -75,7 +95,7 @@ class Sabnzbd(Downloader): }) except: log.error('Failed getting queue: %s', traceback.format_exc(1)) - return False + return [] # Go through history items try: @@ -85,41 +105,42 @@ class Sabnzbd(Downloader): }) except: log.error('Failed getting history json: %s', traceback.format_exc(1)) - return False + return [] release_downloads = ReleaseDownloadList(self) # Get busy releases for nzb in queue.get('slots', []): - status = 'busy' - if 'ENCRYPTED / ' in nzb['filename']: - status = 'failed' + if nzb['nzo_id'] in ids: + status = 'busy' + if 'ENCRYPTED / ' in nzb['filename']: + status = 'failed' - release_downloads.append({ - 'id': nzb['nzo_id'], - 'name': nzb['filename'], - 'status': status, - 'original_status': nzb['status'], - 'timeleft': nzb['timeleft'] if not queue['paused'] else -1, - }) + release_downloads.append({ + 'id': nzb['nzo_id'], + 'name': nzb['filename'], + 'status': status, + 'original_status': nzb['status'], + 'timeleft': nzb['timeleft'] if not queue['paused'] else -1, + }) # Get old releases for nzb in history.get('slots', []): + if nzb['nzo_id'] in ids: + status = 'busy' + if nzb['status'] == 'Failed' or (nzb['status'] == 'Completed' and nzb['fail_message'].strip()): + status = 'failed' + elif nzb['status'] == 'Completed': + status = 'completed' - status = 'busy' - if nzb['status'] == 'Failed' or (nzb['status'] == 'Completed' and nzb['fail_message'].strip()): - status = 'failed' - elif nzb['status'] == 'Completed': - status = 'completed' - - release_downloads.append({ - 'id': nzb['nzo_id'], - 'name': nzb['name'], - 'status': status, - 'original_status': nzb['status'], - 'timeleft': str(timedelta(seconds = 0)), - 'folder': sp(os.path.dirname(nzb['storage']) if os.path.isfile(nzb['storage']) else nzb['storage']), - }) + release_downloads.append({ + 'id': nzb['nzo_id'], + 'name': nzb['name'], + 'status': status, + 'original_status': nzb['status'], + 'timeleft': str(timedelta(seconds = 0)), + 'folder': sp(os.path.dirname(nzb['storage']) if os.path.isfile(nzb['storage']) else nzb['storage']), + }) return release_downloads @@ -164,9 +185,9 @@ class Sabnzbd(Downloader): def call(self, request_params, use_json = True, **kwargs): - url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(request_params, { - 'apikey': self.conf('api_key'), - 'output': 'json' + url = cleanHost(self.conf('host'), ssl = self.conf('ssl')) + 'api?' + tryUrlencode(mergeDicts(request_params, { + 'apikey': self.conf('api_key'), + 'output': 'json' })) data = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()}, **kwargs) diff --git a/couchpotato/core/downloaders/synology/__init__.py b/couchpotato/core/downloaders/synology/__init__.py index 8be16f61..d0c57c2f 100644 --- a/couchpotato/core/downloaders/synology/__init__.py +++ b/couchpotato/core/downloaders/synology/__init__.py @@ -1,5 +1,6 @@ from .main import Synology + def start(): return Synology() diff --git a/couchpotato/core/downloaders/synology/main.py b/couchpotato/core/downloaders/synology/main.py index 0721085c..7e5b6098 100644 --- a/couchpotato/core/downloaders/synology/main.py +++ b/couchpotato/core/downloaders/synology/main.py @@ -1,5 +1,6 @@ from couchpotato.core.downloaders.base import Downloader from couchpotato.core.helpers.encoding import isInt +from couchpotato.core.helpers.variable import cleanHost from couchpotato.core.logger import CPLog import json import requests @@ -11,17 +12,17 @@ log = CPLog(__name__) class Synology(Downloader): protocol = ['nzb', 'torrent', 'torrent_magnet'] - log = CPLog(__name__) + status_support = False - def download(self, data = None, movie = None, filedata = None): - if not movie: movie = {} + def download(self, data = None, media = None, filedata = None): + if not media: media = {} if not data: data = {} response = False log.error('Sending "%s" (%s) to Synology.', (data['name'], data['protocol'])) # Load host from config and split out port. - host = self.conf('host').split(':') + 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 @@ -42,7 +43,17 @@ class Synology(Downloader): except: log.error('Exception while adding torrent: %s', traceback.format_exc()) finally: - return response + return self.downloadReturnId('') if response else False + + def test(self): + host = cleanHost(self.conf('host'), protocol = False).split(':') + try: + srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password')) + test_result = srpc.test() + except: + return False + + return test_result def getEnabledProtocol(self): if self.conf('use_for') == 'both': @@ -64,6 +75,7 @@ class Synology(Downloader): return super(Synology, self).isEnabled(manual, data) and\ ((self.conf('use_for') in for_protocol)) + class SynologyRPC(object): """SynologyRPC lite library""" @@ -106,11 +118,11 @@ class SynologyRPC(object): if response['success']: log.info('Synology action successfull') return response - except requests.ConnectionError, err: + except requests.ConnectionError as err: log.error('Synology connection error, check your config %s', err) - except requests.HTTPError, err: + except requests.HTTPError as err: log.error('SynologyRPC HTTPError: %s', err) - except Exception, err: + except Exception as err: log.error('Exception: %s', err) finally: return response @@ -145,3 +157,6 @@ class SynologyRPC(object): self._logout() return result + + def test(self): + return bool(self._login()) diff --git a/couchpotato/core/downloaders/transmission/__init__.py b/couchpotato/core/downloaders/transmission/__init__.py index f96e628e..4c9b4aad 100644 --- a/couchpotato/core/downloaders/transmission/__init__.py +++ b/couchpotato/core/downloaders/transmission/__init__.py @@ -1,5 +1,6 @@ from .main import Transmission + def start(): return Transmission() diff --git a/couchpotato/core/downloaders/transmission/main.py b/couchpotato/core/downloaders/transmission/main.py index 2eabb2e8..4c42bf0f 100644 --- a/couchpotato/core/downloaders/transmission/main.py +++ b/couchpotato/core/downloaders/transmission/main.py @@ -1,7 +1,7 @@ from base64 import b64encode from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList from couchpotato.core.helpers.encoding import isInt, sp -from couchpotato.core.helpers.variable import tryInt, tryFloat +from couchpotato.core.helpers.variable import tryInt, tryFloat, cleanHost from couchpotato.core.logger import CPLog from datetime import timedelta import httplib @@ -19,19 +19,21 @@ class Transmission(Downloader): log = CPLog(__name__) trpc = None - def connect(self): + def connect(self, reconnect = False): # Load host from config and split out port. - host = self.conf('host').split(':') + 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.trpc: - self.trpc = TransmissionRPC(host[0], port = host[1], rpc_url = self.conf('rpc_url'), username = self.conf('username'), password = self.conf('password')) + if not self.trpc or reconnect: + self.trpc = TransmissionRPC(host[0], port = host[1], rpc_url = self.conf('rpc_url').strip('/ '), username = self.conf('username'), password = self.conf('password')) return self.trpc - def download(self, data, movie, filedata = None): + def download(self, data = None, media = None, filedata = None): + if not media: media = {} + if not data: data = {} log.info('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('protocol'))) @@ -81,12 +83,17 @@ class Transmission(Downloader): log.info('Torrent sent to Transmission successfully.') return self.downloadReturnId(remote_torrent['torrent-added']['hashString']) - def getAllDownloadStatus(self): + def test(self): + if self.connect(True) and self.trpc.get_session(): + return True + return False + + def getAllDownloadStatus(self, ids): log.debug('Checking Transmission download status.') if not self.connect(): - return False + return [] release_downloads = ReleaseDownloadList(self) @@ -94,37 +101,44 @@ class Transmission(Downloader): 'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isStalled', 'isFinished', 'downloadDir', 'uploadRatio', 'secondsSeeding', 'seedIdleLimit', 'files'] } + session = self.trpc.get_session() queue = self.trpc.get_alltorrents(return_params) if not (queue and queue.get('torrents')): log.debug('Nothing in queue or error') - return False + return [] for torrent in queue['torrents']: - log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / eta=%s / uploadRatio=%s / isFinished=%s', - (torrent['name'], torrent['id'], torrent['downloadDir'], torrent['hashString'], torrent['percentDone'], torrent['status'], torrent['eta'], torrent['uploadRatio'], torrent['isFinished'])) + if torrent['hashString'] in ids: + log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / isStalled=%s / eta=%s / uploadRatio=%s / isFinished=%s / incomplete-dir-enabled=%s / incomplete-dir=%s', + (torrent['name'], torrent['id'], torrent['downloadDir'], torrent['hashString'], torrent['percentDone'], torrent['status'], torrent.get('isStalled', 'N/A'), torrent['eta'], torrent['uploadRatio'], torrent['isFinished'], session['incomplete-dir-enabled'], session['incomplete-dir'])) - torrent_files = [] - for file_item in torrent['files']: - torrent_files.append(sp(os.path.join(torrent['downloadDir'], file_item['name']))) + status = 'busy' + if torrent.get('isStalled') and not torrent['percentDone'] == 1 and self.conf('stalled_as_failed'): + status = 'failed' + elif torrent['status'] == 0 and torrent['percentDone'] == 1: + status = 'completed' + elif torrent['status'] in [5, 6]: + status = 'seeding' - status = 'busy' - if torrent.get('isStalled') and self.conf('stalled_as_failed'): - status = 'failed' - elif torrent['status'] == 0 and torrent['percentDone'] == 1: - status = 'completed' - elif torrent['status'] in [5, 6]: - status = 'seeding' + if session['incomplete-dir-enabled'] and status == 'busy': + torrent_folder = session['incomplete-dir'] + else: + torrent_folder = torrent['downloadDir'] - release_downloads.append({ - 'id': torrent['hashString'], - 'name': torrent['name'], - 'status': status, - 'original_status': torrent['status'], - 'seed_ratio': torrent['uploadRatio'], - 'timeleft': str(timedelta(seconds = torrent['eta'])), - 'folder': sp(torrent['downloadDir'] if len(torrent_files) == 1 else os.path.join(torrent['downloadDir'], torrent['name'])), - 'files': '|'.join(torrent_files) - }) + torrent_files = [] + for file_item in torrent['files']: + torrent_files.append(sp(os.path.join(torrent_folder, file_item['name']))) + + release_downloads.append({ + 'id': torrent['hashString'], + 'name': torrent['name'], + 'status': status, + 'original_status': torrent['status'], + 'seed_ratio': torrent['uploadRatio'], + 'timeleft': str(timedelta(seconds = torrent['eta'])), + 'folder': sp(torrent_folder if len(torrent_files) == 1 else os.path.join(torrent_folder, torrent['name'])), + 'files': '|'.join(torrent_files) + }) return release_downloads @@ -178,10 +192,10 @@ class TransmissionRPC(object): else: log.debug('Unknown failure sending command to Transmission. Return text is: %s', response['result']) return False - except httplib.InvalidURL, err: + except httplib.InvalidURL as err: log.error('Invalid Transmission host, check your config %s', err) return False - except urllib2.HTTPError, err: + except urllib2.HTTPError as err: if err.code == 401: log.error('Invalid Transmission Username or Password, check your config') return False @@ -199,7 +213,7 @@ class TransmissionRPC(object): log.error('Unable to get Transmission Session-Id %s', err) else: log.error('TransmissionRPC HTTPError: %s', err) - except urllib2.URLError, err: + except urllib2.URLError as err: log.error('Unable to connect to Transmission %s', err) def get_session(self): diff --git a/couchpotato/core/downloaders/utorrent/__init__.py b/couchpotato/core/downloaders/utorrent/__init__.py index d45e2e6c..da160956 100644 --- a/couchpotato/core/downloaders/utorrent/__init__.py +++ b/couchpotato/core/downloaders/utorrent/__init__.py @@ -1,5 +1,6 @@ from .main import uTorrent + def start(): return uTorrent() @@ -23,7 +24,7 @@ config = [{ { 'name': 'host', 'default': 'localhost:8000', - 'description': 'Hostname with port. Usually localhost:8000', + 'description': 'Port can be found in settings when enabling WebUI.', }, { 'name': 'username', diff --git a/couchpotato/core/downloaders/utorrent/main.py b/couchpotato/core/downloaders/utorrent/main.py index 1db1b8a3..6a5e4257 100644 --- a/couchpotato/core/downloaders/utorrent/main.py +++ b/couchpotato/core/downloaders/utorrent/main.py @@ -2,7 +2,7 @@ from base64 import b16encode, b32decode from bencode import bencode as benc, bdecode from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList from couchpotato.core.helpers.encoding import isInt, ss, sp -from couchpotato.core.helpers.variable import tryInt, tryFloat +from couchpotato.core.helpers.variable import tryInt, tryFloat, cleanHost from couchpotato.core.logger import CPLog from datetime import timedelta from hashlib import sha1 @@ -24,10 +24,20 @@ class uTorrent(Downloader): protocol = ['torrent', 'torrent_magnet'] utorrent_api = None + status_flags = { + 'STARTED' : 1, + 'CHECKING' : 2, + 'CHECK-START' : 4, + 'CHECKED' : 8, + 'ERROR' : 16, + 'PAUSED' : 32, + 'QUEUED' : 64, + 'LOADED' : 128 + } def connect(self): # Load host from config and split out port. - host = self.conf('host').split(':') + 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 @@ -36,11 +46,11 @@ class uTorrent(Downloader): return self.utorrent_api - def download(self, data = None, movie = None, filedata = None): - if not movie: movie = {} + def download(self, data = None, media = None, filedata = None): + if not media: media = {} if not data: data = {} - log.debug('Sending "%s" (%s) to uTorrent.', (data.get('name'), data.get('protocol'))) + log.debug("Sending '%s' (%s) to uTorrent.", (data.get('name'), data.get('protocol'))) if not self.connect(): return False @@ -56,7 +66,7 @@ class uTorrent(Downloader): new_settings['seed_prio_limitul_flag'] = True log.info('Updated uTorrent settings to set a torrent to complete after it the seeding requirements are met.') - if settings.get('bt.read_only_on_complete'): #This doesn't work as this option seems to be not available through the api. Mitigated with removeReadOnly function + if settings.get('bt.read_only_on_complete'): #This doesn't work as this option seems to be not available through the api. Mitigated with removeReadOnly function new_settings['bt.read_only_on_complete'] = False log.info('Updated uTorrent settings to not set the files to read only after completing.') @@ -75,9 +85,10 @@ class uTorrent(Downloader): torrent_hash = re.findall('urn:btih:([\w]{32,40})', data.get('url'))[0].upper() torrent_params['trackers'] = '%0D%0A%0D%0A'.join(self.torrent_trackers) else: - info = bdecode(filedata)["info"] + info = bdecode(filedata)['info'] torrent_hash = sha1(benc(info)).hexdigest().upper() - torrent_filename = self.createFileName(data, filedata, movie) + + torrent_filename = self.createFileName(data, filedata, media) if data.get('seed_ratio'): torrent_params['seed_override'] = 1 @@ -104,72 +115,73 @@ class uTorrent(Downloader): return self.downloadReturnId(torrent_hash) - def getAllDownloadStatus(self): + def test(self): + if self.connect(): + build_version = self.utorrent_api.get_build() + if not build_version: + return False + if build_version < 25406: # This build corresponds to version 3.0.0 stable + return False, 'Your uTorrent client is too old, please update to newest version.' + return True + + return False + + def getAllDownloadStatus(self, ids): log.debug('Checking uTorrent download status.') if not self.connect(): - return False + return [] release_downloads = ReleaseDownloadList(self) data = self.utorrent_api.get_status() if not data: log.error('Error getting data from uTorrent') - return False + return [] queue = json.loads(data) if queue.get('error'): log.error('Error getting data from uTorrent: %s', queue.get('error')) - return False + return [] if not queue.get('torrents'): log.debug('Nothing in queue') - return False + return [] # Get torrents for torrent in queue['torrents']: + if torrent[0] in ids: - #Get files of the torrent - torrent_files = [] - try: - torrent_files = json.loads(self.utorrent_api.get_files(torrent[0])) - torrent_files = [sp(os.path.join(torrent[26], torrent_file[0])) for torrent_file in torrent_files['files'][1]] - except: - log.debug('Failed getting files from torrent: %s', torrent[2]) + #Get files of the torrent + torrent_files = [] + try: + torrent_files = json.loads(self.utorrent_api.get_files(torrent[0])) + torrent_files = [sp(os.path.join(torrent[26], torrent_file[0])) for torrent_file in torrent_files['files'][1]] + except: + log.debug('Failed getting files from torrent: %s', torrent[2]) - status_flags = { - "STARTED" : 1, - "CHECKING" : 2, - "CHECK-START" : 4, - "CHECKED" : 8, - "ERROR" : 16, - "PAUSED" : 32, - "QUEUED" : 64, - "LOADED" : 128 - } + status = 'busy' + if (torrent[1] & self.status_flags['STARTED'] or torrent[1] & self.status_flags['QUEUED']) and torrent[4] == 1000: + status = 'seeding' + elif (torrent[1] & self.status_flags['ERROR']): + status = 'failed' + elif torrent[4] == 1000: + status = 'completed' - status = 'busy' - if (torrent[1] & status_flags["STARTED"] or torrent[1] & status_flags["QUEUED"]) and torrent[4] == 1000: - status = 'seeding' - elif (torrent[1] & status_flags["ERROR"]): - status = 'failed' - elif torrent[4] == 1000: - status = 'completed' + if not status == 'busy': + self.removeReadOnly(torrent_files) - if not status == 'busy': - self.removeReadOnly(torrent_files) - - release_downloads.append({ - 'id': torrent[0], - 'name': torrent[2], - 'status': status, - 'seed_ratio': float(torrent[7]) / 1000, - 'original_status': torrent[1], - 'timeleft': str(timedelta(seconds = torrent[10])), - 'folder': sp(torrent[26]), - 'files': '|'.join(torrent_files) - }) + release_downloads.append({ + 'id': torrent[0], + 'name': torrent[2], + 'status': status, + 'seed_ratio': float(torrent[7]) / 1000, + 'original_status': torrent[1], + 'timeleft': str(timedelta(seconds = torrent[10])), + 'folder': sp(torrent[26]), + 'files': '|'.join(torrent_files) + }) return release_downloads @@ -222,7 +234,7 @@ class uTorrentAPI(object): if time.time() > self.last_time + 1800: self.last_time = time.time() self.token = self.get_token() - request = urllib2.Request(self.url + "?token=" + self.token + "&" + action, data) + request = urllib2.Request(self.url + '?token=' + self.token + '&' + action, data) try: open_request = self.opener.open(request) response = open_request.read() @@ -230,64 +242,64 @@ class uTorrentAPI(object): return response else: log.debug('Unknown failure sending command to uTorrent. Return text is: %s', response) - except httplib.InvalidURL, err: + except httplib.InvalidURL as err: log.error('Invalid uTorrent host, check your config %s', err) - except urllib2.HTTPError, err: + except urllib2.HTTPError as err: if err.code == 401: log.error('Invalid uTorrent Username or Password, check your config') else: log.error('uTorrent HTTPError: %s', err) - except urllib2.URLError, err: + except urllib2.URLError as err: log.error('Unable to connect to uTorrent %s', err) return False def get_token(self): - request = self.opener.open(self.url + "token.html") - token = re.findall("