From 1d00b0e4c2085f7f45d63feedda49ce5ed8d3f3a Mon Sep 17 00:00:00 2001 From: Prinz23 Date: Fri, 14 Sep 2018 02:22:30 +0100 Subject: [PATCH 1/4] Fix manual search button on displayShow and episode view page. Change feedback result of manual search on the clicked button image/tooltip. Devel: Change items from search to retry with Archived status on displayShow page. Change keep manual/failed search item progress for 30 mins in internal manual_search_history. Fix buttons for snatched statuses. Change set wantedquality list for failed to True (needed to also search for previously skipped and similar status items). Change make get_queued_manual() and get_current_manualsearch_item() thread-safe. Change only return retrystatus true to ui if use_failed_downloads is enabled. Change add multi-ep snatches to ajax response. --- .../interfaces/default/inc_displayShow.tmpl | 5 +- gui/slick/js/ajaxEpSearch.js | 25 ++-- sickbeard/generic_queue.py | 20 +++ sickbeard/search_queue.py | 72 ++++++++++- sickbeard/show_updater.py | 6 + sickbeard/webserve.py | 114 +++++++++++------- 6 files changed, 185 insertions(+), 57 deletions(-) diff --git a/gui/slick/interfaces/default/inc_displayShow.tmpl b/gui/slick/interfaces/default/inc_displayShow.tmpl index 80e383d6..a65e1493 100644 --- a/gui/slick/interfaces/default/inc_displayShow.tmpl +++ b/gui/slick/interfaces/default/inc_displayShow.tmpl @@ -1,7 +1,7 @@ #import datetime #import sickbeard #from sickbeard import network_timezones, sbdatetime, subtitles -#from sickbeard.common import Overview, Quality, statusStrings, ARCHIVED, UNAIRED, SUBTITLED +#from sickbeard.common import Overview, Quality, statusStrings, ARCHIVED, UNAIRED, SUBTITLED, SNATCHED_ANY, DOWNLOADED #from lib import subliminal <% def sg_var(varname, default=False): return getattr(sickbeard, varname, default) %>#slurp# <% def sg_str(varname, default=''): return getattr(sickbeard, varname, default) %>#slurp# @@ -107,7 +107,8 @@ #end if #if 0 != int($ep['season']) - #if (int($ep['status']) in $Quality.SNATCHED or int($ep['status']) in $Quality.DOWNLOADED) and $sg_var('USE_FAILED_DOWNLOADS') + #set $status = $Quality.splitCompositeStatus(int($ep['status']))[0] + #if ($status in $SNATCHED_ANY + [$DOWNLOADED, $ARCHIVED]) and $sg_var('USE_FAILED_DOWNLOADS') retry #else search diff --git a/gui/slick/js/ajaxEpSearch.js b/gui/slick/js/ajaxEpSearch.js index 2c6ebea2..acfb8170 100644 --- a/gui/slick/js/ajaxEpSearch.js +++ b/gui/slick/js/ajaxEpSearch.js @@ -47,7 +47,7 @@ function updateImages(data) { if (ep.searchstatus == 'searching') { //el=$('td#' + ep.season + 'x' + ep.episode + '.search img'); img.attr('title','Searching'); - img.attr('alt','earching'); + img.prop('alt','searching'); img.attr('src',sbRoot+'/images/' + loadingImage); disableLink(el); // Update Status and Quality @@ -58,20 +58,31 @@ function updateImages(data) { else if (ep.searchstatus == 'queued') { //el=$('td#' + ep.season + 'x' + ep.episode + '.search img'); img.attr('title','Queued'); - img.attr('alt','queued'); + img.prop('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'); - if (ep.retrystatus) {img.parent().attr('class','epRetry');} else {img.parent().attr('class','epSearch');} + imgparent=img.parent(); + if (ep.retrystatus) { + imgparent.attr('class','epRetry'); + imgparent.attr('href', imgparent.attr('href').replace('/home/searchEpisode?', '/home/retryEpisode?')); + img.attr('title','Retry download'); + img.prop('alt', 'retry download'); + } + else { + imgparent.attr('class','epSearch'); + imgparent.attr('href', imgparent.attr('href').replace('/home/retryEpisode?', '/home/searchEpisode?')); + img.attr('title','Manual search'); + img.prop('alt', 'manual search'); + } img.attr('src',sbRoot+'/images/' + searchImage); enableLink(el); // Update Status and Quality + parent.closest('tr').removeClass('skipped wanted qual good unaired snatched').addClass(ep.statusoverview); var rSearchTerm = /(\w+)\s\((.+?)\)/; HtmlContent = ep.status.replace(rSearchTerm,"$1"+' '+"$2"+''); @@ -140,7 +151,7 @@ function disableLink(el) { // 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.prop('alt',''); img.attr('src',sbRoot+'/images/' + options.loadingImage); @@ -169,7 +180,7 @@ function disableLink(el) { // put the corresponding image as the result of queuing of the manual search img.attr('title',img_result); - img.attr('alt',img_result); + img.prop('alt',img_result); img.attr('height', options.size); img.attr('src',sbRoot+"/images/"+img_name); }); diff --git a/sickbeard/generic_queue.py b/sickbeard/generic_queue.py index f5d49bd5..ec2fd103 100644 --- a/sickbeard/generic_queue.py +++ b/sickbeard/generic_queue.py @@ -18,6 +18,7 @@ import datetime import threading +import copy from sickbeard import logger @@ -98,6 +99,7 @@ class GenericQueue(object): self.currentItem.name = self.queue_name + '-' + self.currentItem.name self.currentItem.start() + class QueueItem(threading.Thread): def __init__(self, name, action_id=0): super(QueueItem, self).__init__() @@ -109,6 +111,24 @@ class QueueItem(threading.Thread): self.stop = threading.Event() self.added = None + def copy(self, deepcopy_obj=None): + """ + Returns a shallow copy of QueueItem with optional deepcopises of in deepcopy_obj listed objects + :param deepcopy_obj: List of properties to be deepcopied + :type deepcopy_obj: list + :return: return QueueItem + :rtype: QueueItem + """ + cls = self.__class__ + result = cls.__new__(cls) + result.__dict__.update(self.__dict__) + if deepcopy_obj: + for o in deepcopy_obj: + if self.__dict__.get(o): + new_seg = copy.deepcopy(self.__dict__.get(o)) + result.__dict__[o] = new_seg + return result + def run(self): """Implementing classes should call this""" diff --git a/sickbeard/search_queue.py b/sickbeard/search_queue.py index 783f94d9..10da45e2 100644 --- a/sickbeard/search_queue.py +++ b/sickbeard/search_queue.py @@ -70,12 +70,18 @@ class SearchQueue(generic_queue.GenericQueue): return False def get_all_ep_from_queue(self, show): + """ + Returns False or List of copies of all show related items in manual or failed queue + :param show: indexerid + :type show: str + :return: False or List of copies of all show related items in manual or failed queue + """ with self.lock: ep_obj_list = [] for cur_item in self.queue: if (isinstance(cur_item, (ManualSearchQueueItem, FailedQueueItem)) and show == str(cur_item.show.indexerid)): - ep_obj_list.append(cur_item) + ep_obj_list.append(cur_item.copy()) if ep_obj_list: return ep_obj_list @@ -101,6 +107,18 @@ class SearchQueue(generic_queue.GenericQueue): return True return False + def get_current_manualsearch_item(self, show): + """ + Returns a static copy of the current item + :param show: indexerid + :type show: str + :return: copy of ManualSearchQueueItem or FailedQueueItem or None + """ + with self.lock: + if self.currentItem and isinstance(self.currentItem, (ManualSearchQueueItem, FailedQueueItem)) \ + and show == str(self.currentItem.show.indexerid): + return self.currentItem.copy() + def is_manualsearch_in_progress(self): # Only referenced in webserve.py, only current running manualsearch or failedsearch is needed!! return self._is_in_progress((ManualSearchQueueItem, FailedQueueItem)) @@ -188,6 +206,7 @@ class RecentSearchQueueItem(generic_queue.QueueItem): self.success = None self.episodes = [] generic_queue.QueueItem.__init__(self, 'Recent Search', RECENT_SEARCH) + self.snatched_eps = {} def run(self): generic_queue.QueueItem.run(self) @@ -243,6 +262,10 @@ class RecentSearchQueueItem(generic_queue.QueueItem): # just use the first result for now logger.log(u'Downloading %s from %s' % (result.name, result.provider.name)) self.success = search.snatch_episode(result) + if self.success: + for ep in result.episodes: + self.snatched_eps.update({'%sx%s' % (ep.season, ep.episode): + {'season': ep.season, 'episode': ep.episode}}) helpers.cpu_sleep() @@ -400,6 +423,13 @@ class ManualSearchQueueItem(generic_queue.QueueItem): self.show = show self.segment = segment self.started = None + self.snatched_eps = {} + + def copy(self, deepcopy_obj=None): + if not isinstance(deepcopy_obj, list): + deepcopy_obj = [] + deepcopy_obj += ['segment', 'show'] + return super(ManualSearchQueueItem, self).copy(deepcopy_obj) def run(self): generic_queue.QueueItem.run(self) @@ -417,6 +447,9 @@ class ManualSearchQueueItem(generic_queue.QueueItem): # just use the first result for now logger.log(u'Downloading %s from %s' % (search_result[0].name, search_result[0].provider.name)) self.success = search.snatch_episode(search_result[0]) + for ep in search_result[0].episodes: + self.snatched_eps.update({'%sx%s' % (ep.season, ep.episode): + {'season': ep.season, 'episode': ep.episode}}) helpers.cpu_sleep() @@ -451,6 +484,13 @@ class BacklogQueueItem(generic_queue.QueueItem): self.limited_backlog = limited_backlog self.forced = forced self.torrent_only = torrent_only + self.snatched_eps = {} + + def copy(self, deepcopy_obj=None): + if not isinstance(deepcopy_obj, list): + deepcopy_obj = [] + deepcopy_obj += ['segment', 'show'] + return super(BacklogQueueItem, self).copy(deepcopy_obj) def run(self): generic_queue.QueueItem.run(self) @@ -472,7 +512,10 @@ class BacklogQueueItem(generic_queue.QueueItem): for result in search_result: # just use the first result for now logger.log(u'Downloading %s from %s' % (result.name, result.provider.name)) - search.snatch_episode(result) + if search.snatch_episode(result): + for ep in result.episodes: + self.snatched_eps.update({'%sx%s' % (ep.season, ep.episode): + {'season': ep.season, 'episode': ep.episode}}) helpers.cpu_sleep() else: @@ -495,6 +538,13 @@ class FailedQueueItem(generic_queue.QueueItem): self.segment = segment self.success = None self.started = None + self.snatched_eps = {} + + def copy(self, deepcopy_obj=None): + if not isinstance(deepcopy_obj, list): + deepcopy_obj = [] + deepcopy_obj += ['segment', 'show'] + return super(FailedQueueItem, self).copy(deepcopy_obj) def run(self): generic_queue.QueueItem.run(self) @@ -515,7 +565,7 @@ class FailedQueueItem(generic_queue.QueueItem): logger.log(u'Beginning failed download search for: [%s]' % ep_obj.prettyName()) - set_wanted_aired(ep_obj, True, ep_count, ep_count_scene) + set_wanted_aired(ep_obj, True, ep_count, ep_count_scene, manual=True) search_result = search.search_providers(self.show, self.segment, True, try_other_searches=True) @@ -523,7 +573,10 @@ class FailedQueueItem(generic_queue.QueueItem): for result in search_result: # just use the first result for now logger.log(u'Downloading %s from %s' % (result.name, result.provider.name)) - search.snatch_episode(result) + if search.snatch_episode(result): + for ep in result.episodes: + self.snatched_eps.update({'%sx%s' % (ep.season, ep.episode): + {'season': ep.season, 'episode': ep.episode}}) helpers.cpu_sleep() else: @@ -543,6 +596,17 @@ class FailedQueueItem(generic_queue.QueueItem): def fifo(my_list, item, max_size=100): + remove_old_fifo(my_list) + item.added_dt = datetime.datetime.now() if len(my_list) >= max_size: my_list.pop(0) my_list.append(item) + + +def remove_old_fifo(my_list, age=datetime.timedelta(minutes=30)): + try: + now = datetime.datetime.now() + my_list[:] = [i for i in my_list if not isinstance(getattr(i, 'added_dt', None), datetime.datetime) + or now - i.added_dt < age] + except (StandardError, Exception): + pass diff --git a/sickbeard/show_updater.py b/sickbeard/show_updater.py index 1d2fc27a..0058db1d 100644 --- a/sickbeard/show_updater.py +++ b/sickbeard/show_updater.py @@ -85,6 +85,12 @@ class ShowUpdater: logger.log('image cache cleanup error', logger.ERROR) logger.log(traceback.format_exc(), logger.ERROR) + # cleanup manual search history + try: + sickbeard.search_queue.remove_old_fifo(sickbeard.search_queue.MANUAL_SEARCH_HISTORY) + except (StandardError, Exception): + pass + # add missing mapped ids if not sickbeard.background_mapping_task.is_alive(): logger.log(u'Updating the Indexer mappings') diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index 209d847e..e50a3dee 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -2777,8 +2777,8 @@ class Home(MainHandler): sickbeard.searchQueueScheduler.action.add_item(ep_queue_item) # @UndefinedVariable - if ep_queue_item.success: - return returnManualSearchResult(ep_queue_item) + #if ep_queue_item.success: + # 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: @@ -2792,6 +2792,7 @@ class Home(MainHandler): def getManualSearchStatus(self, show=None, season=None): episodes = [] + eps_list = set() currentManualSearchThreadsQueued = [] currentManualSearchThreadActive = [] finishedManualSearchThreadItems= [] @@ -2799,83 +2800,108 @@ class Home(MainHandler): # 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 + currentManualSearchThreadActive = sickbeard.searchQueueScheduler.action.get_current_manualsearch_item(show) # Finished Searches - finishedManualSearchThreadItems = sickbeard.search_queue.MANUAL_SEARCH_HISTORY + sickbeard.search_queue.remove_old_fifo(sickbeard.search_queue.MANUAL_SEARCH_HISTORY) + finishedManualSearchThreadItems = sickbeard.search_queue.MANUAL_SEARCH_HISTORY if currentManualSearchThreadsQueued: for searchThread in currentManualSearchThreadsQueued: searchstatus = 'queued' if isinstance(searchThread, sickbeard.search_queue.ManualSearchQueueItem): + eps_list.add('%sx%s' % (searchThread.segment.season, searchThread.segment.episode)) episodes.append({'episode': searchThread.segment.episode, - 'episodeindexid': searchThread.segment.indexerid, - 'season' : searchThread.segment.season, - 'searchstatus' : searchstatus, - 'status' : statusStrings[searchThread.segment.status], + 'showindexer': searchThread.show.indexer, + 'showindexid': searchThread.show.indexerid, + 'season': searchThread.segment.season, + 'searchstatus': searchstatus, + 'status': statusStrings[searchThread.segment.status], 'quality': self.getQualityClass(searchThread.segment)}) elif hasattr(searchThread, 'segment'): for epObj in searchThread.segment: + eps_list.add('%sx%s' % (epObj.season, epObj.episode)) episodes.append({'episode': epObj.episode, - 'episodeindexid': epObj.indexerid, - 'season' : epObj.season, - 'searchstatus' : searchstatus, - 'status' : statusStrings[epObj.status], - 'quality': self.getQualityClass(epObj)}) + 'showindexer': epObj.show.indexer, + 'showindexid': epObj.show.indexerid, + 'season': epObj.season, + 'searchstatus': searchstatus, + 'status': statusStrings[epObj.status], + 'quality': self.getQualityClass(epObj)}) retry_statues = SNATCHED_ANY + [DOWNLOADED, ARCHIVED] if currentManualSearchThreadActive: searchThread = currentManualSearchThreadActive - searchstatus = 'searching' if searchThread.success: searchstatus = 'finished' else: searchstatus = 'searching' if isinstance(searchThread, sickbeard.search_queue.ManualSearchQueueItem): + eps_list.add('%sx%s' % (searchThread.segment.season, searchThread.segment.episode)) episodes.append({'episode': searchThread.segment.episode, - 'episodeindexid': searchThread.segment.indexerid, - 'season' : searchThread.segment.season, - 'searchstatus' : searchstatus, - 'retrystatus': Quality.splitCompositeStatus(searchThread.segment.status)[0] in retry_statues, - 'status' : statusStrings[searchThread.segment.status], + 'showindexer': searchThread.show.indexer, + 'showindexid': searchThread.show.indexerid, + 'season': searchThread.segment.season, + 'searchstatus': searchstatus, + 'retrystatus': sickbeard.USE_FAILED_DOWNLOADS and Quality.splitCompositeStatus(searchThread.segment.status)[0] in retry_statues, + 'status': statusStrings[searchThread.segment.status], 'quality': self.getQualityClass(searchThread.segment)}) elif hasattr(searchThread, 'segment'): for epObj in searchThread.segment: + eps_list.add('%sx%s' % (epObj.season, epObj.episode)) episodes.append({'episode': epObj.episode, - 'episodeindexid': epObj.indexerid, - 'season' : epObj.season, - 'searchstatus' : searchstatus, - 'retrystatus': Quality.splitCompositeStatus(epObj.status)[0] in retry_statues, - 'status' : statusStrings[epObj.status], + 'showindexer': epObj.show.indexer, + 'showindexid': epObj.show.indexerid, + 'season': epObj.season, + 'searchstatus': searchstatus, + 'retrystatus': sickbeard.USE_FAILED_DOWNLOADS and Quality.splitCompositeStatus(epObj.status)[0] in retry_statues, + 'status': statusStrings[epObj.status], 'quality': self.getQualityClass(epObj)}) if finishedManualSearchThreadItems: + searchstatus = 'finished' for searchThread in finishedManualSearchThreadItems: if isinstance(searchThread, sickbeard.search_queue.ManualSearchQueueItem): - if str(searchThread.show.indexerid) == show and not [x for x in episodes if x['episodeindexid'] == searchThread.segment.indexerid]: - searchstatus = 'finished' + if str(searchThread.show.indexerid) == show and '%sx%s' % (searchThread.segment.season, searchThread.segment.episode) not in eps_list: + eps_list.add('%sx%s' % (searchThread.segment.season, searchThread.segment.episode)) episodes.append({'episode': searchThread.segment.episode, - 'episodeindexid': searchThread.segment.indexerid, - 'season' : searchThread.segment.season, - 'searchstatus' : searchstatus, - 'retrystatus': Quality.splitCompositeStatus(searchThread.segment.status)[0] in retry_statues, - 'status' : statusStrings[searchThread.segment.status], - 'quality': self.getQualityClass(searchThread.segment)}) + 'showindexer': searchThread.show.indexer, + 'showindexid': searchThread.show.indexerid, + 'season': searchThread.segment.season, + 'searchstatus': searchstatus, + 'retrystatus': sickbeard.USE_FAILED_DOWNLOADS and Quality.splitCompositeStatus(searchThread.segment.status)[0] in retry_statues, + 'status': statusStrings[searchThread.segment.status], + 'quality': self.getQualityClass(searchThread.segment), + 'statusoverview': Overview.overviewStrings[searchThread.show.getOverview(searchThread.segment.status)]}) ### These are only Failed Downloads/Retry SearchThreadItems.. lets loop through the segement/episodes elif hasattr(searchThread, 'segment') and str(searchThread.show.indexerid) == show: for epObj in searchThread.segment: - if not [x for x in episodes if x['episodeindexid'] == epObj.indexerid]: - searchstatus = 'finished' + if '%sx%s' % (epObj.season, epObj.episode) not in eps_list: + eps_list.add('%sx%s' % (epObj.season, epObj.episode)) episodes.append({'episode': epObj.episode, - 'episodeindexid': epObj.indexerid, - 'season' : epObj.season, - 'searchstatus' : searchstatus, - 'retrystatus': Quality.splitCompositeStatus(epObj.status)[0] in retry_statues, - 'status' : statusStrings[epObj.status], - 'quality': self.getQualityClass(epObj)}) + 'showindexer': epObj.show.indexer, + 'showindexid': epObj.show.indexerid, + 'season': epObj.season, + 'searchstatus': searchstatus, + 'retrystatus': sickbeard.USE_FAILED_DOWNLOADS and Quality.splitCompositeStatus(epObj.status)[0] in retry_statues, + 'status': statusStrings[epObj.status], + 'quality': self.getQualityClass(epObj), + 'statusoverview': Overview.overviewStrings[searchThread.show.getOverview(epObj.status)]}) - return json.dumps({'show': show, 'episodes' : episodes}) + for ep in [v for k, v in searchThread.snatched_eps.iteritems() if k not in eps_list]: + ep_obj = searchThread.show.getEpisode(season=ep['season'], episode=ep['episode']) + episodes.append({'episode': ep['episode'], + 'showindexer': searchThread.show.indexer, + 'showindexid': searchThread.show.indexerid, + 'season': ep['season'], + 'searchstatus': searchstatus, + 'retrystatus': sickbeard.USE_FAILED_DOWNLOADS and Quality.splitCompositeStatus(ep_obj.status)[0] in retry_statues, + 'status': statusStrings[ep_obj.status], + 'quality': self.getQualityClass(ep_obj), + 'statusoverview': Overview.overviewStrings[searchThread.show.getOverview(ep_obj.status)]}) + + + return json.dumps({'show': show, 'episodes': episodes}) #return json.dumps() @@ -2936,8 +2962,8 @@ class Home(MainHandler): ep_queue_item = search_queue.FailedQueueItem(ep_obj.show, [ep_obj]) sickbeard.searchQueueScheduler.action.add_item(ep_queue_item) # @UndefinedVariable - if ep_queue_item.success: - return returnManualSearchResult(ep_queue_item) + #if ep_queue_item.success: + # 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: From 85a5f6ea00bfe10a3b251d54e471b19c63950453 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Sun, 16 Sep 2018 03:11:31 +0100 Subject: [PATCH 2/4] Change code refactor. Devel: Use camel case for JS vars. Correct typos. Define attr added_dt. Remove redundant try/expect. --- gui/slick/js/ajaxEpSearch.js | 10 +- sickbeard/generic_queue.py | 4 +- sickbeard/search_queue.py | 82 ++++++------ sickbeard/show_updater.py | 5 +- sickbeard/webserve.py | 248 +++++++++++++++-------------------- 5 files changed, 149 insertions(+), 200 deletions(-) diff --git a/gui/slick/js/ajaxEpSearch.js b/gui/slick/js/ajaxEpSearch.js index acfb8170..aad1f07f 100644 --- a/gui/slick/js/ajaxEpSearch.js +++ b/gui/slick/js/ajaxEpSearch.js @@ -65,16 +65,16 @@ function updateImages(data) { } else if (ep.searchstatus == 'finished') { //el=$('td#' + ep.season + 'x' + ep.episode + '.search img'); - imgparent=img.parent(); + var imgParent = img.parent(); if (ep.retrystatus) { - imgparent.attr('class','epRetry'); - imgparent.attr('href', imgparent.attr('href').replace('/home/searchEpisode?', '/home/retryEpisode?')); + imgParent.attr('class','epRetry'); + imgParent.attr('href', imgParent.attr('href').replace('/home/searchEpisode?', '/home/retryEpisode?')); img.attr('title','Retry download'); img.prop('alt', 'retry download'); } else { - imgparent.attr('class','epSearch'); - imgparent.attr('href', imgparent.attr('href').replace('/home/retryEpisode?', '/home/searchEpisode?')); + imgParent.attr('class','epSearch'); + imgParent.attr('href', imgParent.attr('href').replace('/home/retryEpisode?', '/home/searchEpisode?')); img.attr('title','Manual search'); img.prop('alt', 'manual search'); } diff --git a/sickbeard/generic_queue.py b/sickbeard/generic_queue.py index ec2fd103..9315b3eb 100644 --- a/sickbeard/generic_queue.py +++ b/sickbeard/generic_queue.py @@ -113,8 +113,8 @@ class QueueItem(threading.Thread): def copy(self, deepcopy_obj=None): """ - Returns a shallow copy of QueueItem with optional deepcopises of in deepcopy_obj listed objects - :param deepcopy_obj: List of properties to be deepcopied + Returns a shallow copy of QueueItem with optional deepcopy of in deepcopy_obj listed objects + :param deepcopy_obj: List of properties to be deep copied :type deepcopy_obj: list :return: return QueueItem :rtype: QueueItem diff --git a/sickbeard/search_queue.py b/sickbeard/search_queue.py index 10da45e2..2d3b45e9 100644 --- a/sickbeard/search_queue.py +++ b/sickbeard/search_queue.py @@ -69,24 +69,6 @@ class SearchQueue(generic_queue.GenericQueue): return True return False - def get_all_ep_from_queue(self, show): - """ - Returns False or List of copies of all show related items in manual or failed queue - :param show: indexerid - :type show: str - :return: False or List of copies of all show related items in manual or failed queue - """ - with self.lock: - ep_obj_list = [] - for cur_item in self.queue: - if (isinstance(cur_item, (ManualSearchQueueItem, FailedQueueItem)) and - show == str(cur_item.show.indexerid)): - ep_obj_list.append(cur_item.copy()) - - if ep_obj_list: - return ep_obj_list - return False - def pause_backlog(self): with self.lock: self.min_priority = generic_queue.QueuePriorities.HIGH @@ -107,22 +89,34 @@ class SearchQueue(generic_queue.GenericQueue): return True return False - def get_current_manualsearch_item(self, show): + def get_queued_manual(self, show): """ - Returns a static copy of the current item - :param show: indexerid - :type show: str + Returns None or List of copies of all show related items in manual or failed queue + :param show: show indexerid or None for all q items + :type show: String or None + :return: List with 0 or more items + """ + ep_obj_list = [] + with self.lock: + for cur_item in self.queue: + if (isinstance(cur_item, (ManualSearchQueueItem, FailedQueueItem)) and + (not show or show == str(cur_item.show.indexerid))): + ep_obj_list.append(cur_item.copy()) + + return ep_obj_list + + def get_current_manual_item(self, show): + """ + Returns a static copy of the currently active manual search item + :param show: show indexerid or None for all q items + :type show: String or None :return: copy of ManualSearchQueueItem or FailedQueueItem or None """ with self.lock: if self.currentItem and isinstance(self.currentItem, (ManualSearchQueueItem, FailedQueueItem)) \ - and show == str(self.currentItem.show.indexerid): + and (not show or show == str(self.currentItem.show.indexerid)): return self.currentItem.copy() - def is_manualsearch_in_progress(self): - # Only referenced in webserve.py, only current running manualsearch or failedsearch is needed!! - return self._is_in_progress((ManualSearchQueueItem, FailedQueueItem)) - def is_backlog_in_progress(self): return self._is_in_progress(BacklogQueueItem) @@ -206,7 +200,7 @@ class RecentSearchQueueItem(generic_queue.QueueItem): self.success = None self.episodes = [] generic_queue.QueueItem.__init__(self, 'Recent Search', RECENT_SEARCH) - self.snatched_eps = {} + self.snatched_eps = set([]) def run(self): generic_queue.QueueItem.run(self) @@ -264,8 +258,7 @@ class RecentSearchQueueItem(generic_queue.QueueItem): self.success = search.snatch_episode(result) if self.success: for ep in result.episodes: - self.snatched_eps.update({'%sx%s' % (ep.season, ep.episode): - {'season': ep.season, 'episode': ep.episode}}) + self.snatched_eps.add((ep.show.indexer, ep.show.indexerid, ep.season, ep.episode)) helpers.cpu_sleep() @@ -423,7 +416,8 @@ class ManualSearchQueueItem(generic_queue.QueueItem): self.show = show self.segment = segment self.started = None - self.snatched_eps = {} + self.added_dt = None + self.snatched_eps = set([]) def copy(self, deepcopy_obj=None): if not isinstance(deepcopy_obj, list): @@ -448,8 +442,7 @@ class ManualSearchQueueItem(generic_queue.QueueItem): logger.log(u'Downloading %s from %s' % (search_result[0].name, search_result[0].provider.name)) self.success = search.snatch_episode(search_result[0]) for ep in search_result[0].episodes: - self.snatched_eps.update({'%sx%s' % (ep.season, ep.episode): - {'season': ep.season, 'episode': ep.episode}}) + self.snatched_eps.add((ep.show.indexer, ep.show.indexerid, ep.season, ep.episode)) helpers.cpu_sleep() @@ -463,8 +456,8 @@ class ManualSearchQueueItem(generic_queue.QueueItem): logger.log(traceback.format_exc(), logger.ERROR) finally: - # Keep a list with the 100 last executed searches - fifo(MANUAL_SEARCH_HISTORY, self, MANUAL_SEARCH_HISTORY_SIZE) + # Keep a list with the last executed searches + fifo(MANUAL_SEARCH_HISTORY, self) if self.success is None: self.success = False @@ -484,7 +477,7 @@ class BacklogQueueItem(generic_queue.QueueItem): self.limited_backlog = limited_backlog self.forced = forced self.torrent_only = torrent_only - self.snatched_eps = {} + self.snatched_eps = set([]) def copy(self, deepcopy_obj=None): if not isinstance(deepcopy_obj, list): @@ -514,8 +507,7 @@ class BacklogQueueItem(generic_queue.QueueItem): logger.log(u'Downloading %s from %s' % (result.name, result.provider.name)) if search.snatch_episode(result): for ep in result.episodes: - self.snatched_eps.update({'%sx%s' % (ep.season, ep.episode): - {'season': ep.season, 'episode': ep.episode}}) + self.snatched_eps.add((ep.show.indexer, ep.show.indexerid, ep.season, ep.episode)) helpers.cpu_sleep() else: @@ -538,7 +530,8 @@ class FailedQueueItem(generic_queue.QueueItem): self.segment = segment self.success = None self.started = None - self.snatched_eps = {} + self.added_dt = None + self.snatched_eps = set([]) def copy(self, deepcopy_obj=None): if not isinstance(deepcopy_obj, list): @@ -575,8 +568,7 @@ class FailedQueueItem(generic_queue.QueueItem): logger.log(u'Downloading %s from %s' % (result.name, result.provider.name)) if search.snatch_episode(result): for ep in result.episodes: - self.snatched_eps.update({'%sx%s' % (ep.season, ep.episode): - {'season': ep.season, 'episode': ep.episode}}) + self.snatched_eps.add((ep.show.indexer, ep.show.indexerid, ep.season, ep.episode)) helpers.cpu_sleep() else: @@ -586,8 +578,8 @@ class FailedQueueItem(generic_queue.QueueItem): logger.log(traceback.format_exc(), logger.ERROR) finally: - # Keep a list with the 100 last executed searches - fifo(MANUAL_SEARCH_HISTORY, self, MANUAL_SEARCH_HISTORY_SIZE) + # Keep a list with the last executed searches + fifo(MANUAL_SEARCH_HISTORY, self) if self.success is None: self.success = False @@ -595,10 +587,10 @@ class FailedQueueItem(generic_queue.QueueItem): self.finish() -def fifo(my_list, item, max_size=100): +def fifo(my_list, item): remove_old_fifo(my_list) item.added_dt = datetime.datetime.now() - if len(my_list) >= max_size: + if len(my_list) >= MANUAL_SEARCH_HISTORY_SIZE: my_list.pop(0) my_list.append(item) diff --git a/sickbeard/show_updater.py b/sickbeard/show_updater.py index 0058db1d..649a62d5 100644 --- a/sickbeard/show_updater.py +++ b/sickbeard/show_updater.py @@ -86,10 +86,7 @@ class ShowUpdater: logger.log(traceback.format_exc(), logger.ERROR) # cleanup manual search history - try: - sickbeard.search_queue.remove_old_fifo(sickbeard.search_queue.MANUAL_SEARCH_HISTORY) - except (StandardError, Exception): - pass + sickbeard.search_queue.remove_old_fifo(sickbeard.search_queue.MANUAL_SEARCH_HISTORY) # add missing mapped ids if not sickbeard.background_mapping_task.is_alive(): diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index e50a3dee..0c25e686 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -2765,158 +2765,138 @@ class Home(MainHandler): self.redirect('/home/displayShow?show=' + show) - def searchEpisode(self, show=None, season=None, episode=None): + def episode_search(self, show=None, season=None, episode=None, retry=False): + + result = dict(result='failure') # retrieve the episode object and fail if we can't get one ep_obj = self._getEpisode(show, season, episode) - if isinstance(ep_obj, str): - return json.dumps({'result': 'failure'}) + if not isinstance(ep_obj, str): - # make a queue item for it and put it on the queue - ep_queue_item = search_queue.ManualSearchQueueItem(ep_obj.show, ep_obj) + # make a queue item for the TVEpisode and put it on the queue + ep_queue_item = (search_queue.ManualSearchQueueItem(ep_obj.show, ep_obj), + search_queue.FailedQueueItem(ep_obj.show, [ep_obj]))[retry] - sickbeard.searchQueueScheduler.action.add_item(ep_queue_item) # @UndefinedVariable + sickbeard.searchQueueScheduler.action.add_item(ep_queue_item) - #if ep_queue_item.success: - # 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'}) + if None is ep_queue_item.success: # invocation + result.update(dict(result=('success', 'queueing')[not ep_queue_item.started])) + # elif ep_queue_item.success: + # return self.search_q_progress(str(ep_obj.show.indexerid)) # page refresh - ### 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) + + def episode_retry(self, show, season, episode): + + return self.episode_search(show, season, episode, True) + + # Return progress for queued, active and finished episodes + def search_q_progress(self, show=None): episodes = [] - eps_list = set() - currentManualSearchThreadsQueued = [] - currentManualSearchThreadActive = [] - finishedManualSearchThreadItems= [] + seen_eps = set([]) - # Queued Searches - currentManualSearchThreadsQueued = sickbeard.searchQueueScheduler.action.get_all_ep_from_queue(show) - # Running Searches - currentManualSearchThreadActive = sickbeard.searchQueueScheduler.action.get_current_manualsearch_item(show) + # Queued searches + queued_items = sickbeard.searchQueueScheduler.action.get_queued_manual(show) - # Finished Searches + # Active search + active_item = sickbeard.searchQueueScheduler.action.get_current_manual_item(show) + + # Finished searches sickbeard.search_queue.remove_old_fifo(sickbeard.search_queue.MANUAL_SEARCH_HISTORY) - finishedManualSearchThreadItems = sickbeard.search_queue.MANUAL_SEARCH_HISTORY + finished_items = sickbeard.search_queue.MANUAL_SEARCH_HISTORY - if currentManualSearchThreadsQueued: - for searchThread in currentManualSearchThreadsQueued: - searchstatus = 'queued' - if isinstance(searchThread, sickbeard.search_queue.ManualSearchQueueItem): - eps_list.add('%sx%s' % (searchThread.segment.season, searchThread.segment.episode)) - episodes.append({'episode': searchThread.segment.episode, - 'showindexer': searchThread.show.indexer, - 'showindexid': searchThread.show.indexerid, - 'season': searchThread.segment.season, - 'searchstatus': searchstatus, - 'status': statusStrings[searchThread.segment.status], - 'quality': self.getQualityClass(searchThread.segment)}) - elif hasattr(searchThread, 'segment'): - for epObj in searchThread.segment: - eps_list.add('%sx%s' % (epObj.season, epObj.episode)) - episodes.append({'episode': epObj.episode, - 'showindexer': epObj.show.indexer, - 'showindexid': epObj.show.indexerid, - 'season': epObj.season, - 'searchstatus': searchstatus, - 'status': statusStrings[epObj.status], - 'quality': self.getQualityClass(epObj)}) + progress = 'queued' + for thread in queued_items: + if isinstance(thread, sickbeard.search_queue.ManualSearchQueueItem): + ep, uniq_sxe = self.prepare_episode(thread.show, thread.segment, progress) + episodes.append(ep) + seen_eps.add(uniq_sxe) - retry_statues = SNATCHED_ANY + [DOWNLOADED, ARCHIVED] - if currentManualSearchThreadActive: - searchThread = currentManualSearchThreadActive - if searchThread.success: - searchstatus = 'finished' - else: - searchstatus = 'searching' - if isinstance(searchThread, sickbeard.search_queue.ManualSearchQueueItem): - eps_list.add('%sx%s' % (searchThread.segment.season, searchThread.segment.episode)) - episodes.append({'episode': searchThread.segment.episode, - 'showindexer': searchThread.show.indexer, - 'showindexid': searchThread.show.indexerid, - 'season': searchThread.segment.season, - 'searchstatus': searchstatus, - 'retrystatus': sickbeard.USE_FAILED_DOWNLOADS and Quality.splitCompositeStatus(searchThread.segment.status)[0] in retry_statues, - 'status': statusStrings[searchThread.segment.status], - 'quality': self.getQualityClass(searchThread.segment)}) - elif hasattr(searchThread, 'segment'): - for epObj in searchThread.segment: - eps_list.add('%sx%s' % (epObj.season, epObj.episode)) - episodes.append({'episode': epObj.episode, - 'showindexer': epObj.show.indexer, - 'showindexid': epObj.show.indexerid, - 'season': epObj.season, - 'searchstatus': searchstatus, - 'retrystatus': sickbeard.USE_FAILED_DOWNLOADS and Quality.splitCompositeStatus(epObj.status)[0] in retry_statues, - 'status': statusStrings[epObj.status], - 'quality': self.getQualityClass(epObj)}) + elif hasattr(thread, 'segment'): + for ep_obj in thread.segment: + ep, uniq_sxe = self.prepare_episode(ep_obj.show, ep_obj, progress) + episodes.append(ep) + seen_eps.add(uniq_sxe) - if finishedManualSearchThreadItems: - searchstatus = 'finished' - for searchThread in finishedManualSearchThreadItems: - if isinstance(searchThread, sickbeard.search_queue.ManualSearchQueueItem): - if str(searchThread.show.indexerid) == show and '%sx%s' % (searchThread.segment.season, searchThread.segment.episode) not in eps_list: - eps_list.add('%sx%s' % (searchThread.segment.season, searchThread.segment.episode)) - episodes.append({'episode': searchThread.segment.episode, - 'showindexer': searchThread.show.indexer, - 'showindexid': searchThread.show.indexerid, - 'season': searchThread.segment.season, - 'searchstatus': searchstatus, - 'retrystatus': sickbeard.USE_FAILED_DOWNLOADS and Quality.splitCompositeStatus(searchThread.segment.status)[0] in retry_statues, - 'status': statusStrings[searchThread.segment.status], - 'quality': self.getQualityClass(searchThread.segment), - 'statusoverview': Overview.overviewStrings[searchThread.show.getOverview(searchThread.segment.status)]}) - ### These are only Failed Downloads/Retry SearchThreadItems.. lets loop through the segement/episodes - elif hasattr(searchThread, 'segment') and str(searchThread.show.indexerid) == show: - for epObj in searchThread.segment: - if '%sx%s' % (epObj.season, epObj.episode) not in eps_list: - eps_list.add('%sx%s' % (epObj.season, epObj.episode)) - episodes.append({'episode': epObj.episode, - 'showindexer': epObj.show.indexer, - 'showindexid': epObj.show.indexerid, - 'season': epObj.season, - 'searchstatus': searchstatus, - 'retrystatus': sickbeard.USE_FAILED_DOWNLOADS and Quality.splitCompositeStatus(epObj.status)[0] in retry_statues, - 'status': statusStrings[epObj.status], - 'quality': self.getQualityClass(epObj), - 'statusoverview': Overview.overviewStrings[searchThread.show.getOverview(epObj.status)]}) + if active_item: + thread = active_item + episode_params = dict(([('searchstate', 'finished'), ('statusoverview', True)], + [('searchstate', 'searching'), ('statusoverview', False)])[None is thread.success], + retrystate=True) + if isinstance(thread, sickbeard.search_queue.ManualSearchQueueItem): + ep, uniq_sxe = self.prepare_episode(thread.show, thread.segment, **episode_params) + episodes.append(ep) + seen_eps.add(uniq_sxe) - for ep in [v for k, v in searchThread.snatched_eps.iteritems() if k not in eps_list]: - ep_obj = searchThread.show.getEpisode(season=ep['season'], episode=ep['episode']) - episodes.append({'episode': ep['episode'], - 'showindexer': searchThread.show.indexer, - 'showindexid': searchThread.show.indexerid, - 'season': ep['season'], - 'searchstatus': searchstatus, - 'retrystatus': sickbeard.USE_FAILED_DOWNLOADS and Quality.splitCompositeStatus(ep_obj.status)[0] in retry_statues, - 'status': statusStrings[ep_obj.status], - 'quality': self.getQualityClass(ep_obj), - 'statusoverview': Overview.overviewStrings[searchThread.show.getOverview(ep_obj.status)]}) + elif hasattr(thread, 'segment'): + for ep_obj in thread.segment: + ep, uniq_sxe = self.prepare_episode(ep_obj.show, ep_obj, **episode_params) + episodes.append(ep) + seen_eps.add(uniq_sxe) + episode_params = dict(searchstate='finished', retrystate=True, statusoverview=True) + for thread in finished_items: + if isinstance(thread, sickbeard.search_queue.ManualSearchQueueItem): + if (not show or show == str(thread.show.indexerid)) and \ + (thread.show.indexer, thread.show.indexerid, thread.segment.season, thread.segment.episode) \ + not in seen_eps: + ep, uniq_sxe = self.prepare_episode(thread.show, thread.segment, **episode_params) + episodes.append(ep) + seen_eps.add(uniq_sxe) - return json.dumps({'show': show, 'episodes': episodes}) + # These are only Failed Downloads/Retry SearchThreadItems.. lets loop through the segment/episodes + elif hasattr(thread, 'segment') and show == str(thread.show.indexerid): + for ep_obj in thread.segment: + if (ep_obj.show.indexer, ep_obj.show.indexerid, ep_obj.season, ep_obj.episode) not in seen_eps: + ep, uniq_sxe = self.prepare_episode(ep_obj.show, ep_obj, **episode_params) + episodes.append(ep) + seen_eps.add(uniq_sxe) - #return json.dumps() + for snatched in filter(lambda v: v not in seen_eps, thread.snatched_eps): + ep_obj = thread.show.getEpisode(season=snatched[2], episode=snatched[3]) + ep, uniq_sxe = self.prepare_episode(thread.show, ep_obj, **episode_params) + episodes.append(ep) + seen_eps.add(uniq_sxe) - def getQualityClass(self, ep_obj): - # return the correct json value + return json.dumps(dict(episodes=episodes)) + @staticmethod + def prepare_episode(show, ep, searchstate, retrystate=False, statusoverview=False): + """ + Prepare episode data and its unique id + + :param show: Show object + :type show: TVShow object + :param ep: Episode object + :type ep: TVEpisode object + :param searchstate: Progress of search + :type searchstate: string + :param retrystate: True to add retrystate to data + :type retrystate: bool + :param statusoverview: True to add statusoverview to data + :type statusoverview: bool + :return: Episode data and its unique episode id + :rtype: tuple containing a dict and a tuple + """ # Find the quality class for the episode quality_class = Quality.qualityStrings[Quality.UNKNOWN] - ep_status, ep_quality = Quality.splitCompositeStatus(ep_obj.status) + ep_status, ep_quality = Quality.splitCompositeStatus(ep.status) for x in (SD, HD720p, HD1080p, UHD2160p): if ep_quality in Quality.splitQuality(x)[0]: quality_class = qualityPresetStrings[x] break - return quality_class + ep_data = dict(showindexer=show.indexer, showindexid=show.indexerid, + season=ep.season, episode=ep.episode, quality=quality_class, + searchstate=searchstate, status=statusStrings[ep.status]) + if retrystate: + retry_statuses = SNATCHED_ANY + [DOWNLOADED, ARCHIVED] + ep_data.update(dict(retrystate=sickbeard.USE_FAILED_DOWNLOADS and ep_status in retry_statuses)) + if statusoverview: + ep_data.update(dict(statusoverview=Overview.overviewStrings[show.getOverview(ep.status)])) + + return ep_data, (show.indexer, show.indexerid, ep.season, ep.episode) def searchEpisodeSubtitles(self, show=None, season=None, episode=None): # retrieve the episode object and fail if we can't get one @@ -2951,26 +2931,6 @@ class Home(MainHandler): return json.dumps(result) - def retryEpisode(self, show, season, episode): - - # retrieve the episode object and fail if we can't get one - ep_obj = self._getEpisode(show, season, episode) - if isinstance(ep_obj, str): - return json.dumps({'result': 'failure'}) - - # make a queue item for it and put it on the queue - ep_queue_item = search_queue.FailedQueueItem(ep_obj.show, [ep_obj]) - sickbeard.searchQueueScheduler.action.add_item(ep_queue_item) # @UndefinedVariable - - #if ep_queue_item.success: - # 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'}) - @staticmethod def fetch_releasegroups(show_name): From 89905fc94dbcbd309a6e7303f34083d228b99f61 Mon Sep 17 00:00:00 2001 From: Prinz23 Date: Wed, 26 Sep 2018 00:19:58 +0100 Subject: [PATCH 3/4] Change add BaseSearchQueueItem and base_info() for thread-safe checks. Change to making simple base info of search_queue items instead of full deep copies. Change move TVShow.getOverview logic to helpers.episode_status_overview() and call it with parameter in tv.py and webserve.py --- sickbeard/classes.py | 14 ++++++ sickbeard/helpers.py | 33 +++++++++++++- sickbeard/search_queue.py | 93 +++++++++++++++++++++++---------------- sickbeard/tv.py | 29 +----------- sickbeard/webserve.py | 19 +++----- 5 files changed, 107 insertions(+), 81 deletions(-) diff --git a/sickbeard/classes.py b/sickbeard/classes.py index 9ea85d83..72a43111 100644 --- a/sickbeard/classes.py +++ b/sickbeard/classes.py @@ -452,3 +452,17 @@ else: return v if v is not None else default sickbeard.ENV = LinuxEnv(os.environ) + + +# backport from python 3 +class SimpleNamespace: + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + def __repr__(self): + keys = sorted(self.__dict__) + items = ("{}={!r}".format(k, self.__dict__[k]) for k in keys) + return "{}({})".format(type(self).__name__, ", ".join(items)) + + def __eq__(self, other): + return self.__dict__ == other.__dict__ diff --git a/sickbeard/helpers.py b/sickbeard/helpers.py index 2ff81260..98fb2594 100644 --- a/sickbeard/helpers.py +++ b/sickbeard/helpers.py @@ -59,7 +59,7 @@ except ImportError: from sickbeard.exceptions import MultipleShowObjectsException, ex from sickbeard import logger, db, notifiers, clients from sickbeard.common import USER_AGENT, mediaExtensions, subtitleExtensions, cpu_presets, statusStrings, \ - SNATCHED_ANY, DOWNLOADED, ARCHIVED, IGNORED, Quality + SNATCHED_ANY, DOWNLOADED, ARCHIVED, IGNORED, WANTED, SKIPPED, UNAIRED, UNKNOWN, SUBTITLED, FAILED, Quality, Overview from sickbeard import encodingKludge as ek from lib.cachecontrol import CacheControl, caches @@ -1797,3 +1797,34 @@ def clean_data(data): from lib.six.moves.html_parser import HTMLParser return HTMLParser().unescape(data).strip().replace(u'&', u'&') return data + + +def getOverview(epStatus, show_quality, upgrade_once): + + status, quality = Quality.splitCompositeStatus(epStatus) + if ARCHIVED == status: + return Overview.GOOD + if WANTED == status: + return Overview.WANTED + if status in (SKIPPED, IGNORED): + return Overview.SKIPPED + if status in (UNAIRED, UNKNOWN): + return Overview.UNAIRED + if status in [SUBTITLED] + Quality.SNATCHED_ANY + Quality.DOWNLOADED + Quality.FAILED: + + if FAILED == status: + return Overview.WANTED + if status in SNATCHED_ANY: + return Overview.SNATCHED + + void, best_qualities = Quality.splitQuality(show_quality) + # if re-downloads aren't wanted then mark it "good" if there is anything + if not len(best_qualities): + return Overview.GOOD + + min_best, max_best = min(best_qualities), max(best_qualities) + if quality >= max_best \ + or (upgrade_once and + (quality in best_qualities or (None is not min_best and quality > min_best))): + return Overview.GOOD + return Overview.QUAL diff --git a/sickbeard/search_queue.py b/sickbeard/search_queue.py index 2d3b45e9..ccf44bd9 100644 --- a/sickbeard/search_queue.py +++ b/sickbeard/search_queue.py @@ -27,7 +27,7 @@ import sickbeard from sickbeard import db, logger, common, exceptions, helpers, network_timezones, generic_queue, search, \ failed_history, history, ui, properFinder from sickbeard.search import wanted_episodes, get_aired_in_season, set_wanted_aired -from sickbeard.classes import Proper +from sickbeard.classes import Proper, SimpleNamespace from sickbeard.indexers.indexer_config import INDEXER_TVDB @@ -101,7 +101,7 @@ class SearchQueue(generic_queue.GenericQueue): for cur_item in self.queue: if (isinstance(cur_item, (ManualSearchQueueItem, FailedQueueItem)) and (not show or show == str(cur_item.show.indexerid))): - ep_obj_list.append(cur_item.copy()) + ep_obj_list.append(cur_item.base_info()) return ep_obj_list @@ -115,7 +115,7 @@ class SearchQueue(generic_queue.GenericQueue): with self.lock: if self.currentItem and isinstance(self.currentItem, (ManualSearchQueueItem, FailedQueueItem)) \ and (not show or show == str(self.currentItem.show.indexerid)): - return self.currentItem.copy() + return self.currentItem.base_info() def is_backlog_in_progress(self): return self._is_in_progress(BacklogQueueItem) @@ -407,23 +407,60 @@ class ProperSearchQueueItem(generic_queue.QueueItem): self.finish() -class ManualSearchQueueItem(generic_queue.QueueItem): - def __init__(self, show, segment): - generic_queue.QueueItem.__init__(self, 'Manual Search', MANUAL_SEARCH) - self.priority = generic_queue.QueuePriorities.HIGH - self.name = 'MANUAL-%s' % show.indexerid - self.success = None - self.show = show +class BaseSearchQueueItem(generic_queue.QueueItem): + def __init__(self, show, segment, name, action_id=0): + super(BaseSearchQueueItem, self).__init__(name, action_id) self.segment = segment - self.started = None - self.added_dt = None + self.show = show + self.success = None self.snatched_eps = set([]) + def base_info(self): + o = SimpleNamespace() + o.success = self.success + o.show = SimpleNamespace() + o.show.indexer = self.show.indexer + o.show.indexerid = self.show.indexerid + o.show.quality = self.show.quality + o.show.upgrade_once = self.show.upgrade_once + sl = [] + for s in ([self.segment], self.segment)[isinstance(self.segment, list)]: + eo = SimpleNamespace() + eo.episode = s.episode + eo.season = s.season + eo.status = s.status + eo.show = SimpleNamespace() + eo.show.indexer = s.show.indexer + eo.show.indexerid = s.show.indexerid + eo.show.quality = s.show.quality + eo.show.upgrade_once = s.show.upgrade_once + sl.append(eo) + o.segment = sl + + return o + def copy(self, deepcopy_obj=None): if not isinstance(deepcopy_obj, list): deepcopy_obj = [] - deepcopy_obj += ['segment', 'show'] - return super(ManualSearchQueueItem, self).copy(deepcopy_obj) + deepcopy_obj += ['segment'] + same_show = True + if (isinstance(self.segment, list) and getattr(self.segment[0], 'show') is not self.show) \ + or getattr(self.segment, 'show') is not self.show: + same_show = False + deepcopy_obj += ['show'] + n_o = super(BaseSearchQueueItem, self).copy(deepcopy_obj) + if same_show: + n_o.show = (getattr(n_o.segment, 'show'), getattr(n_o.segment[0], 'show'))[isinstance(n_o.segment, list)] + return n_o + + +class ManualSearchQueueItem(BaseSearchQueueItem): + def __init__(self, show, segment): + super(ManualSearchQueueItem, self).__init__(show, segment, 'Manual Search', MANUAL_SEARCH) + self.priority = generic_queue.QueuePriorities.HIGH + self.name = 'MANUAL-%s' % show.indexerid + self.started = None + self.added_dt = None def run(self): generic_queue.QueueItem.run(self) @@ -465,25 +502,15 @@ class ManualSearchQueueItem(generic_queue.QueueItem): self.finish() -class BacklogQueueItem(generic_queue.QueueItem): +class BacklogQueueItem(BaseSearchQueueItem): def __init__(self, show, segment, standard_backlog=False, limited_backlog=False, forced=False, torrent_only=False): - generic_queue.QueueItem.__init__(self, 'Backlog', BACKLOG_SEARCH) + super(BacklogQueueItem, self).__init__(show, segment, 'Backlog', BACKLOG_SEARCH) self.priority = generic_queue.QueuePriorities.LOW self.name = 'BACKLOG-%s' % show.indexerid - self.success = None - self.show = show - self.segment = segment self.standard_backlog = standard_backlog self.limited_backlog = limited_backlog self.forced = forced self.torrent_only = torrent_only - self.snatched_eps = set([]) - - def copy(self, deepcopy_obj=None): - if not isinstance(deepcopy_obj, list): - deepcopy_obj = [] - deepcopy_obj += ['segment', 'show'] - return super(BacklogQueueItem, self).copy(deepcopy_obj) def run(self): generic_queue.QueueItem.run(self) @@ -521,23 +548,13 @@ class BacklogQueueItem(generic_queue.QueueItem): self.finish() -class FailedQueueItem(generic_queue.QueueItem): +class FailedQueueItem(BaseSearchQueueItem): def __init__(self, show, segment): - generic_queue.QueueItem.__init__(self, 'Retry', FAILED_SEARCH) + super(FailedQueueItem, self).__init__(show, segment, 'Retry', FAILED_SEARCH) self.priority = generic_queue.QueuePriorities.HIGH self.name = 'RETRY-%s' % show.indexerid - self.show = show - self.segment = segment - self.success = None self.started = None self.added_dt = None - self.snatched_eps = set([]) - - def copy(self, deepcopy_obj=None): - if not isinstance(deepcopy_obj, list): - deepcopy_obj = [] - deepcopy_obj += ['segment', 'show'] - return super(FailedQueueItem, self).copy(deepcopy_obj) def run(self): generic_queue.QueueItem.run(self) diff --git a/sickbeard/tv.py b/sickbeard/tv.py index 72b496d1..ddd775b4 100644 --- a/sickbeard/tv.py +++ b/sickbeard/tv.py @@ -1615,34 +1615,7 @@ class TVShow(object): return False def getOverview(self, epStatus): - - status, quality = Quality.splitCompositeStatus(epStatus) - if ARCHIVED == status: - return Overview.GOOD - if WANTED == status: - return Overview.WANTED - if status in (SKIPPED, IGNORED): - return Overview.SKIPPED - if status in (UNAIRED, UNKNOWN): - return Overview.UNAIRED - if status in [SUBTITLED] + Quality.SNATCHED_ANY + Quality.DOWNLOADED + Quality.FAILED: - - if FAILED == status: - return Overview.WANTED - if status in SNATCHED_ANY: - return Overview.SNATCHED - - void, best_qualities = Quality.splitQuality(self.quality) - # if re-downloads aren't wanted then mark it "good" if there is anything - if not len(best_qualities): - return Overview.GOOD - - min_best, max_best = min(best_qualities), max(best_qualities) - if quality >= max_best \ - or (self.upgrade_once and - (quality in best_qualities or (None is not min_best and quality > min_best))): - return Overview.GOOD - return Overview.QUAL + return helpers.getOverview(epStatus, self.quality, self.upgrade_once) def __getstate__(self): d = dict(self.__dict__) diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index 0c25e686..c08e28c8 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -2808,12 +2808,7 @@ class Home(MainHandler): progress = 'queued' for thread in queued_items: - if isinstance(thread, sickbeard.search_queue.ManualSearchQueueItem): - ep, uniq_sxe = self.prepare_episode(thread.show, thread.segment, progress) - episodes.append(ep) - seen_eps.add(uniq_sxe) - - elif hasattr(thread, 'segment'): + if hasattr(thread, 'segment'): for ep_obj in thread.segment: ep, uniq_sxe = self.prepare_episode(ep_obj.show, ep_obj, progress) episodes.append(ep) @@ -2824,12 +2819,7 @@ class Home(MainHandler): episode_params = dict(([('searchstate', 'finished'), ('statusoverview', True)], [('searchstate', 'searching'), ('statusoverview', False)])[None is thread.success], retrystate=True) - if isinstance(thread, sickbeard.search_queue.ManualSearchQueueItem): - ep, uniq_sxe = self.prepare_episode(thread.show, thread.segment, **episode_params) - episodes.append(ep) - seen_eps.add(uniq_sxe) - - elif hasattr(thread, 'segment'): + if hasattr(thread, 'segment'): for ep_obj in thread.segment: ep, uniq_sxe = self.prepare_episode(ep_obj.show, ep_obj, **episode_params) episodes.append(ep) @@ -2837,7 +2827,7 @@ class Home(MainHandler): episode_params = dict(searchstate='finished', retrystate=True, statusoverview=True) for thread in finished_items: - if isinstance(thread, sickbeard.search_queue.ManualSearchQueueItem): + if not isinstance(getattr(thread, 'segment'), list): if (not show or show == str(thread.show.indexerid)) and \ (thread.show.indexer, thread.show.indexerid, thread.segment.season, thread.segment.episode) \ not in seen_eps: @@ -2894,7 +2884,8 @@ class Home(MainHandler): retry_statuses = SNATCHED_ANY + [DOWNLOADED, ARCHIVED] ep_data.update(dict(retrystate=sickbeard.USE_FAILED_DOWNLOADS and ep_status in retry_statuses)) if statusoverview: - ep_data.update(dict(statusoverview=Overview.overviewStrings[show.getOverview(ep.status)])) + ep_data.update(dict(statusoverview=Overview.overviewStrings[ + helpers.getOverview(ep.status, show.quality, show.upgrade_once)])) return ep_data, (show.indexer, show.indexerid, ep.season, ep.episode) From ac6d1f69d963d806e50025a1b6e8dae37c1979a0 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Tue, 2 Oct 2018 20:03:01 +0100 Subject: [PATCH 4/4] Change refactor manual search processing. Change reduce browser I/O on displayShow. Fix displayShow bug where click holding on a season btn and then dragging away leaves 50% white. Devel: Change sets and lists are assigned by reference, so snatched_eps are deep copied in base_info(). Change comment out BaseSearchQueueItem::copy() as deprecated for base_info(). Change improve ajax consumer to reduce polling. Simplify SimpleNamespace init in base_info(). Use base info instead of thread object for MANUAL_SEARCH_HISTORY in (ManualSearchQueueItem + FailedQueueItem) to streamline the finished search processing in webserve, this means add_dt has to be moved to BaseSearchQueueItem for base_info(). SimpleNameSpace Ref error is now in PYC, not sure if this is valid tho. --- CHANGES.md | 4 + gui/slick/css/dark.css | 1 + gui/slick/css/style.css | 13 + gui/slick/interfaces/default/displayShow.tmpl | 4 +- gui/slick/interfaces/default/episodeView.tmpl | 23 +- .../interfaces/default/inc_displayShow.tmpl | 6 +- gui/slick/js/ajaxEpSearch.js | 389 ++++++++++-------- gui/slick/js/displayShow.js | 2 +- sickbeard/search_queue.py | 75 ++-- sickbeard/webserve.py | 78 ++-- 10 files changed, 319 insertions(+), 276 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 51779e36..2604399d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -24,6 +24,10 @@ * Add search results sort by oldest aired * Change requirements.txt Cheetah >= 3.1.0 * Add Snowfl torrent provider +* Fix manual search button on displayShow and episode view page +* Change feedback result of manual search on the clicked button image/tooltip +* Change reduce browser I/O on displayShow +* Fix displayShow bug where click holding on a season btn and then dragging away leaves 50% white [develop changelog] diff --git a/gui/slick/css/dark.css b/gui/slick/css/dark.css index be30683a..87f9bc40 100644 --- a/gui/slick/css/dark.css +++ b/gui/slick/css/dark.css @@ -1060,6 +1060,7 @@ fieldset[disabled] .navbar-default .btn-link:focus{ outline:thin dotted #333; outline:5px auto -webkit-focus-ring-color; outline-offset:-2px; + background-position:0; color:#ddd } diff --git a/gui/slick/css/style.css b/gui/slick/css/style.css index c15fa3f9..19e48ea9 100644 --- a/gui/slick/css/style.css +++ b/gui/slick/css/style.css @@ -2130,6 +2130,19 @@ td.col-search{ width:46px } +td.col-search{ + font-size:10px +} + +.ep-search, +.ep-retry, +.ep-search img[src=""], +.ep-retry img[src=""]{ + display:inline-block; + width:16px; + height:16px +} + #testRenameTable tbody td.col-checkbox, #testRenameTable tbody td.col-ep{width:1%;vertical-align:middle} #testRenameTable tbody td.col-name{ diff --git a/gui/slick/interfaces/default/displayShow.tmpl b/gui/slick/interfaces/default/displayShow.tmpl index d28eb872..9bbd09e2 100644 --- a/gui/slick/interfaces/default/displayShow.tmpl +++ b/gui/slick/interfaces/default/displayShow.tmpl @@ -50,11 +50,11 @@ -
+
#if $has_art diff --git a/gui/slick/interfaces/default/episodeView.tmpl b/gui/slick/interfaces/default/episodeView.tmpl index 19a30db6..768a4b3f 100644 --- a/gui/slick/interfaces/default/episodeView.tmpl +++ b/gui/slick/interfaces/default/episodeView.tmpl @@ -17,6 +17,7 @@ #set $restart = 'Restart SickGear for new features on this page' #set $show_message = (None, $restart)[not $varExists('fanart')] #set global $page_body_attr = 'episode-view" class="' + $css +#set theme_suffix = ('', '-dark')['dark' == $sg_str('THEME_NAME', 'dark')] ## #import os.path #include $os.path.join($sg_str('PROG_DIR'), 'gui/slick/interfaces/default/inc_top.tmpl') @@ -176,7 +177,9 @@ .asc{border-top:0; border-bottom:8px solid} .desc{border-top:8px solid; border-bottom:0} #end if +.bfr{position:absolute;left:-999px;top:-999px}.bfr img,img.spinner,img.queued,img.search{display:inline-block;width:16px;height:16px}img.spinner{background:url(${sbRoot}/images/loading16${theme_suffix}.gif) no-repeat 0 0}img.queued{background:url(${sbRoot}/images/queued.png) no-repeat 0 0}img.search{background:url(${sbRoot}/images/search16.png) no-repeat 0 0} +
#if $show_message
@@ -293,9 +296,9 @@ }); $(document).ready(function(){ - - sortList = [[$table_sort_header_codes[$sort], 0]]; - +#end raw + var sortList = [[$table_sort_header_codes[$sort], 0]]; +#raw $('#showListTable:has(tbody tr)').tablesorter({ widgets: ['stickyHeaders'], sortList: sortList, @@ -381,9 +384,10 @@ #end if #end if - #set $show_id = '%s_%sx%s' % (str($cur_result['showid']), str($cur_result['season']), str($cur_result['episode'])) + #set $show_id = '%s_%sx%s' % ($cur_result['showid'], $cur_result['season'], $cur_result['episode']) + #set $id_sxe = '%s_%s' % ($cur_result['indexer'], $show_id) - + ## forced to use a div to wrap airdate, the column sort went crazy with a span
$sbdatetime.sbdatetime.sbfdatetime($cur_result['localtime']).decode($sickbeard.SYS_ENCODING)
$cur_result['localtime'].strftime('%Y%m%d%H%M') @@ -428,7 +432,7 @@ - [search] + [search] @@ -455,7 +459,7 @@ -
+ #set $id_sxe = '%s_%s_%sx%s' % ($cur_result['indexer'], $cur_result['showid'], $cur_result['season'], $cur_result['episode']) +
@@ -614,7 +619,7 @@ [$sickbeard.indexerApi(INDEXER_IMDB).name] #end if $sickbeard.indexerApi($cur_indexer).name - [search] + [search] diff --git a/gui/slick/interfaces/default/inc_displayShow.tmpl b/gui/slick/interfaces/default/inc_displayShow.tmpl index a65e1493..a96c9c53 100644 --- a/gui/slick/interfaces/default/inc_displayShow.tmpl +++ b/gui/slick/interfaces/default/inc_displayShow.tmpl @@ -101,7 +101,7 @@ #slurp #set $curStatus, $curQuality = $Quality.splitCompositeStatus(int($ep['status'])) #if Quality.NONE != $curQuality - + #else #end if @@ -109,9 +109,9 @@ #if 0 != int($ep['season']) #set $status = $Quality.splitCompositeStatus(int($ep['status']))[0] #if ($status in $SNATCHED_ANY + [$DOWNLOADED, $ARCHIVED]) and $sg_var('USE_FAILED_DOWNLOADS') - retry + retry #else - search + search #end if #end if #slurp diff --git a/gui/slick/js/ajaxEpSearch.js b/gui/slick/js/ajaxEpSearch.js index aad1f07f..a7343d20 100644 --- a/gui/slick/js/ajaxEpSearch.js +++ b/gui/slick/js/ajaxEpSearch.js @@ -1,194 +1,235 @@ -var search_status_url = sbRoot + '/home/getManualSearchStatus'; +/** @namespace $.SickGear.Root */ +/** @namespace data.episodes */ +/** @namespace ep.showindexer */ +/** @namespace ep.showindexid */ +/** @namespace ep.season */ +/** @namespace ep.episode */ +/** @namespace ep.searchstate */ +/** @namespace ep.status */ +/** @namespace ep.quality */ +/** @namespace ep.retrystate */ +/** @namespace ep.statusoverview */ + +var dev = !1, + logInfo = dev ? console.info.bind(window.console) : function() {}, + logErr = dev ? console.error.bind(window.console) : function() {}; + PNotify.prototype.options.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.gif'; - var queuedImage = 'queued.png'; - var searchImage = 'search16.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.prop('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.prop('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'); - var imgParent = img.parent(); - if (ep.retrystatus) { - imgParent.attr('class','epRetry'); - imgParent.attr('href', imgParent.attr('href').replace('/home/searchEpisode?', '/home/retryEpisode?')); - img.attr('title','Retry download'); - img.prop('alt', 'retry download'); - } - else { - imgParent.attr('class','epSearch'); - imgParent.attr('href', imgParent.attr('href').replace('/home/retryEpisode?', '/home/searchEpisode?')); - img.attr('title','Manual search'); - img.prop('alt', 'manual search'); - } - img.attr('src',sbRoot+'/images/' + searchImage); - enableLink(el); - - // Update Status and Quality - parent.closest('tr').removeClass('skipped wanted qual good unaired snatched').addClass(ep.statusoverview); - var rSearchTerm = /(\w+)\s\((.+?)\)/; - HtmlContent = ep.status.replace(rSearchTerm,"$1"+' '+"$2"+''); - - } - // update the status column if it exists - parent.siblings('.col-status').html(HtmlContent) - - } - - }); -} - $(document).ready(function () { - check_manual_searches(); + ajaxConsumer.checkManualSearches(); }); -function enableLink(el) { - el.on('click.disabled', false); - el.attr('enableClick', '1'); - el.fadeTo("fast", 1) +var ajaxConsumer = function () { + var that = this; + that.timeoutId = 0; + that.pollInterval = 0; + logInfo('init ajaxConsumer'); + + return { + checkManualSearches : function () { + logInfo('ajaxConsumer.checkManualSearches()'); + var showId = $('#showID').val(); + $.getJSON({ + url: $.SickGear.Root + '/home/search_q_progress' + (/undefined/i.test(showId) ? '' : '?show=' + showId), + timeout: 15000 // timeout request after 15 secs + }) + .done(function (data) { + logInfo('search_q_progress.success(data)', data); + if (!data.episodes || 0 === data.episodes.length) { + imgRestore(); + } + // using 5s as a reasonable max. when updating images from historical statuses after a page refresh + that.pollInterval = data.episodes && data.episodes.length + ? (uiUpdateComplete(data) ? 5000 : 1000) : 10000; // 10000/0 + }) + .fail(function () { + logErr('search_q_progress.error()'); + that.pollInterval = 30000; + }) + .always(function (jqXHR, textStatus) { + logInfo('search_q_progress.complete(textStatus)', '\'' + textStatus + '\'.'); + clearTimeout(that.timeoutId); + if (that.pollInterval) + that.timeoutId = setTimeout(ajaxConsumer.checkManualSearches, that.pollInterval); + logInfo(that.pollInterval ? '^-- ' + that.pollInterval/1000 + 's to next work' : '^-- no more work'); + logInfo('===='); + }); + + } + }; +}(); + +function uiUpdateComplete(data) { + var isFinished = !0; + $.each(data.episodes, function (name, ep) { + + var sxe = ep.season + 'x' + ep.episode, + displayShow$ = $('#' + sxe).closest('tr'), + episodeView$ = $('[data-show-id="' + ep.showindexer + '_' + ep.showindexid + '_' + sxe + '"]'), + link$ = (displayShow$.length ? displayShow$ : episodeView$).find('.ep-search, .ep-retry'), + uiOptions = $.ajaxEpSearch.defaults; + + logInfo('^-- data item', name, ep.searchstate, ep.showindexid, sxe, ep.statusoverview); + + if (link$.length) { + var htmlContent = '', imgTip, imgCls; + + switch (ep.searchstate) { + case 'searching': + isFinished = !1; + imgUpdate(link$, 'Searching', uiOptions.loadingImage); + disableLink(link$); + htmlContent = '[' + ep.searchstate + ']'; + break; + case 'queued': + isFinished = !1; + imgUpdate(link$, 'Queued', uiOptions.queuedImage); + disableLink(link$); + htmlContent = '[' + ep.searchstate + ']'; + break; + case 'finished': + var attrName = !!getAttr(link$, 'href') ? 'href' : 'data-href', href = getAttr(link$, attrName); + if (ep.retrystate) { + imgTip = 'Click to retry download'; + link$.attr('class', 'ep-retry').attr(attrName, href.replace('search', 'retry')); + } else { + imgTip = 'Click for manual search'; + link$.attr('class', 'ep-search').attr(attrName, href.replace('retry', 'search')); + } + if (/good/i.test(ep.statusoverview)) { + imgCls = uiOptions.searchImage; + } else if (/snatched/i.test(ep.statusoverview)) { + imgCls = uiOptions.imgYes; + } else { + imgTip = 'Last manual search failed. Click to try again'; + imgCls = uiOptions.imgNo; + } + imgUpdate(link$, imgTip, imgCls); + enableLink(link$); + + // update row status + if (ep.statusoverview) { + link$.closest('tr') + .removeClass('skipped wanted qual good unaired snatched') + .addClass(ep.statusoverview); + } + // update quality text for status column + var rSearchTerm = /(\w+)\s\((.+?)\)/; + htmlContent = ep.status.replace(rSearchTerm, + '$1' + ' ' + '$2' + ''); + + // remove backed vars + link$.removeAttr('data-status data-imgclass'); + } + + // update the status area + link$.closest('.col-search').siblings('.col-status').html(htmlContent); + } + }); + return isFinished; } -function disableLink(el) { - el.off('click.disabled'); - el.attr('enableClick', '0'); - el.fadeTo("fast", .5) +function enableLink(el$) { + el$.attr('href', el$.attr('data-href')).removeAttr('data-href').fadeTo('fast', 1); } -(function(){ +function disableLink(el$) { + el$.attr('data-href', el$.attr('href')).removeAttr('href').fadeTo('fast', .7); +} + +function getAttr(el$, name) { + return el$.is('[' + name + ']') ? el$.attr(name) : !1; +} + +function imgUpdate(link$, tip, cls) { + link$.find('img').attr('src', '').attr('title', tip).prop('alt', '') + .removeClass('spinner queued search no yes').addClass(cls); +} + +function imgRestore() { + $('a[data-status]').each(function() { + $(this).closest('.col-search').siblings('.col-status').html($(this).attr('data-status')); + imgUpdate($(this), + getAttr($(this), 'data-imgtitle'), + getAttr($(this), 'data-imgclass') || $.ajaxEpSearch.defaults.searchImage); + $(this).removeAttr('data-status data-imgclass data-imgtitle'); + }); +} + +(function() { $.ajaxEpSearch = { - defaults: { - size: 16, - colorRow: false, - loadingImage: 'loading16.gif', - queuedImage: 'queued.png', - noImage: 'no16.png', - yesImage: 'yes16.png' - } + defaults: { + size: 16, + colorRow: !1, + loadingImage: 'spinner', + queuedImage: 'queued', + searchImage: 'search', + imgNo: 'no', + imgYes: 'yes' + } }; - $.fn.ajaxEpSearch = function(options){ - options = $.extend({}, $.ajaxEpSearch.defaults, options); - - $('.epSearch, .epRetry').click(function(event){ + $.fn.ajaxEpSearch = function(uiOptions) { + uiOptions = $.extend( {}, $.ajaxEpSearch.defaults, uiOptions); + + $('.ep-search, .ep-retry').on('click', function(event) { event.preventDefault(); - - // 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.prop('alt',''); - img.attr('src',sbRoot+'/images/' + options.loadingImage); - - - $.getJSON($(this).attr('href'), function(data){ - - // if they failed then just put the red X - if (data.result == 'failure') { - img_name = options.noImage; - img_result = 'failed'; + logInfo(($(this).hasClass('ep-search') ? 'Search' : 'Retry') + ' clicked'); - // if the snatch was successful then apply the corresponding class and fill in the row appropriately - } else { - img_name = options.loadingImage; - img_result = 'success'; - // color the row - if (options.colorRow) - parent.parent().removeClass('skipped wanted qual good unaired').addClass('snatched'); - // applying the quality class - var rSearchTerm = /(\w+)\s\((.+?)\)/; - HtmlContent = data.result.replace(rSearchTerm,"$1"+' '+"$2"+''); - // update the status column if it exists - parent.siblings('.col-status').html(HtmlContent) - // Only if the queing was succesfull, disable the onClick event of the loading image - disableLink(link); - } + // check if we have disabled the click + if (!!getAttr($(this), 'data-href')) { + logInfo('Already queued, not downloading!'); + return !1; + } - // put the corresponding image as the result of queuing of the manual search - img.attr('title',img_result); - img.prop('alt',img_result); - img.attr('height', options.size); - img.attr('src',sbRoot+"/images/"+img_name); - }); - // - - // don't follow the link - return false; + if ($(this).hasClass('ep-retry') + && !confirm('Mark download as bad and retry?')) { + return !1; + } + + var link$ = $(this), img$ = link$.find('img'), img = ['Failed', uiOptions.imgNo], imgCls; + // backup ui vars + if (link$.closest('.col-search') && link$.closest('.col-search').siblings('.col-status')) { + link$.attr('data-status', link$.closest('.col-search').siblings('.col-status').html().trim()); + } + link$.attr('data-imgtitle', getAttr(img$, 'title')); + if (imgCls = getAttr(img$, 'class')) { + link$.attr('data-imgclass', imgCls.trim()); + } + + imgUpdate(link$, 'Loading', uiOptions.loadingImage); + + $.getJSON({url: $(this).attr('href'), timeout: 15000}) + .done(function(data) { + logInfo('getJSON() data...', data); + + // if failed, replace success/queued with initiated red X/No + if ('failure' !== data.result) { + // otherwise, queued successfully + + // update ui status + link$.closest('.col-search').siblings('.col-status').html('[' + data.result + ']'); + + // prevent further interaction + disableLink(link$); + + img = 'queueing' === data.result + ? ['Queueing', uiOptions.queuedImage] + : ['Searching', uiOptions.loadingImage]; + } + + // update ui image + imgUpdate(link$, img[0], img[1]); + ajaxConsumer.checkManualSearches(); + }) + .fail(function() { imgRestore(); }); + + // prevent following the clicked link + return !1; }); - } + }; })(); - diff --git a/gui/slick/js/displayShow.js b/gui/slick/js/displayShow.js index 52dde687..94ca22f8 100644 --- a/gui/slick/js/displayShow.js +++ b/gui/slick/js/displayShow.js @@ -173,7 +173,7 @@ $(document).ready(function() { qTips($('.addQTip')); function table_init(table$) { - $('#sbRoot').ajaxEpSearch({'colorRow': true}); + $('#sbRoot').ajaxEpSearch(); $('#sbRoot').ajaxEpSubtitlesSearch(); if ($.SickGear.config.useFuzzy) { diff --git a/sickbeard/search_queue.py b/sickbeard/search_queue.py index ccf44bd9..d73e848f 100644 --- a/sickbeard/search_queue.py +++ b/sickbeard/search_queue.py @@ -22,6 +22,7 @@ import traceback import threading import datetime import re +import copy import sickbeard from sickbeard import db, logger, common, exceptions, helpers, network_timezones, generic_queue, search, \ @@ -91,7 +92,7 @@ class SearchQueue(generic_queue.GenericQueue): def get_queued_manual(self, show): """ - Returns None or List of copies of all show related items in manual or failed queue + Returns None or List of base info items of all show related items in manual or failed queue :param show: show indexerid or None for all q items :type show: String or None :return: List with 0 or more items @@ -107,10 +108,10 @@ class SearchQueue(generic_queue.GenericQueue): def get_current_manual_item(self, show): """ - Returns a static copy of the currently active manual search item + Returns a base info item of the currently active manual search item :param show: show indexerid or None for all q items :type show: String or None - :return: copy of ManualSearchQueueItem or FailedQueueItem or None + :return: base info item of ManualSearchQueueItem or FailedQueueItem or None """ with self.lock: if self.currentItem and isinstance(self.currentItem, (ManualSearchQueueItem, FailedQueueItem)) \ @@ -412,46 +413,38 @@ class BaseSearchQueueItem(generic_queue.QueueItem): super(BaseSearchQueueItem, self).__init__(name, action_id) self.segment = segment self.show = show + self.added_dt = None self.success = None self.snatched_eps = set([]) def base_info(self): - o = SimpleNamespace() - o.success = self.success - o.show = SimpleNamespace() - o.show.indexer = self.show.indexer - o.show.indexerid = self.show.indexerid - o.show.quality = self.show.quality - o.show.upgrade_once = self.show.upgrade_once - sl = [] - for s in ([self.segment], self.segment)[isinstance(self.segment, list)]: - eo = SimpleNamespace() - eo.episode = s.episode - eo.season = s.season - eo.status = s.status - eo.show = SimpleNamespace() - eo.show.indexer = s.show.indexer - eo.show.indexerid = s.show.indexerid - eo.show.quality = s.show.quality - eo.show.upgrade_once = s.show.upgrade_once - sl.append(eo) - o.segment = sl + return SimpleNamespace( + success=self.success, + added_dt=self.added_dt, + snatched_eps=copy.deepcopy(self.snatched_eps), + show=SimpleNamespace( + indexer=self.show.indexer, indexerid=self.show.indexerid, + quality=self.show.quality, upgrade_once=self.show.upgrade_once), + segment=[SimpleNamespace( + season=s.season, episode=s.episode, status=s.status, + show=SimpleNamespace( + indexer=s.show.indexer, indexerid=s.show.indexerid, + quality=s.show.quality, upgrade_once=s.show.upgrade_once + )) for s in ([self.segment], self.segment)[isinstance(self.segment, list)]]) - return o - - def copy(self, deepcopy_obj=None): - if not isinstance(deepcopy_obj, list): - deepcopy_obj = [] - deepcopy_obj += ['segment'] - same_show = True - if (isinstance(self.segment, list) and getattr(self.segment[0], 'show') is not self.show) \ - or getattr(self.segment, 'show') is not self.show: - same_show = False - deepcopy_obj += ['show'] - n_o = super(BaseSearchQueueItem, self).copy(deepcopy_obj) - if same_show: - n_o.show = (getattr(n_o.segment, 'show'), getattr(n_o.segment[0], 'show'))[isinstance(n_o.segment, list)] - return n_o + # def copy(self, deepcopy_obj=None): + # if not isinstance(deepcopy_obj, list): + # deepcopy_obj = [] + # deepcopy_obj += ['segment'] + # same_show = True + # if (isinstance(self.segment, list) and getattr(self.segment[0], 'show') is not self.show) \ + # or getattr(self.segment, 'show') is not self.show: + # same_show = False + # deepcopy_obj += ['show'] + # n_o = super(BaseSearchQueueItem, self).copy(deepcopy_obj) + # if same_show: + # n_o.show = (getattr(n_o.segment, 'show'), getattr(n_o.segment[0], 'show'))[isinstance(n_o.segment, list)] + # return n_o class ManualSearchQueueItem(BaseSearchQueueItem): @@ -460,7 +453,6 @@ class ManualSearchQueueItem(BaseSearchQueueItem): self.priority = generic_queue.QueuePriorities.HIGH self.name = 'MANUAL-%s' % show.indexerid self.started = None - self.added_dt = None def run(self): generic_queue.QueueItem.run(self) @@ -494,7 +486,7 @@ class ManualSearchQueueItem(BaseSearchQueueItem): finally: # Keep a list with the last executed searches - fifo(MANUAL_SEARCH_HISTORY, self) + fifo(MANUAL_SEARCH_HISTORY, self.base_info()) if self.success is None: self.success = False @@ -554,7 +546,6 @@ class FailedQueueItem(BaseSearchQueueItem): self.priority = generic_queue.QueuePriorities.HIGH self.name = 'RETRY-%s' % show.indexerid self.started = None - self.added_dt = None def run(self): generic_queue.QueueItem.run(self) @@ -596,7 +587,7 @@ class FailedQueueItem(BaseSearchQueueItem): finally: # Keep a list with the last executed searches - fifo(MANUAL_SEARCH_HISTORY, self) + fifo(MANUAL_SEARCH_HISTORY, self.base_info()) if self.success is None: self.success = False diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index c08e28c8..b9206fa6 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -2797,69 +2797,57 @@ class Home(MainHandler): seen_eps = set([]) # Queued searches - queued_items = sickbeard.searchQueueScheduler.action.get_queued_manual(show) + queued = sickbeard.searchQueueScheduler.action.get_queued_manual(show) # Active search - active_item = sickbeard.searchQueueScheduler.action.get_current_manual_item(show) + active = sickbeard.searchQueueScheduler.action.get_current_manual_item(show) # Finished searches sickbeard.search_queue.remove_old_fifo(sickbeard.search_queue.MANUAL_SEARCH_HISTORY) - finished_items = sickbeard.search_queue.MANUAL_SEARCH_HISTORY + results = sickbeard.search_queue.MANUAL_SEARCH_HISTORY - progress = 'queued' - for thread in queued_items: - if hasattr(thread, 'segment'): - for ep_obj in thread.segment: - ep, uniq_sxe = self.prepare_episode(ep_obj.show, ep_obj, progress) - episodes.append(ep) - seen_eps.add(uniq_sxe) + for item in filter(lambda q: hasattr(q, 'segment'), queued): + for ep_base in item.segment: + ep, uniq_sxe = self.prepare_episode(ep_base, 'queued') + episodes.append(ep) + seen_eps.add(uniq_sxe) - if active_item: - thread = active_item + if active and hasattr(active, 'segment'): episode_params = dict(([('searchstate', 'finished'), ('statusoverview', True)], - [('searchstate', 'searching'), ('statusoverview', False)])[None is thread.success], + [('searchstate', 'searching'), ('statusoverview', False)])[None is active.success], retrystate=True) - if hasattr(thread, 'segment'): - for ep_obj in thread.segment: - ep, uniq_sxe = self.prepare_episode(ep_obj.show, ep_obj, **episode_params) - episodes.append(ep) - seen_eps.add(uniq_sxe) + for ep_base in active.segment: + ep, uniq_sxe = self.prepare_episode(ep_base, **episode_params) + episodes.append(ep) + seen_eps.add(uniq_sxe) episode_params = dict(searchstate='finished', retrystate=True, statusoverview=True) - for thread in finished_items: - if not isinstance(getattr(thread, 'segment'), list): - if (not show or show == str(thread.show.indexerid)) and \ - (thread.show.indexer, thread.show.indexerid, thread.segment.season, thread.segment.episode) \ - not in seen_eps: - ep, uniq_sxe = self.prepare_episode(thread.show, thread.segment, **episode_params) - episodes.append(ep) - seen_eps.add(uniq_sxe) + for item in filter(lambda r: hasattr(r, 'segment') and (not show or show == str(r.show.indexerid)), results): + for ep_base in filter( + lambda e: (e.show.indexer, e.show.indexerid, e.season, e.episode) not in seen_eps, item.segment): + ep, uniq_sxe = self.prepare_episode(ep_base, **episode_params) + episodes.append(ep) + seen_eps.add(uniq_sxe) - # These are only Failed Downloads/Retry SearchThreadItems.. lets loop through the segment/episodes - elif hasattr(thread, 'segment') and show == str(thread.show.indexerid): - for ep_obj in thread.segment: - if (ep_obj.show.indexer, ep_obj.show.indexerid, ep_obj.season, ep_obj.episode) not in seen_eps: - ep, uniq_sxe = self.prepare_episode(ep_obj.show, ep_obj, **episode_params) - episodes.append(ep) - seen_eps.add(uniq_sxe) - - for snatched in filter(lambda v: v not in seen_eps, thread.snatched_eps): - ep_obj = thread.show.getEpisode(season=snatched[2], episode=snatched[3]) - ep, uniq_sxe = self.prepare_episode(thread.show, ep_obj, **episode_params) + for snatched in filter(lambda s: (s not in seen_eps), item.snatched_eps): + try: + show = helpers.find_show_by_id(sickbeard.showList, dict({snatched[0]: snatched[1]})) + ep_obj = show.getEpisode(season=snatched[2], episode=snatched[3]) + except (StandardError, Exception): + continue + ep, uniq_sxe = self.prepare_episode(ep_obj, **episode_params) episodes.append(ep) seen_eps.add(uniq_sxe) return json.dumps(dict(episodes=episodes)) @staticmethod - def prepare_episode(show, ep, searchstate, retrystate=False, statusoverview=False): + def prepare_episode(ep, searchstate, retrystate=False, statusoverview=False): """ Prepare episode data and its unique id - :param show: Show object - :type show: TVShow object - :param ep: Episode object - :type ep: TVEpisode object + :param ep: Episode structure containing the show that it relates to + :type ep: TVEpisode object or Episode Base Namespace :param searchstate: Progress of search :type searchstate: string :param retrystate: True to add retrystate to data @@ -2877,7 +2865,7 @@ class Home(MainHandler): quality_class = qualityPresetStrings[x] break - ep_data = dict(showindexer=show.indexer, showindexid=show.indexerid, + ep_data = dict(showindexer=ep.show.indexer, showindexid=ep.show.indexerid, season=ep.season, episode=ep.episode, quality=quality_class, searchstate=searchstate, status=statusStrings[ep.status]) if retrystate: @@ -2885,9 +2873,9 @@ class Home(MainHandler): ep_data.update(dict(retrystate=sickbeard.USE_FAILED_DOWNLOADS and ep_status in retry_statuses)) if statusoverview: ep_data.update(dict(statusoverview=Overview.overviewStrings[ - helpers.getOverview(ep.status, show.quality, show.upgrade_once)])) + helpers.getOverview(ep.status, ep.show.quality, ep.show.upgrade_once)])) - return ep_data, (show.indexer, show.indexerid, ep.season, ep.episode) + return ep_data, (ep.show.indexer, ep.show.indexerid, ep.season, ep.episode) def searchEpisodeSubtitles(self, show=None, season=None, episode=None): # retrieve the episode object and fail if we can't get one
#if $SUBTITLED == $curStatus##else#$statusStrings[$curStatus].replace('Downloaded', '')#end if# $Quality.get_quality_ui($curQuality)#if $SUBTITLED == $curStatus##else#$statusStrings[$curStatus].replace('Downloaded', '')#end if# #if 'Unknown' != $statusStrings[$curStatus]#$Quality.get_quality_ui($curQuality)#end if#$statusStrings[$curStatus].replace('SD DVD', 'SD DVD/BR/BD')