Cleanup of automated failed download detection.
This commit is contained in:
@@ -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.',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,6 +19,7 @@ class StatusPlugin(Plugin):
|
||||
'downloaded': 'Downloaded',
|
||||
'wanted': 'Wanted',
|
||||
'snatched': 'Snatched',
|
||||
'failed': 'Failed',
|
||||
'deleted': 'Deleted',
|
||||
'ignored': 'Ignored',
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
@@ -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': {
|
||||
|
||||
Reference in New Issue
Block a user