diff --git a/gui/slick/images/queued.png b/gui/slick/images/queued.png
new file mode 100644
index 00000000..4e048113
Binary files /dev/null and b/gui/slick/images/queued.png differ
diff --git a/gui/slick/interfaces/default/displayShow.tmpl b/gui/slick/interfaces/default/displayShow.tmpl
index 53099a08..f38aa998 100644
--- a/gui/slick/interfaces/default/displayShow.tmpl
+++ b/gui/slick/interfaces/default/displayShow.tmpl
@@ -411,9 +411,9 @@
#if int($epResult["season"]) != 0:
#if ( int($epResult["status"]) in $Quality.SNATCHED or int($epResult["status"]) in $Quality.DOWNLOADED ) and $sickbeard.USE_FAILED_DOWNLOADS:
-
+ " name="<%=str(epResult["season"]) +"x"+str(epResult["episode"]) %>" href="retryEpisode?show=$show.indexerid&season=$epResult["season"]&episode=$epResult["episode"]">
#else:
-
+ " name="<%=str(epResult["season"]) +"x"+str(epResult["episode"]) %>" href="searchEpisode?show=$show.indexerid&season=$epResult["season"]&episode=$epResult["episode"]">
#end if
#end if
#if $sickbeard.USE_SUBTITLES and $show.subtitles and len(set(str($epResult["subtitles"]).split(',')).intersection(set($subtitles.wantedLanguages()))) < len($subtitles.wantedLanguages()) and $epResult["location"]
diff --git a/gui/slick/js/ajaxEpSearch.js b/gui/slick/js/ajaxEpSearch.js
index 5185b81b..ffe6f6f0 100644
--- a/gui/slick/js/ajaxEpSearch.js
+++ b/gui/slick/js/ajaxEpSearch.js
@@ -1,3 +1,112 @@
+var search_status_url = sbRoot + '/getManualSearchStatus';
+$.pnotify.defaults.width = "400px";
+$.pnotify.defaults.styling = "jqueryui";
+$.pnotify.defaults.history = false;
+$.pnotify.defaults.shadow = false;
+$.pnotify.defaults.delay = 4000;
+$.pnotify.defaults.maxonscreen = 5;
+
+$.fn.manualSearches = [];
+
+function check_manual_searches() {
+ var poll_interval = 5000;
+ $.ajax({
+ url: search_status_url + '?show=' + $('#showID').val(),
+ success: function (data) {
+ if (data.episodes) {
+ poll_interval = 5000;
+ }
+ else {
+ poll_interval = 15000;
+ }
+
+ updateImages(data);
+ //cleanupManualSearches(data);
+ },
+ error: function () {
+ poll_interval = 30000;
+ },
+ type: "GET",
+ dataType: "json",
+ complete: function () {
+ setTimeout(check_manual_searches, poll_interval);
+ },
+ timeout: 15000 // timeout every 15 secs
+ });
+}
+
+
+function updateImages(data) {
+ $.each(data.episodes, function (name, ep) {
+ console.debug(ep.searchstatus);
+ // Get td element for current ep
+ var loadingImage = 'loading16_dddddd.gif';
+ var queuedImage = 'queued.png';
+ var searchImage = 'search32.png';
+ var status = null;
+ //Try to get the Element
+ el=$('a[id=' + ep.season + 'x' + ep.episode+']');
+ img=el.children('img');
+ parent=el.parent();
+ if (el) {
+ if (ep.searchstatus == 'searching') {
+ //el=$('td#' + ep.season + 'x' + ep.episode + '.search img');
+ img.attr('title','Searching');
+ img.attr('alt','searching');
+ img.attr('src',sbRoot+'/images/' + loadingImage);
+ disableLink(el);
+ // Update Status and Quality
+ var rSearchTerm = /(\w+)\s\((.+?)\)/;
+ HtmlContent = ep.searchstatus;
+
+ }
+ else if (ep.searchstatus == 'queued') {
+ //el=$('td#' + ep.season + 'x' + ep.episode + '.search img');
+ img.attr('title','Queued');
+ img.attr('alt','queued');
+ img.attr('src',sbRoot+'/images/' + queuedImage );
+ disableLink(el);
+ HtmlContent = ep.searchstatus;
+ }
+ else if (ep.searchstatus == 'finished') {
+ //el=$('td#' + ep.season + 'x' + ep.episode + '.search img');
+ img.attr('title','Searching');
+ img.attr('alt','searching');
+ img.parent().attr('class','epRetry');
+ img.attr('src',sbRoot+'/images/' + searchImage);
+ enableLink(el);
+
+ // Update Status and Quality
+ var rSearchTerm = /(\w+)\s\((.+?)\)/;
+ HtmlContent = ep.status.replace(rSearchTerm,"$1"+' '+"$2"+'');
+
+ }
+ // update the status column if it exists
+ parent.siblings('.status_column').html(HtmlContent)
+
+ }
+
+ });
+}
+
+$(document).ready(function () {
+
+ check_manual_searches();
+
+});
+
+function enableLink(el) {
+ el.on('click.disabled', false);
+ el.attr('enableClick', '1');
+ el.fadeTo("fast", 1)
+}
+
+function disableLink(el) {
+ el.off('click.disabled');
+ el.attr('enableClick', '0');
+ el.fadeTo("fast", .5)
+}
+
(function(){
$.ajaxEpSearch = {
@@ -5,6 +114,7 @@
size: 16,
colorRow: false,
loadingImage: 'loading16_dddddd.gif',
+ queuedImage: 'queued.png',
noImage: 'no16.png',
yesImage: 'yes16.png'
}
@@ -13,22 +123,42 @@
$.fn.ajaxEpSearch = function(options){
options = $.extend({}, $.ajaxEpSearch.defaults, options);
- $('.epSearch').click(function(){
- var parent = $(this).parent();
+ $('.epSearch').click(function(event){
+ event.preventDefault();
- // put the ajax spinner (for non white bg) placeholder while we wait
- parent.empty();
- parent.append($("").attr({"src": sbRoot+"/images/"+options.loadingImage, "height": options.size, "alt": "", "title": "loading"}));
+ // Check if we have disabled the click
+ if ( $(this).attr('enableClick') == '0' ) {
+ console.debug("Already queued, not downloading!");
+ return false;
+ }
+
+ if ( $(this).attr('class') == "epRetry" ) {
+ if ( !confirm("Mark download as bad and retry?") )
+ return false;
+ };
+
+ var parent = $(this).parent();
+
+ // Create var for anchor
+ link = $(this);
+
+ // Create var for img under anchor and set options for the loading gif
+ img=$(this).children('img');
+ img.attr('title','loading');
+ img.attr('alt','');
+ img.attr('src',sbRoot+'/images/' + options.loadingImage);
+
$.getJSON($(this).attr('href'), function(data){
- // if they failed then just put the red X
+
+ // if they failed then just put the red X
if (data.result == 'failure') {
img_name = options.noImage;
img_result = 'failed';
// if the snatch was successful then apply the corresponding class and fill in the row appropriately
} else {
- img_name = options.yesImage;
+ img_name = options.loadingImage;
img_result = 'success';
// color the row
if (options.colorRow)
@@ -37,16 +167,22 @@
var rSearchTerm = /(\w+)\s\((.+?)\)/;
HtmlContent = data.result.replace(rSearchTerm,"$1"+' '+"$2"+'');
// update the status column if it exists
- parent.siblings('.status_column').html(HtmlContent)
+ parent.siblings('.status_column').html(HtmlContent)
+ // Only if the queing was succesfull, disable the onClick event of the loading image
+ disableLink(link);
}
- // put the corresponding image as the result for the the row
- parent.empty();
- parent.append($("").attr({"src": sbRoot+"/images/"+img_name, "height": options.size, "alt": img_result, "title": img_result}));
+ // put the corresponding image as the result of queuing of the manual search
+ img.attr('title',img_result);
+ img.attr('alt',img_result);
+ img.attr('height', options.size);
+ img.attr('src',sbRoot+"/images/"+img_name);
});
-
- // fon't follow the link
+ //
+
+ // don't follow the link
return false;
});
}
})();
+
diff --git a/gui/slick/js/displayShow.js b/gui/slick/js/displayShow.js
index 85879664..0abf94d9 100644
--- a/gui/slick/js/displayShow.js
+++ b/gui/slick/js/displayShow.js
@@ -1,7 +1,7 @@
$(document).ready(function () {
$('#sbRoot').ajaxEpSearch({'colorRow': true});
- $('#sbRoot').ajaxEpRetry({'colorRow': true});
+ //$('#sbRoot').ajaxEpRetry({'colorRow': true});
$('#sbRoot').ajaxEpSubtitlesSearch();
diff --git a/sickbeard/search_queue.py b/sickbeard/search_queue.py
index 85bae16b..27b429dd 100644
--- a/sickbeard/search_queue.py
+++ b/sickbeard/search_queue.py
@@ -37,6 +37,8 @@ DAILY_SEARCH = 20
FAILED_SEARCH = 30
MANUAL_SEARCH = 40
+MANUAL_SEARCH_HISTORY = []
+MANUAL_SEARCH_HISTORY_SIZE = 100
class SearchQueue(generic_queue.GenericQueue):
def __init__(self):
@@ -54,7 +56,23 @@ class SearchQueue(generic_queue.GenericQueue):
if isinstance(cur_item, (ManualSearchQueueItem, FailedQueueItem)) and cur_item.segment == segment:
return True
return False
-
+
+ def is_show_in_queue(self, show):
+ for cur_item in self.queue:
+ if isinstance(cur_item, (ManualSearchQueueItem, FailedQueueItem)) and cur_item.show.indexerid == show:
+ return True
+ return False
+
+ def get_all_ep_from_queue(self, show):
+ ep_obj_list = []
+ for cur_item in self.queue:
+ if isinstance(cur_item, (ManualSearchQueueItem, FailedQueueItem)) and str(cur_item.show.indexerid) == show:
+ ep_obj_list.append(cur_item)
+
+ if ep_obj_list:
+ return ep_obj_list
+ return False
+
def pause_backlog(self):
self.min_priority = generic_queue.QueuePriorities.HIGH
@@ -65,6 +83,12 @@ class SearchQueue(generic_queue.GenericQueue):
# backlog priorities are NORMAL, this should be done properly somewhere
return self.min_priority >= generic_queue.QueuePriorities.NORMAL
+ def is_manualsearch_in_progress(self):
+ for cur_item in self.queue + [self.currentItem]:
+ if isinstance(cur_item, (ManualSearchQueueItem, FailedQueueItem)):
+ return True
+ return False
+
def is_backlog_in_progress(self):
for cur_item in self.queue + [self.currentItem]:
if isinstance(cur_item, BacklogQueueItem):
@@ -140,12 +164,15 @@ class ManualSearchQueueItem(generic_queue.QueueItem):
self.success = None
self.show = show
self.segment = segment
+ self.started = None
def run(self):
generic_queue.QueueItem.run(self)
try:
logger.log("Beginning manual search for: [" + self.segment.prettyName() + "]")
+ self.started = True
+
searchResult = search.searchProviders(self.show, [self.segment], True)
if searchResult:
@@ -164,7 +191,10 @@ class ManualSearchQueueItem(generic_queue.QueueItem):
except Exception:
logger.log(traceback.format_exc(), logger.DEBUG)
-
+
+ ### Keep a list with the 100 last executed searches
+ fifo(MANUAL_SEARCH_HISTORY, self, MANUAL_SEARCH_HISTORY_SIZE)
+
if self.success is None:
self.success = False
@@ -245,4 +275,9 @@ class FailedQueueItem(generic_queue.QueueItem):
if self.success is None:
self.success = False
- self.finish()
\ No newline at end of file
+ self.finish()
+
+def fifo(myList, item, maxSize = 100):
+ if len(myList) >= maxSize:
+ myList.pop(0)
+ myList.append(item)
\ No newline at end of file
diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py
index 83e833bd..eeb32fc7 100644
--- a/sickbeard/webserve.py
+++ b/sickbeard/webserve.py
@@ -4307,10 +4307,9 @@ class Home(MainHandler):
root_ep_obj.rename()
redirect("/home/displayShow?show=" + show)
-
-
+
def searchEpisode(self, show=None, season=None, episode=None):
-
+
# retrieve the episode object and fail if we can't get one
ep_obj = _getEpisode(show, season, episode)
if isinstance(ep_obj, str):
@@ -4318,28 +4317,87 @@ class Home(MainHandler):
# make a queue item for it and put it on the queue
ep_queue_item = search_queue.ManualSearchQueueItem(ep_obj.show, ep_obj)
+
sickbeard.searchQueueScheduler.action.add_item(ep_queue_item) # @UndefinedVariable
-
- # wait until the queue item tells us whether it worked or not
- while ep_queue_item.success is None: # @UndefinedVariable
- time.sleep(cpu_presets[sickbeard.CPU_PRESET])
-
- # return the correct json value
+
if ep_queue_item.success:
- # Find the quality class for the episode
- quality_class = Quality.qualityStrings[Quality.UNKNOWN]
- ep_status, ep_quality = Quality.splitCompositeStatus(ep_obj.status)
- for x in (SD, HD720p, HD1080p):
- if ep_quality in Quality.splitQuality(x)[0]:
- quality_class = qualityPresetStrings[x]
- break
+ return returnManualSearchResult(ep_queue_item)
+ if not ep_queue_item.started and ep_queue_item.success is None:
+ return json.dumps({'result': 'success'}) #I Actually want to call it queued, because the search hasnt been started yet!
+ if ep_queue_item.started and ep_queue_item.success is None:
+ return json.dumps({'result': 'success'})
+ else:
+ return json.dumps({'result': 'failure'})
- return json.dumps({'result': statusStrings[ep_obj.status],
- 'quality': quality_class
- })
+ ### Returns the current ep_queue_item status for the current viewed show.
+ # Possible status: Downloaded, Snatched, etc...
+ # Returns {'show': 279530, 'episodes' : ['episode' : 6, 'season' : 1, 'searchstatus' : 'queued', 'status' : 'running', 'quality': '4013']
+ def getManualSearchStatus(self, show=None, season=None):
- return json.dumps({'result': 'failure'})
+ episodes = []
+ currentManualSearchThreadsQueued = []
+ currentManualSearchThreadActive = []
+ finishedManualSearchThreadItems= []
+
+ # Queued Searches
+ currentManualSearchThreadsQueued = sickbeard.searchQueueScheduler.action.get_all_ep_from_queue(show)
+ # Running Searches
+ if (sickbeard.searchQueueScheduler.action.is_manualsearch_in_progress()):
+ currentManualSearchThreadActive = sickbeard.searchQueueScheduler.action.currentItem
+
+ # Finished Searches
+ finishedManualSearchThreadItems = sickbeard.search_queue.MANUAL_SEARCH_HISTORY
+
+ if currentManualSearchThreadsQueued:
+ for searchThread in currentManualSearchThreadsQueued:
+ searchstatus = 'queued'
+
+ episodes.append({'episode': searchThread.segment.episode,
+ 'episodeindexid': searchThread.segment.indexerid,
+ 'season' : searchThread.segment.season,
+ 'searchstatus' : searchstatus,
+ 'status' : statusStrings[searchThread.segment.status],
+ 'quality': self.getQualityClass(searchThread.segment)})
+
+ if currentManualSearchThreadActive:
+ searchThread = currentManualSearchThreadActive
+ searchstatus = 'searching'
+ if searchThread.success:
+ searchstatus = 'finished'
+ episodes.append({'episode': searchThread.segment.episode,
+ 'episodeindexid': searchThread.segment.indexerid,
+ 'season' : searchThread.segment.season,
+ 'searchstatus' : searchstatus,
+ 'status' : statusStrings[searchThread.segment.status],
+ 'quality': self.getQualityClass(searchThread.segment)})
+
+ if finishedManualSearchThreadItems:
+ for searchThread in finishedManualSearchThreadItems:
+ if str(searchThread.show.indexerid) == show and not [x for x in episodes if x['episodeindexid'] == searchThread.segment.indexerid]:
+ searchstatus = 'finished'
+ episodes.append({'episode': searchThread.segment.episode,
+ 'episodeindexid': searchThread.segment.indexerid,
+ 'season' : searchThread.segment.season,
+ 'searchstatus' : searchstatus,
+ 'status' : statusStrings[searchThread.segment.status],
+ 'quality': self.getQualityClass(searchThread.segment)})
+
+ return json.dumps({'show': show, 'episodes' : episodes})
+ #return json.dumps()
+
+ def getQualityClass(self, ep_obj):
+ # return the correct json value
+
+ # Find the quality class for the episode
+ quality_class = Quality.qualityStrings[Quality.UNKNOWN]
+ ep_status, ep_quality = Quality.splitCompositeStatus(ep_obj.status)
+ for x in (SD, HD720p, HD1080p):
+ if ep_quality in Quality.splitQuality(x)[0]:
+ quality_class = qualityPresetStrings[x]
+ break
+
+ return quality_class
def searchEpisodeSubtitles(self, show=None, season=None, episode=None):
|