diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index 74e04de6..1be9dccc 100644 --- a/couchpotato/core/downloaders/base.py +++ b/couchpotato/core/downloaders/base.py @@ -7,19 +7,22 @@ import os log = CPLog(__name__) - class Downloader(Plugin): type = [] def __init__(self): addEvent('download', self.download) + addEvent('getdownloadstatus', self.getdownloadstatus) - def download(self, data = {}): + def download(self, data = {}, movie = {}, manual = False, filedata = None): + pass + + def getdownloadstatus(self, data = {}, movie = {}): pass def createNzbName(self, data, movie): - return '%s%s' % (toSafeString(data.get('name')), self.cpTag(movie)) + return '%s%s' % (toSafeString(data.get('name')[:40]), self.cpTag(movie)) def createFileName(self, data, filedata, movie): name = os.path.join(self.createNzbName(data, movie)) diff --git a/couchpotato/core/downloaders/sabnzbd/__init__.py b/couchpotato/core/downloaders/sabnzbd/__init__.py index ac0ce05c..1baaea0d 100644 --- a/couchpotato/core/downloaders/sabnzbd/__init__.py +++ b/couchpotato/core/downloaders/sabnzbd/__init__.py @@ -35,11 +35,18 @@ config = [{ }, { 'name': 'manual', - 'default': 0, + 'default': False, 'type': 'bool', 'advanced': True, 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', }, + { + 'name': 'delete failed', + 'default': True, + 'type': 'bool', + 'advanced': True, + 'description': 'Delete a release after it\'s download failed.', + }, ], } ], diff --git a/couchpotato/core/downloaders/sabnzbd/main.py b/couchpotato/core/downloaders/sabnzbd/main.py index 64b24509..4144b279 100644 --- a/couchpotato/core/downloaders/sabnzbd/main.py +++ b/couchpotato/core/downloaders/sabnzbd/main.py @@ -3,6 +3,7 @@ from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.variable import cleanHost from couchpotato.core.logger import CPLog import traceback +import json log = CPLog(__name__) @@ -39,14 +40,14 @@ class Sabnzbd(Downloader): try: if params.get('mode') is 'addfile': - data = self.urlopen(url, timeout = 60, params = {"nzbfile": (nzb_filename, filedata)}, multipart = True, show_error = False) + sab = self.urlopen(url, timeout = 60, params = {"nzbfile": (nzb_filename, filedata)}, multipart = True, show_error = False) else: - data = self.urlopen(url, timeout = 60, show_error = False) + sab = self.urlopen(url, timeout = 60, show_error = False) except: log.error(traceback.format_exc()) return False - result = data.strip() + result = sab.strip() if not result: log.error("SABnzbd didn't return anything.") return False @@ -61,3 +62,97 @@ class Sabnzbd(Downloader): else: log.error("Unknown error: " + result[:40]) return False + + def getdownloadstatus(self, data = {}, movie = {}): + if self.isDisabled(manual = True) or not self.isCorrectType(data.get('type')): + return + + nzbname = self.createNzbName(data, movie) + log.info('Checking download status of "%s" at SABnzbd.', nzbname) + + # Go through Queue + params = { + 'apikey': self.conf('api_key'), + 'mode': 'queue', + 'output': 'json' + } + url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params) + + try: + sab = self.urlopen(url, timeout = 60, show_error = False) + except: + log.error(traceback.format_exc()) + return + try: + history = json.loads(sab) + except: + log.debug("Result text from SAB: " + sab[:40]) + log.error(traceback.format_exc()) + return + + for slot in history['queue']['slots']: + if slot['cat'] == self.conf('category'): + log.debug('Found %s in SabNZBd queue, which is %s, with %s left', (slot['filename'], slot['status'], slot['timeleft'])) + if slot['filename'] == nzbname: + return slot['status'] + + # Go through history items + params = { + 'apikey': self.conf('api_key'), + 'mode': 'history', + 'output': 'json' + } + url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params) + + try: + sab = self.urlopen(url, timeout = 60, show_error = False) + except: + log.error(traceback.format_exc()) + return + try: + history = json.loads(sab) + except: + log.debug("Result text from SAB: " + sab[:40]) + log.error(traceback.format_exc()) + return + + for slot in history['history']['slots']: + if slot['category'] == self.conf('category'): + log.debug('Found %s in SabNZBd history, which has %s', (slot['name'], slot['status'])) + if slot['name'] == nzbname: + if slot['status'] == 'Failed' or 'fail' in slot['fail_message'].lower(): + + # Delete failed download + if self.conf('delete failed', default = True): + log.info('%s failed downloading, deleting...', slot['name']) + params = { + 'apikey': self.conf('api_key'), + 'mode': 'history', + 'name': 'delete', + 'del_files': '1', + 'value': slot['nzo_id'] + } + url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params) + try: + sab = self.urlopen(url, timeout = 60, show_error = False) + except: + log.error(traceback.format_exc()) + return False + + result = sab.strip() + if not result: + log.error("SABnzbd didn't return anything.") + + log.debug("Result text from SAB: " + result[:40]) + if result == "ok": + log.info('SabNZBd deleted failed release %s successfully.', slot['name']) + elif result == "Missing authentication": + log.error("Incorrect username/password.") + else: + log.error("Unknown error: " + result[:40]) + + return 'Failed' + else: + return slot['status'] + + return 'Not found' diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py index 59439544..21076d68 100644 --- a/couchpotato/core/plugins/renamer/__init__.py +++ b/couchpotato/core/plugins/renamer/__init__.py @@ -86,15 +86,6 @@ config = [{ 'label': 'Separator', 'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.', }, - { - 'advanced': True, - 'name': 'run_every', - 'label': 'Run every', - 'default': 1, - 'type': 'int', - 'unit': 'min(s)', - 'description': 'Search for new movies inside the folder every X minutes.', - }, ], }, { 'tab': 'renamer', diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 2cd09e48..a2882eae 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -30,7 +30,7 @@ class Renamer(Plugin): addEvent('renamer.scan', self.scan) addEvent('app.load', self.scan) - fireEvent('schedule.interval', 'renamer.scan', self.scan, minutes = self.conf('run_every')) + #fireEvent('schedule.interval', 'renamer.scan', self.scan, minutes = self.conf('run_every')) def scanView(self): diff --git a/couchpotato/core/plugins/searcher/__init__.py b/couchpotato/core/plugins/searcher/__init__.py index f499e2bd..2d82b61c 100644 --- a/couchpotato/core/plugins/searcher/__init__.py +++ b/couchpotato/core/plugins/searcher/__init__.py @@ -39,6 +39,22 @@ config = [{ 'type': 'dropdown', 'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrents', 'torrent')], }, + { + 'advanced': True, + 'name': 'run_every', + 'label': 'Run every', + 'default': 1, + 'type': 'int', + 'unit': 'min(s)', + 'description': 'Detect movie status every X minutes. Will start the renamer if movie is completed or handle failed download if these options are enabled', + }, + { + 'name': 'failed download', + 'default': True, + 'type': 'bool', + 'advanced': True, + 'description': 'Try the next best release for a movie after a download failed.', + }, ], }, { 'tab': 'searcher', diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py index dfefdab2..6fc6fe4c 100644 --- a/couchpotato/core/plugins/searcher/main.py +++ b/couchpotato/core/plugins/searcher/main.py @@ -25,9 +25,12 @@ class Searcher(Plugin): addEvent('searcher.single', self.single) addEvent('searcher.correct_movie', self.correctMovie) addEvent('searcher.download', self.download) + addEvent('searcher.checksnatched', self.checksnatched) # Schedule cronjob fireEvent('schedule.cron', 'searcher.all', self.all_movies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute')) + fireEvent('schedule.interval', 'searcher.checksnatched', self.checksnatched, minutes = self.conf('run_every')) + def all_movies(self): @@ -439,3 +442,81 @@ class Searcher(Plugin): return False + + def checksnatched(self): + snatched_status = fireEvent('status.get', 'snatched', single = True) + ignored_status = fireEvent('status.get', 'ignored', single = True) + + db = get_session() + rels = db.query(Release).filter_by(status_id = snatched_status.get('id')) + + log.info('Checking status snatched releases...') + scanrequired = False + + for rel in rels: + + # Get current selected title + default_title = '' + for title in rel.movie.library.titles: + if title.default: default_title = title.title + + log.debug('Checking snatched movie: %s' , default_title) + + item = {} + for info in rel.info: + item[info.identifier] = info.value + + movie = rel.movie.to_dict({ + 'profile': {'types': {'quality': {}}}, + 'releases': {'status': {}, 'quality': {}}, + 'library': {'titles': {}, 'files':{}}, + 'files': {} + }) + + # check status + downloadstatus = fireEvent('getdownloadstatus', data = item, movie = movie) + if not downloadstatus: # Downloader not compatible with download status + log.debug('Download status functionality is not implemented for active downloaders.') + scanrequired = True + + else: + log.debug('Download status: %s' , downloadstatus[0]) + + if downloadstatus[0] == 'Failed': + # if failed set status to ignored + rel.status_id = ignored_status.get('id') + db.commit() + + # search/download again + # if downloaded manually: # this is currently not stored... + # log.info('Download of %s failed...', item['name']) + # return + + if self.conf('failed download', default = True): + + #update movie to reflect release status update + movie = rel.movie.to_dict({ + 'profile': {'types': {'quality': {}}}, + 'releases': {'status': {}, 'quality': {}}, + 'library': {'titles': {}, 'files':{}}, + 'files': {} + }) + log.info('Download of %s failed, trying next release...', item['name']) + fireEvent('searcher.single', movie) + else: + log.info('Download of %s failed.', item['name']) + + elif downloadstatus[0] == 'Completed': + log.info('Download of %s completed!', item['name']) + scanrequired = True + + elif downloadstatus[0] == 'Not found': + log.info('%s not found in downloaders', item['name']) + rel.status_id = ignored_status.get('id') + db.commit() + + # Note that Queued, Downloading, Paused, Repair and Unpackimg are also available as status for SabNZBd + if scanrequired: + fireEvent('renamer.scan') + + return