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):
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))
@@ -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.',
},
],
}
+19 -15
View File
@@ -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'
@@ -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;
+105 -16
View File
@@ -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);
}
});
@@ -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',
},
{
'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.',
},
],
+57 -39
View File
@@ -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
+1
View File
@@ -19,6 +19,7 @@ class StatusPlugin(Plugin):
'downloaded': 'Downloaded',
'wanted': 'Wanted',
'snatched': 'Snatched',
'failed': 'Failed',
'deleted': 'Deleted',
'ignored': 'Ignored',
}
+12 -9
View File
@@ -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);
}
});
})();
+1 -2
View File
@@ -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': {