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': {