Cleanup of automated failed download detection.

This commit is contained in:
Ruud
2012-08-12 23:53:53 +02:00
parent 9b6261f0b3
commit f63e332861
10 changed files with 228 additions and 89 deletions
+4 -3
View File
@@ -13,16 +13,17 @@ class Downloader(Plugin):
def __init__(self): def __init__(self):
addEvent('download', self.download) addEvent('download', self.download)
addEvent('getdownloadstatus', self.getdownloadstatus) addEvent('download.status', self.getDownloadStatus)
def download(self, data = {}, movie = {}, manual = False, filedata = None): def download(self, data = {}, movie = {}, manual = False, filedata = None):
pass pass
def getdownloadstatus(self, data = {}, movie = {}): def getDownloadStatus(self, data = {}, movie = {}):
pass pass
def createNzbName(self, data, movie): 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): def createFileName(self, data, filedata, movie):
name = os.path.join(self.createNzbName(data, movie)) name = os.path.join(self.createNzbName(data, movie))
@@ -41,11 +41,10 @@ config = [{
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
}, },
{ {
'name': 'delete failed', 'name': 'delete_failed',
'default': True, 'default': True,
'type': 'bool', 'type': 'bool',
'advanced': True, 'description': 'Delete a release after the download has failed.',
'description': 'Delete a release after it\'s download failed.',
}, },
], ],
} }
+19 -15
View File
@@ -44,7 +44,7 @@ class Sabnzbd(Downloader):
else: else:
sab = self.urlopen(url, timeout = 60, show_error = False) sab = self.urlopen(url, timeout = 60, show_error = False)
except: except:
log.error(traceback.format_exc()) log.error('Failed sending release: %s', traceback.format_exc())
return False return False
result = sab.strip() result = sab.strip()
@@ -63,7 +63,7 @@ class Sabnzbd(Downloader):
log.error("Unknown error: " + result[:40]) log.error("Unknown error: " + result[:40])
return False return False
def getdownloadstatus(self, data = {}, movie = {}): def getDownloadStatus(self, data = {}, movie = {}):
if self.isDisabled(manual = True) or not self.isCorrectType(data.get('type')): if self.isDisabled(manual = True) or not self.isCorrectType(data.get('type')):
return return
@@ -81,20 +81,21 @@ class Sabnzbd(Downloader):
try: try:
sab = self.urlopen(url, timeout = 60, show_error = False) sab = self.urlopen(url, timeout = 60, show_error = False)
except: except:
log.error(traceback.format_exc()) log.error('Failed checking status: %s', traceback.format_exc())
return return False
try: try:
history = json.loads(sab) history = json.loads(sab)
except: except:
log.debug("Result text from SAB: " + sab[:40]) log.debug("Result text from SAB: " + sab[:40])
log.error(traceback.format_exc()) log.error('Failed parsing json status: %s', traceback.format_exc())
return return False
for slot in history['queue']['slots']: for slot in history['queue']['slots']:
if slot['cat'] == self.conf('category'): 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'])) log.debug('Found %s in SabNZBd queue, which is %s, with %s left', (slot['filename'], slot['status'], slot['timeleft']))
if slot['filename'] == nzbname: if slot['filename'] == nzbname:
return slot['status'] return slot['status'].lower()
# Go through history items # Go through history items
params = { params = {
@@ -107,13 +108,14 @@ class Sabnzbd(Downloader):
try: try:
sab = self.urlopen(url, timeout = 60, show_error = False) sab = self.urlopen(url, timeout = 60, show_error = False)
except: except:
log.error(traceback.format_exc()) log.error('Failed getting history: %s', traceback.format_exc())
return return
try: try:
history = json.loads(sab) history = json.loads(sab)
except: except:
log.debug("Result text from SAB: " + sab[:40]) log.debug("Result text from SAB: " + sab[:40])
log.error(traceback.format_exc()) log.error('Failed parsing history json: %s', traceback.format_exc())
return return
for slot in history['history']['slots']: for slot in history['history']['slots']:
@@ -123,7 +125,8 @@ class Sabnzbd(Downloader):
if slot['status'] == 'Failed' or 'fail' in slot['fail_message'].lower(): if slot['status'] == 'Failed' or 'fail' in slot['fail_message'].lower():
# Delete failed download # 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']) log.info('%s failed downloading, deleting...', slot['name'])
params = { params = {
'apikey': self.conf('api_key'), 'apikey': self.conf('api_key'),
@@ -133,10 +136,11 @@ class Sabnzbd(Downloader):
'value': slot['nzo_id'] 'value': slot['nzo_id']
} }
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params) url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
try: try:
sab = self.urlopen(url, timeout = 60, show_error = False) sab = self.urlopen(url, timeout = 60, show_error = False)
except: except:
log.error(traceback.format_exc()) log.error('Failed deleting: %s', traceback.format_exc())
return False return False
result = sab.strip() result = sab.strip()
@@ -147,12 +151,12 @@ class Sabnzbd(Downloader):
if result == "ok": if result == "ok":
log.info('SabNZBd deleted failed release %s successfully.', slot['name']) log.info('SabNZBd deleted failed release %s successfully.', slot['name'])
elif result == "Missing authentication": elif result == "Missing authentication":
log.error("Incorrect username/password.") log.error("Incorrect username/password or API?.")
else: else:
log.error("Unknown error: " + result[:40]) log.error("Unknown error: " + result[:40])
return 'Failed' return 'failed'
else: else:
return slot['status'] return slot['status'].lower()
return 'Not found' return 'not_found'
@@ -340,6 +340,32 @@
.movies .movie .hide_trailer.hide { .movies .movie .hide_trailer.hide {
top: -30px; 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 { .movies .load_more {
display: block; display: block;
+105 -16
View File
@@ -146,7 +146,7 @@ var Movie = new Class({
}); });
// Add done releases // 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), var q = self.quality.getElement('.q_id'+ release.quality_id),
status = Status.get(release.status_id); status = Status.get(release.status_id);
@@ -159,9 +159,9 @@ var Movie = new Class({
}); });
Object.each(self.options.actions, function(action, key){ Object.each(self.options.actions, function(action, key){
self.actions.adopt( self.action[key.toLowerCase()] = action = new self.options.actions[key](self)
self.action[key.toLowerCase()] = new self.options.actions[key](self) if(action.el)
) self.actions.adopt(action)
}); });
if(!self.data.library.rating) if(!self.data.library.rating)
@@ -280,6 +280,31 @@ var MovieAction = new Class({
this.el.removeClass('disable') 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(){ toElement: function(){
return this.el || null return this.el || null
} }
@@ -318,13 +343,10 @@ var IMDBAction = new Class({
var ReleaseAction = new Class({ var ReleaseAction = new Class({
Extends: MovieAction, Extends: MovieAction,
id: null,
create: function(){ create: function(){
var self = this; var self = this;
self.id = self.movie.get('identifier');
self.el = new Element('a.releases.icon.download', { self.el = new Element('a.releases.icon.download', {
'title': 'Show the releases that are available for ' + self.movie.getTitle(), 'title': 'Show the releases that are available for ' + self.movie.getTitle(),
'events': { '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){ show: function(e){
var self = this; var self = this;
(e).preventDefault(); if(e)
(e).preventDefault();
if(!self.options_container){ if(!self.options_container){
self.options_container = new Element('div.options').adopt( 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'); ).inject(self.movie, 'top');
// Header // Header
@@ -354,7 +394,7 @@ var ReleaseAction = new Class({
new Element('span.provider', {'text': 'Provider'}) new Element('span.provider', {'text': 'Provider'})
).inject(self.release_container) ).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), var status = Status.get(release.status_id),
quality = Quality.getProfile(release.quality_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; var details_url = info.filter(function(item){ return item.identifier == 'detail_url' }).pick().value;
} catch(e){} } 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', { 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 'id': 'release_'+release.id
}).adopt( }).adopt(
new Element('span.name', {'text': self.get(release, 'name'), 'title': self.get(release, 'name')}), 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) ).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); self.movie.slide('in', self.options_container);
}, },
get: function(release, type){ get: function(release, type){
var self = this; return release.info[type] || 'n/a'
return (release.info.filter(function(info){
return type == info.identifier
}).pick() || {}).value || 'n/a'
}, },
download: function(release){ 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);
} }
}); });
@@ -49,10 +49,9 @@ config = [{
'description': 'Detect movie status every X minutes. Will start the renamer if movie is <strong>completed</strong> or handle <strong>failed</strong> download if these options are enabled', 'description': 'Detect movie status every X minutes. Will start the renamer if movie is <strong>completed</strong> or handle <strong>failed</strong> download if these options are enabled',
}, },
{ {
'name': 'failed download', 'name': 'next_on_failed',
'default': True, 'default': True,
'type': 'bool', 'type': 'bool',
'advanced': True,
'description': 'Try the next best release for a movie after a download failed.', 'description': 'Try the next best release for a movie after a download failed.',
}, },
], ],
+57 -39
View File
@@ -1,6 +1,8 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import simplifyString, toUnicode 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.helpers.variable import md5, getImdb, getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
@@ -25,11 +27,15 @@ class Searcher(Plugin):
addEvent('searcher.single', self.single) addEvent('searcher.single', self.single)
addEvent('searcher.correct_movie', self.correctMovie) addEvent('searcher.correct_movie', self.correctMovie)
addEvent('searcher.download', self.download) 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 # 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.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): def all_movies(self):
@@ -439,14 +445,17 @@ class Searcher(Plugin):
return False return False
def checksnatched(self): def checkSnatched(self):
snatched_status = fireEvent('status.get', 'snatched', single = True) snatched_status = fireEvent('status.get', 'snatched', single = True)
ignored_status = fireEvent('status.get', 'ignored', single = True) ignored_status = fireEvent('status.get', 'ignored', single = True)
failed_status = fireEvent('status.get', 'failed', single = True)
db = get_session() db = get_session()
rels = db.query(Release).filter_by(status_id = snatched_status.get('id')) 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 scanrequired = False
for rel in rels: for rel in rels:
@@ -462,57 +471,66 @@ class Searcher(Plugin):
for info in rel.info: for info in rel.info:
item[info.identifier] = info.value item[info.identifier] = info.value
movie = rel.movie.to_dict({ movie_dict = fireEvent('movie.get', rel.movie_id, single = True)
'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}, 'files':{}},
'files': {}
})
# check status # check status
downloadstatus = fireEvent('getdownloadstatus', data = item, movie = movie) downloadstatus = fireEvent('download.status', data = item, movie = movie_dict, single = True)
if not downloadstatus: # Downloader not compatible with download status if not downloadstatus:
log.debug('Download status functionality is not implemented for active downloaders.') log.debug('Download status functionality is not implemented for active downloaders.')
scanrequired = True scanrequired = True
else: else:
log.debug('Download status: %s' , downloadstatus[0]) log.debug('Download status: %s' , downloadstatus)
if downloadstatus[0] == 'Failed': if downloadstatus == 'failed':
# if failed set status to ignored if self.conf('next_on_failed'):
rel.status_id = ignored_status.get('id') self.tryNextRelease(rel.movie_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: else:
rel.status_id = failed_status.get('id')
db.commit()
log.info('Download of %s failed.', item['name']) log.info('Download of %s failed.', item['name'])
elif downloadstatus[0] == 'Completed': elif downloadstatus == 'completed':
log.info('Download of %s completed!', item['name']) log.info('Download of %s completed!', item['name'])
scanrequired = True scanrequired = True
elif downloadstatus[0] == 'Not found': elif downloadstatus == 'not_found':
log.info('%s not found in downloaders', item['name']) log.info('%s not found in downloaders', item['name'])
rel.status_id = ignored_status.get('id') rel.status_id = ignored_status.get('id')
db.commit() 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: if scanrequired:
fireEvent('renamer.scan') 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
+1
View File
@@ -19,6 +19,7 @@ class StatusPlugin(Plugin):
'downloaded': 'Downloaded', 'downloaded': 'Downloaded',
'wanted': 'Wanted', 'wanted': 'Wanted',
'snatched': 'Snatched', 'snatched': 'Snatched',
'failed': 'Failed',
'deleted': 'Deleted', 'deleted': 'Deleted',
'ignored': 'Ignored', 'ignored': 'Ignored',
} }
+12 -9
View File
@@ -419,15 +419,18 @@ function randomString(length, extra) {
return 0; return 0;
}; };
Array.implement('sortBy', function(){ Array.implement({
keyPaths.empty(); sortBy: function(){
Array.each(arguments, function(argument) { keyPaths.empty();
switch (typeOf(argument)) {
case "array": saveKeyPath(argument); break; Array.each(arguments, function(argument) {
case "string": saveKeyPath(argument.match(/[+-]|[^.]+/g)); break; switch (typeOf(argument)) {
} case "array": saveKeyPath(argument); break;
}); case "string": saveKeyPath(argument.match(/[+-]|[^.]+/g)); break;
return this.sort(comparer); }
});
return this.sort(comparer);
}
}); });
})(); })();
+1 -2
View File
@@ -31,7 +31,6 @@ window.addEvent('domready', function(){
'IMDB': IMDBAction 'IMDB': IMDBAction
,'Trailer': TrailerAction ,'Trailer': TrailerAction
,'Releases': ReleaseAction ,'Releases': ReleaseAction
,'Edit': new Class({ ,'Edit': new Class({
Extends: MovieAction, Extends: MovieAction,
@@ -170,7 +169,7 @@ window.addEvent('domready', function(){
(e).preventDefault(); (e).preventDefault();
if(!self.delete_container){ 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', { new Element('a.cancel', {
'text': 'Cancel', 'text': 'Cancel',
'events': { 'events': {