diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index 1be9dccc..d5f1c422 100644 --- a/couchpotato/core/downloaders/base.py +++ b/couchpotato/core/downloaders/base.py @@ -13,16 +13,17 @@ class Downloader(Plugin): def __init__(self): addEvent('download', self.download) - addEvent('getdownloadstatus', self.getdownloadstatus) + addEvent('download.status', self.getDownloadStatus) def download(self, data = {}, movie = {}, manual = False, filedata = None): pass - def getdownloadstatus(self, data = {}, movie = {}): + def getDownloadStatus(self, data = {}, movie = {}): pass def createNzbName(self, data, movie): - return '%s%s' % (toSafeString(data.get('name')[:40]), self.cpTag(movie)) + tag = self.cpTag(movie) + return '%s%s' % (toSafeString(data.get('name')[:127 - len(tag)]), tag) 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 1baaea0d..927a9ff2 100644 --- a/couchpotato/core/downloaders/sabnzbd/__init__.py +++ b/couchpotato/core/downloaders/sabnzbd/__init__.py @@ -41,11 +41,10 @@ config = [{ 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', }, { - 'name': 'delete failed', + 'name': 'delete_failed', 'default': True, 'type': 'bool', - 'advanced': True, - 'description': 'Delete a release after it\'s download failed.', + 'description': 'Delete a release after the download has failed.', }, ], } diff --git a/couchpotato/core/downloaders/sabnzbd/main.py b/couchpotato/core/downloaders/sabnzbd/main.py index 4144b279..e174259d 100644 --- a/couchpotato/core/downloaders/sabnzbd/main.py +++ b/couchpotato/core/downloaders/sabnzbd/main.py @@ -44,7 +44,7 @@ class Sabnzbd(Downloader): else: sab = self.urlopen(url, timeout = 60, show_error = False) except: - log.error(traceback.format_exc()) + log.error('Failed sending release: %s', traceback.format_exc()) return False result = sab.strip() @@ -63,7 +63,7 @@ class Sabnzbd(Downloader): log.error("Unknown error: " + result[:40]) return False - def getdownloadstatus(self, data = {}, movie = {}): + def getDownloadStatus(self, data = {}, movie = {}): if self.isDisabled(manual = True) or not self.isCorrectType(data.get('type')): return @@ -81,20 +81,21 @@ class Sabnzbd(Downloader): try: sab = self.urlopen(url, timeout = 60, show_error = False) except: - log.error(traceback.format_exc()) - return + log.error('Failed checking status: %s', traceback.format_exc()) + return False + try: history = json.loads(sab) except: log.debug("Result text from SAB: " + sab[:40]) - log.error(traceback.format_exc()) - return + log.error('Failed parsing json status: %s', traceback.format_exc()) + return False 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'] + return slot['status'].lower() # Go through history items params = { @@ -107,13 +108,14 @@ class Sabnzbd(Downloader): try: sab = self.urlopen(url, timeout = 60, show_error = False) except: - log.error(traceback.format_exc()) + log.error('Failed getting history: %s', traceback.format_exc()) return + try: history = json.loads(sab) except: log.debug("Result text from SAB: " + sab[:40]) - log.error(traceback.format_exc()) + log.error('Failed parsing history json: %s', traceback.format_exc()) return for slot in history['history']['slots']: @@ -123,7 +125,8 @@ class Sabnzbd(Downloader): if slot['status'] == 'Failed' or 'fail' in slot['fail_message'].lower(): # Delete failed download - if self.conf('delete failed', default = True): + if self.conf('delete_failed', default = True): + log.info('%s failed downloading, deleting...', slot['name']) params = { 'apikey': self.conf('api_key'), @@ -133,10 +136,11 @@ class Sabnzbd(Downloader): '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()) + log.error('Failed deleting: %s', traceback.format_exc()) return False result = sab.strip() @@ -147,12 +151,12 @@ class Sabnzbd(Downloader): if result == "ok": log.info('SabNZBd deleted failed release %s successfully.', slot['name']) elif result == "Missing authentication": - log.error("Incorrect username/password.") + log.error("Incorrect username/password or API?.") else: log.error("Unknown error: " + result[:40]) - return 'Failed' + return 'failed' else: - return slot['status'] + return slot['status'].lower() - return 'Not found' + return 'not_found' diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index c30ee99a..f809c579 100644 --- a/couchpotato/core/plugins/movie/static/movie.css +++ b/couchpotato/core/plugins/movie/static/movie.css @@ -340,6 +340,32 @@ .movies .movie .hide_trailer.hide { top: -30px; } + + .movies .movie .try_container { + padding: 5px 10px; + text-align: center; + } + + .movies .movie .try_container a { + margin: 0 5px; + padding: 2px 5px; + } + + .movies .movie .releases .next_release { + border-left: 6px solid #2aa300; + } + + .movies .movie .releases .next_release > :first-child { + margin-left: -6px; + } + + .movies .movie .releases .last_release { + border-left: 6px solid #ffa200; + } + + .movies .movie .releases .last_release > :first-child { + margin-left: -6px; + } .movies .load_more { display: block; diff --git a/couchpotato/core/plugins/movie/static/movie.js b/couchpotato/core/plugins/movie/static/movie.js index 92a66efd..2182b887 100644 --- a/couchpotato/core/plugins/movie/static/movie.js +++ b/couchpotato/core/plugins/movie/static/movie.js @@ -146,7 +146,7 @@ var Movie = new Class({ }); // Add done releases - Array.each(self.data.releases, function(release){ + self.data.releases.each(function(release){ var q = self.quality.getElement('.q_id'+ release.quality_id), status = Status.get(release.status_id); @@ -159,9 +159,9 @@ var Movie = new Class({ }); Object.each(self.options.actions, function(action, key){ - self.actions.adopt( - self.action[key.toLowerCase()] = new self.options.actions[key](self) - ) + self.action[key.toLowerCase()] = action = new self.options.actions[key](self) + if(action.el) + self.actions.adopt(action) }); if(!self.data.library.rating) @@ -280,6 +280,31 @@ var MovieAction = new Class({ this.el.removeClass('disable') }, + createMask: function(){ + var self = this; + self.mask = new Element('div.mask', { + 'styles': { + 'z-index': '1' + } + }).inject(self.movie, 'top').fade('hide'); + self.positionMask(); + }, + + positionMask: function(){ + var self = this, + movie = $(self.movie), + s = movie.getSize() + + return; + + return self.mask.setStyles({ + 'width': s.x, + 'height': s.y + }).position({ + 'relativeTo': movie + }) + }, + toElement: function(){ return this.el || null } @@ -318,13 +343,10 @@ var IMDBAction = new Class({ var ReleaseAction = new Class({ Extends: MovieAction, - id: null, create: function(){ var self = this; - self.id = self.movie.get('identifier'); - self.el = new Element('a.releases.icon.download', { 'title': 'Show the releases that are available for ' + self.movie.getTitle(), 'events': { @@ -332,15 +354,33 @@ var ReleaseAction = new Class({ } }); + var buttons_done = false; + + self.movie.data.releases.sortBy('-info.score').each(function(release){ + if(buttons_done) return; + + var status = Status.get(release.status_id); + + if((status.identifier == 'ignored' || status.identifier == 'failed') || (!self.next_release && status.identifier == 'available')){ + self.hide_on_click = false; + self.show(); + buttons_done = true; + } + + }); + }, show: function(e){ var self = this; - (e).preventDefault(); + if(e) + (e).preventDefault(); if(!self.options_container){ self.options_container = new Element('div.options').adopt( - self.release_container = new Element('div.releases.table') + self.release_container = new Element('div.releases.table').adopt( + self.trynext_container = new Element('div.buttons.try_container') + ) ).inject(self.movie, 'top'); // Header @@ -354,7 +394,7 @@ var ReleaseAction = new Class({ new Element('span.provider', {'text': 'Provider'}) ).inject(self.release_container) - Array.each(self.movie.data.releases, function(release){ + self.movie.data.releases.sortBy('-info.score').each(function(release){ var status = Status.get(release.status_id), quality = Quality.getProfile(release.quality_id) || {}, @@ -364,8 +404,18 @@ var ReleaseAction = new Class({ var details_url = info.filter(function(item){ return item.identifier == 'detail_url' }).pick().value; } catch(e){} + if( status.identifier == 'ignored' || status.identifier == 'failed'){ + self.last_release = release; + } + else if(!self.next_release && status.identifier == 'available'){ + self.next_release = release; + } + + // Create release new Element('div', { - 'class': 'item '+status.identifier, + 'class': 'item '+status.identifier + + (self.next_release && self.next_release.id == release.id ? ' next_release' : '') + + (self.last_release && self.last_release.id == release.id ? ' last_release' : ''), 'id': 'release_'+release.id }).adopt( new Element('span.name', {'text': self.get(release, 'name'), 'title': self.get(release, 'name')}), @@ -400,17 +450,37 @@ var ReleaseAction = new Class({ ).inject(self.release_container) }); + self.trynext_container.adopt( + new Element('span.or', { + 'text': 'Download' + }), + self.last_release ? new Element('a.button.orange', { + 'text': 'the same release again', + 'events': { + 'click': self.trySameRelease.bind(self) + } + }) : null, + self.next_release && self.last_release ? new Element('span.or', { + 'text': 'or' + }) : null, + self.next_release ? [new Element('a.button.green', { + 'text': self.last_release ? 'another release' : 'the best release', + 'events': { + 'click': self.tryNextRelease.bind(self) + } + }), + new Element('span.or', { + 'text': 'or pick one below' + })] : null + ) + } self.movie.slide('in', self.options_container); }, get: function(release, type){ - var self = this; - - return (release.info.filter(function(info){ - return type == info.identifier - }).pick() || {}).value || 'n/a' + return release.info[type] || 'n/a' }, download: function(release){ @@ -444,6 +514,25 @@ var ReleaseAction = new Class({ } }) + }, + + tryNextRelease: function(movie_id){ + var self = this; + + if(self.last_release) + self.ignore(self.last_release); + + if(self.next_release) + self.download(self.next_release); + + }, + + trySameRelease: function(movie_id){ + var self = this; + + if(self.last_release) + self.download(self.last_release); + } }); diff --git a/couchpotato/core/plugins/searcher/__init__.py b/couchpotato/core/plugins/searcher/__init__.py index 2d82b61c..e2c2bb83 100644 --- a/couchpotato/core/plugins/searcher/__init__.py +++ b/couchpotato/core/plugins/searcher/__init__.py @@ -49,10 +49,9 @@ config = [{ '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', + 'name': 'next_on_failed', 'default': True, 'type': 'bool', - 'advanced': True, 'description': 'Try the next best release for a movie after a download failed.', }, ], diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py index 16e3e5ad..1d966ee8 100644 --- a/couchpotato/core/plugins/searcher/main.py +++ b/couchpotato/core/plugins/searcher/main.py @@ -1,6 +1,8 @@ from couchpotato import get_session +from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import simplifyString, toUnicode +from couchpotato.core.helpers.request import jsonified, getParam from couchpotato.core.helpers.variable import md5, getImdb, getTitle from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin @@ -25,11 +27,15 @@ class Searcher(Plugin): addEvent('searcher.single', self.single) addEvent('searcher.correct_movie', self.correctMovie) addEvent('searcher.download', self.download) - addEvent('searcher.checksnatched', self.checksnatched) + addEvent('searcher.check_snatched', self.checkSnatched) + + addApiView('searcher.try_next', self.tryNextReleaseView, docs = { + 'desc': 'Try next best release', + }) # 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')) + fireEvent('schedule.interval', 'searcher.check_snatched', self.checkSnatched, minutes = self.conf('run_every')) def all_movies(self): @@ -439,14 +445,17 @@ class Searcher(Plugin): return False - def checksnatched(self): + def checkSnatched(self): snatched_status = fireEvent('status.get', 'snatched', single = True) ignored_status = fireEvent('status.get', 'ignored', single = True) + failed_status = fireEvent('status.get', 'failed', single = True) db = get_session() rels = db.query(Release).filter_by(status_id = snatched_status.get('id')) - log.info('Checking status snatched releases...') + if rels: + log.debug('Checking status snatched releases...') + scanrequired = False for rel in rels: @@ -462,57 +471,66 @@ class Searcher(Plugin): 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': {} - }) + movie_dict = fireEvent('movie.get', rel.movie_id, single = True) # check status - downloadstatus = fireEvent('getdownloadstatus', data = item, movie = movie) - if not downloadstatus: # Downloader not compatible with download status + downloadstatus = fireEvent('download.status', data = item, movie = movie_dict, single = True) + if not downloadstatus: log.debug('Download status functionality is not implemented for active downloaders.') scanrequired = True - else: - log.debug('Download status: %s' , downloadstatus[0]) + log.debug('Download status: %s' , downloadstatus) - 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) + if downloadstatus == 'failed': + if self.conf('next_on_failed'): + self.tryNextRelease(rel.movie_id) else: + rel.status_id = failed_status.get('id') + db.commit() + log.info('Download of %s failed.', item['name']) - elif downloadstatus[0] == 'Completed': + elif downloadstatus == 'completed': log.info('Download of %s completed!', item['name']) scanrequired = True - elif downloadstatus[0] == 'Not found': + elif downloadstatus == '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 + # Note that Queued, Downloading, Paused, Repair and Unpackimg are also available as status for SabNZBd if scanrequired: fireEvent('renamer.scan') - return + def tryNextReleaseView(self): + + trynext = self.tryNextRelease(getParam('id')) + + return jsonified({ + 'success': trynext + }) + + def tryNextRelease(self, movie_id, manual = False): + + snatched_status = fireEvent('status.get', 'snatched', single = True) + ignored_status = fireEvent('status.get', 'ignored', single = True) + + try: + movie_dict = fireEvent('movie.get', movie_id, single = True) + + db = get_session() + rels = db.query(Release).filter_by(status_id = snatched_status.get('id')) + + for rel in rels: + rel.status_id = ignored_status.get('id') + db.commit() + + log.info('Trying next release for', getTitle(movie_dict['library'])) + fireEvent('searcher.single', movie_dict) + + return True + + except: + log.error('Failed searching for next release: %s', traceback.format_exc()) + return False diff --git a/couchpotato/core/plugins/status/main.py b/couchpotato/core/plugins/status/main.py index af2e8792..91c2858f 100644 --- a/couchpotato/core/plugins/status/main.py +++ b/couchpotato/core/plugins/status/main.py @@ -19,6 +19,7 @@ class StatusPlugin(Plugin): 'downloaded': 'Downloaded', 'wanted': 'Wanted', 'snatched': 'Snatched', + 'failed': 'Failed', 'deleted': 'Deleted', 'ignored': 'Ignored', } diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js index c0860a99..b99e6c7e 100644 --- a/couchpotato/static/scripts/couchpotato.js +++ b/couchpotato/static/scripts/couchpotato.js @@ -419,15 +419,18 @@ function randomString(length, extra) { return 0; }; - Array.implement('sortBy', function(){ - keyPaths.empty(); - Array.each(arguments, function(argument) { - switch (typeOf(argument)) { - case "array": saveKeyPath(argument); break; - case "string": saveKeyPath(argument.match(/[+-]|[^.]+/g)); break; - } - }); - return this.sort(comparer); + Array.implement({ + sortBy: function(){ + keyPaths.empty(); + + Array.each(arguments, function(argument) { + switch (typeOf(argument)) { + case "array": saveKeyPath(argument); break; + case "string": saveKeyPath(argument.match(/[+-]|[^.]+/g)); break; + } + }); + return this.sort(comparer); + } }); })(); diff --git a/couchpotato/static/scripts/page/wanted.js b/couchpotato/static/scripts/page/wanted.js index 5f2dcf7e..2ce6c14c 100644 --- a/couchpotato/static/scripts/page/wanted.js +++ b/couchpotato/static/scripts/page/wanted.js @@ -31,7 +31,6 @@ window.addEvent('domready', function(){ 'IMDB': IMDBAction ,'Trailer': TrailerAction ,'Releases': ReleaseAction - ,'Edit': new Class({ Extends: MovieAction, @@ -170,7 +169,7 @@ window.addEvent('domready', function(){ (e).preventDefault(); if(!self.delete_container){ - self.delete_container = new Element('div.delete_container').adopt( + self.delete_container = new Element('div.buttons.delete_container').adopt( new Element('a.cancel', { 'text': 'Cancel', 'events': {