From 11711b22388a579d6a5ab01f5bf6497675f385b9 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Sun, 27 Aug 2017 17:33:32 +0100 Subject: [PATCH] Change show search, add options to choose order of search results. Add option to sort search results by 'A to Z' or 'First aired'. Add option to sort search results by 'Relevancy' using Fuzzywuzzy lib. Change search result anchor text uses SORT_ARTICLE setting for display. Change add server side search result ordering. Change use iteritems instead of items to improve performance. Change existing shows in DB are no longer selectable in result list. Change add image to search result item hover over. Change improve image load speed on browse Trakt/IMDb/AniDB pages. Change PEP8. --- CHANGES.md | 10 ++ gui/slick/css/dark.css | 2 + gui/slick/css/light.css | 2 + gui/slick/css/style.css | 15 +- gui/slick/images/image-light.png | Bin 0 -> 215 bytes .../interfaces/default/home_newShow.tmpl | 6 +- gui/slick/js/newShow.js | 137 +++++++++++++++--- sickbeard/__init__.py | 7 + sickbeard/helpers.py | 14 +- sickbeard/webserve.py | 93 +++++++++--- 10 files changed, 233 insertions(+), 53 deletions(-) create mode 100644 gui/slick/images/image-light.png diff --git a/CHANGES.md b/CHANGES.md index f3106eef..12b95882 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -89,6 +89,16 @@ * Add new parameter 'poster' to indexer api * Add optional tvdb_api load season image: lINDEXER_API_PARMS['seasons'] = True * Add optional tvdb_api load season wide image: lINDEXER_API_PARMS['seasonwides'] = True +* Add Fuzzywuzzy 0.15.1 to sort search results +* Change remove search results filtering from tv info source +* Change suppress startup warnings for Fuzzywuzzy and Cheetah libs +* Change show search, add options to choose order of search results +* Add option to sort search results by 'A to Z' or 'First aired' +* Add option to sort search results by 'Relevancy' using Fuzzywuzzy lib +* Change search result anchor text uses SORT_ARTICLE setting for display +* Change existing shows in DB are no longer selectable in result list +* Change add image to search result item hover over +* Change improve image load speed on browse Trakt/IMDb/AniDB pages [develop changelog] diff --git a/gui/slick/css/dark.css b/gui/slick/css/dark.css index 58487684..89657f86 100644 --- a/gui/slick/css/dark.css +++ b/gui/slick/css/dark.css @@ -1289,6 +1289,7 @@ input sizing (for config pages) ========================================================================== */ .showlist-select optgroup, +#results-sortby optgroup, #pickShow optgroup, #showfilter optgroup, #showsort optgroup, @@ -1298,6 +1299,7 @@ input sizing (for config pages) } .showlist-select optgroup option, +#results-sortby optgroup option, #pickShow optgroup option, #showfilter optgroup option, #showsort optgroup option, diff --git a/gui/slick/css/light.css b/gui/slick/css/light.css index d031ef28..c057dd3c 100644 --- a/gui/slick/css/light.css +++ b/gui/slick/css/light.css @@ -1254,6 +1254,7 @@ input sizing (for config pages) ========================================================================== */ .showlist-select optgroup, +#results-sortby optgroup, #pickShow optgroup, #showfilter optgroup, #showsort optgroup, @@ -1263,6 +1264,7 @@ input sizing (for config pages) } .showlist-select optgroup option, +#results-sortby optgroup option, #pickShow optgroup option, #showfilter optgroup option, #showsort optgroup option, diff --git a/gui/slick/css/style.css b/gui/slick/css/style.css index 68fbfda4..8eca368a 100644 --- a/gui/slick/css/style.css +++ b/gui/slick/css/style.css @@ -1113,11 +1113,16 @@ div.formpaginate{ margin-left:10px } -.stepDiv #searchResults div{ +.stepDiv #searchResults .results-item{ + width:100%; line-height:1.7 } -.stepDiv #searchResults div .exists-db{ +.stepDiv #searchResults .results-item input[disabled=disabled]{ + visibility:hidden +} + +.stepDiv #searchResults .results-item .exists-db{ font-weight:800; font-style:italic } @@ -1126,6 +1131,11 @@ div.formpaginate{ margin-right:6px } +a span.article, +a:hover span.article{ + color:#2f4799 +} + .stepone-result-title{ font-weight:600; margin-left:10px @@ -2785,6 +2795,7 @@ config*.tmpl padding-top:10px } +select .selected-text, select .selected{ font-weight:700; color:#888 diff --git a/gui/slick/images/image-light.png b/gui/slick/images/image-light.png new file mode 100644 index 0000000000000000000000000000000000000000..59e658b170ced823a0ecc807fe32173f9d92c35c GIT binary patch literal 215 zcmeAS@N?(olHy`uVBq!ia0vp^0zk~q!3HGX7W?Z1sd=6*jv*T7lM^H|4s;y(=ie76 zmNN1G|Nq{?OD9kKsA*O>gZr8D&94<_e)Ri)uxDmIT(73KtlgvN4-XH|Z%GM>FPu^b z&Y#~usj>0l{uwhO%qKnj9PsmdfL$K*zyJUBKm7k+zY=KL=S7)q4?nl;Il%vQzm2hR zF~h91l%A^$wQOu{zYSc^oH@hJDd|vXm9WS}H7JLHd*w<2P17KCV@8Ie)3z>0j?7&F PbSZ - var show_scene_maps = ${show_scene_maps} + var show_scene_maps = ${show_scene_maps}, + config = { + sortArticle: #echo ['!1','!0'][$sg_var('SORT_ARTICLE')]#, + resultsSortby: '#echo $sg_str('RESULTS_SORTBY', 'rel')#' + } diff --git a/gui/slick/js/newShow.js b/gui/slick/js/newShow.js index f7dc5ead..0a5da2c8 100644 --- a/gui/slick/js/newShow.js +++ b/gui/slick/js/newShow.js @@ -1,3 +1,5 @@ +/** @namespace config.sortArticle */ +/** @namespace config.resultsSortby */ $(document).ready(function () { function populateLangSelect() { @@ -71,59 +73,146 @@ $(document).ready(function () { $('#searchResults').empty().html('search timed out, try again or try another database'); }, success: function (data) { - var resultStr = '', checked = '', rowType, row = 0; + var resultStr = '', attrs = '', checked = !1, rowType, row = 0, srcState = ''; if (0 === data.results.length) { resultStr += 'Sorry, no results found. Try a different search.'; } else { - var idxSrcDB = 0, idxSrcDBId = 1, idxSrcUrl = 2, idxShowID = 3, idxTitle = 4, idxTitleHtml = 5, - idxDate = 6, idxNetwork = 7, idxGenres = 8, idxOverview = 9; - $.each(data.results, function (index, obj) { - checked = (0 == row ? ' checked' : ''); - rowType = (0 == row % 2 ? '' : ' class="alt"'); + var result = { + SrcName: 0, isInDB: 1, SrcId: 2, SrcDBId: 3, SrcUrl: 4, ShowID: 5, Title: 6, TitleHtml: 7, + Aired: 8, Network: 9, Genre: 10, Overview: 11, RelSort: 12, DateSort: 13, AzSort: 14, ImgUrl: 15 + }; + $.each(data.results, function (index, item) { + attrs = (!0 === item[result.isInDB] ? ' disabled="disabled"' : (!0 === checked ? '' : ' checked')); + checked = (' checked' === attrs) ? !0 : checked; + rowType = (0 == row % 2 ? '' : ' alt'); row++; - var display_show_name = cleanseText(obj[idxTitle], !0), showstartdate = ''; + var displayShowName = cleanseText(item[result.Title], !0), showstartdate = ''; - if (null !== obj[idxDate]) { - var startDate = new Date(obj[idxDate]); + if (null !== item[result.Aired]) { + var startDate = new Date(item[result.Aired]); var today = new Date(); showstartdate = ' (' + (startDate > today ? 'will debut' : 'started') - + ': ' + obj[idxDate] + ')'; + + ': ' + item[result.Aired] + ')'; } - resultStr += '' + + srcState = [ + null === item[result.SrcName] ? '' : item[result.SrcName], + !1 === item[result.isInDB] ? '' : 'exists in db'] + .join(' - ').replace(/(^[\s-]+|[\s-]+$)/, ''); + resultStr += '
' + '' + '' + cleanseText(item[result.TitleHtml], !0) + '
' + + (0 < item[result.Genre].length ? '
(' + item[result.Genre] + ')
' : '') + + (0 < item[result.Network].length ? '
' + item[result.Network] + '
' : '') + + '' + + (0 < item[result.Overview].length ? '

' + item[result.Overview] + '

' : '') + + 'Click for more' + '"' - + ' href="' + anonURL + obj[idxSrcUrl] + obj[idxShowID] + ((data.langid && '' != data.langid) ? '&lid=' + data.langid : '') + '"' + + ' href="' + anonURL + item[result.SrcUrl] + item[result.ShowID] + ((data.langid && '' != data.langid) ? '&lid=' + data.langid : '') + '"' + ' onclick="window.open(this.href, \'_blank\'); return false;"' - + '>' + display_show_name + '' + + '>' + (config.sortArticle ? displayShowName : displayShowName.replace(/^((?:A(?!\s+to)n?)|The)(\s)+(.*)/i, '$3$2($1)')) + '' + showstartdate - + (null == obj[idxSrcDB] ? '' - : ' ' + '[' + obj[idxSrcDB] + ']' + '') + + ('' === srcState ? '' + : ' ' + '[' + srcState + ']' + '') + '' + "\n"; + }); } + var selAttr = 'selected="selected" ', + selClass = 'selected-text', + classAttrSel = 'class="' + selClass + '" ', + defSortby = /^az/.test(config.resultsSortby) || /^date/.test(config.resultsSortby) ? '': classAttrSel + selAttr; + $('#searchResults').html( '
' + "\n" + '' + (0 < row ? row : 'No') - + ' search result' + (1 == row ? '' : 's') + '...' + "\n" + + ' search result' + (1 == row ? '' : 's') + '...' + + '' + + '' + + '' + "\n" + + '
' + resultStr + + '
' + '
' ); + + var container$ = $('#holder'), + sortbySelect$ = $('#results-sortby'), + reOrder = (function(value){ + return ($('#results-sortby').find('option[value$="notop"]').hasClass(selClass) + ? (1000 > value ? value + 1000 : value) + : (1000 > value ? value : value - 1000))}), + getData = (function(itemElem, sortby){ + var position = parseInt($(itemElem).attr('data-sort-' + sortby)); + return (!$(itemElem).attr('data-indb')) ? position : reOrder(position); + }); + + sortbySelect$.find('.' + selClass).each(function(){ + $(this).html('> ' + $(this).html()); + }); + + container$.isotope({ + itemSelector: '.results-item', + sortBy: sortbySelect$.find('option:not([value$="top"]).' + selClass).val(), + layoutMode: 'masonry', + getSortData: { + az: function(itemElem){ return getData(itemElem, 'az'); }, + date: function(itemElem){ return getData(itemElem, 'date'); }, + rel: function(itemElem){ return getData(itemElem, 'rel'); } + } + }).on('arrangeComplete', function(event, items){ + $(items).each(function(i, item){ + if (1 === i % 2){ + $(item.element).addClass('alt'); + } + }); + }); + + sortbySelect$.on('change', function(){ + var selectedSort = String($(this).val()), sortby = selectedSort, curSortby$, curSel$, newSel$; + + curSortby$ = $(this).find('option:not([value$="top"])'); + if (/top$/.test(selectedSort)){ + sortby = curSortby$.filter('.' + selClass).val(); + curSortby$ = $(this).find('option[value$="top"]'); + } + curSel$ = curSortby$.filter('.' + selClass); + curSel$.html(curSel$.html().replace(/(?:>|>)\s/ , '')).removeClass(selClass); + + newSel$ = $(this).find('option[value$="' + selectedSort + '"]'); + newSel$.html('> ' + newSel$.html()).addClass(selClass); + + $('.results-item[data-indb="1"]').each(function(){ + $(this).attr(sortby, reOrder(parseInt($(this).attr(sortby), 10))); + }); + $('.results-item').removeClass('alt'); + container$.isotope('updateSortData').isotope({sortBy: sortby}); + + config.resultsSortby = sortby + ($(this).find('option[value$="notop"]').hasClass(selClass) ? ' notop' : ''); + $.get(sbRoot + '/config/general/saveResultPrefs', {ui_results_sortby: selectedSort}); + }); + updateSampleText(); myform.loadsection(0); $('.stepone-result-radio, .stepone-result-title').each(addQTip); diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py index ce6cbe50..dc25093b 100755 --- a/sickbeard/__init__.py +++ b/sickbeard/__init__.py @@ -167,6 +167,8 @@ METADATA_TIVO = None METADATA_MEDE8ER = None METADATA_KODI = None +RESULTS_SORTBY = None + QUALITY_DEFAULT = None STATUS_DEFAULT = None WANTED_BEGIN_DEFAULT = None @@ -536,6 +538,8 @@ def initialize(console_logging=True): versionCheckScheduler, showQueueScheduler, searchQueueScheduler, \ properFinderScheduler, autoPostProcesserScheduler, subtitlesFinderScheduler, background_mapping_task, \ provider_ping_thread_pool + # Add Show Search + global RESULTS_SORTBY # Add Show Defaults global STATUS_DEFAULT, QUALITY_DEFAULT, SHOW_TAG_DEFAULT, FLATTEN_FOLDERS_DEFAULT, SUBTITLES_DEFAULT, \ WANTED_BEGIN_DEFAULT, WANTED_LATEST_DEFAULT, SCENE_DEFAULT, ANIME_DEFAULT @@ -754,6 +758,8 @@ def initialize(console_logging=True): if not re.match(r'\d+\|[^|]+(?:\|[^|]+)*', ROOT_DIRS): ROOT_DIRS = '' + RESULTS_SORTBY = check_setting_str(CFG, 'General', 'results_sortby', '') + QUALITY_DEFAULT = check_setting_int(CFG, 'General', 'quality_default', SD) STATUS_DEFAULT = check_setting_int(CFG, 'General', 'status_default', SKIPPED) WANTED_BEGIN_DEFAULT = check_setting_int(CFG, 'General', 'wanted_begin_default', 0) @@ -1512,6 +1518,7 @@ def save_config(): new_config['General']['recentsearch_startup'] = int(RECENTSEARCH_STARTUP) new_config['General']['backlog_nofull'] = int(BACKLOG_NOFULL) new_config['General']['skip_removed_files'] = int(SKIP_REMOVED_FILES) + new_config['General']['results_sortby'] = str(RESULTS_SORTBY) new_config['General']['quality_default'] = int(QUALITY_DEFAULT) new_config['General']['status_default'] = int(STATUS_DEFAULT) new_config['General']['wanted_begin_default'] = int(WANTED_BEGIN_DEFAULT) diff --git a/sickbeard/helpers.py b/sickbeard/helpers.py index c7b36ade..182c8566 100644 --- a/sickbeard/helpers.py +++ b/sickbeard/helpers.py @@ -1108,13 +1108,6 @@ def getURL(url, post_data=None, params=None, headers=None, timeout=30, session=N 2) Return True/False if success after using kwargs 'savefile' set to file pathname. """ - # download and save file or simply fetch url - savename = None - if 'savename' in kwargs: - # session streaming - session.stream = True - savename = kwargs.pop('savename') - # selectively mute some errors mute = [] for muted in filter( @@ -1126,6 +1119,13 @@ def getURL(url, post_data=None, params=None, headers=None, timeout=30, session=N if None is session: session = CloudflareScraper.create_scraper() + # download and save file or simply fetch url + savename = None + if 'savename' in kwargs: + # session streaming + session.stream = True + savename = kwargs.pop('savename') + if 'nocache' in kwargs: del kwargs['nocache'] else: diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index d022c187..ff6e749c 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -2560,8 +2560,9 @@ class NewHomeAddShows(Home): def sanitizeFileName(self, name): return helpers.sanitizeFileName(name) + # noinspection PyPep8Naming def searchIndexersForShowName(self, search_term, lang='en', indexer=None): - if not lang or lang == 'null': + if not lang or 'null' == lang: lang = 'en' term = search_term.decode('utf-8').strip() terms = [] @@ -2595,7 +2596,7 @@ class NewHomeAddShows(Home): except (StandardError, Exception): search_term = (search_term, '')['tt' in search_id] - # Query Indexers for each search term and build the list of results + # query Indexers for search term and build list of results for indexer in sickbeard.indexerApi().indexers if not int(indexer) else [int(indexer)]: lINDEXER_API_PARMS = sickbeard.indexerApi(indexer).api_params.copy() lINDEXER_API_PARMS['language'] = lang @@ -2608,7 +2609,7 @@ class NewHomeAddShows(Home): logger.log('Fetching show using id: %s (%s) from tv datasource %s' % ( search_id, search_term, sickbeard.indexerApi(indexer).name), logger.DEBUG) r = t[indexer_id, False] - results.setdefault('tt' in search_id and INDEXER_TVDB_X or indexer, {})[int(indexer_id)] = { + results.setdefault((indexer, INDEXER_TVDB_X)['tt' in search_id], {})[int(indexer_id)] = { 'id': indexer_id, 'seriesname': r['seriesname'], 'firstaired': r['firstaired'], 'network': r['network'], 'overview': r['overview'], 'genres': '' if not r['genre'] else r['genre'].lower().strip('|').replace('|', ', '), @@ -2632,7 +2633,7 @@ class NewHomeAddShows(Home): except (StandardError, Exception): pass - # Query trakt for tvdb ids + # query trakt for tvdb ids try: logger.log('Searching for show using search term: %s from tv datasource Trakt' % search_term, logger.DEBUG) resp = [] @@ -2674,34 +2675,74 @@ class NewHomeAddShows(Home): for iid, name in sickbeard.indexerApi().all_indexers.iteritems()} # noinspection PyUnboundLocalVariable map(final_results.extend, - ([['%s%s' % (id_names[id], helpers.find_show_by_id(sickbeard.showList, {(id, INDEXER_TVDB)[id == INDEXER_TVDB_X]: int(show['id'])}, no_mapped_ids=False) and ' - exists in db' or ''), - (id, INDEXER_TVDB)[id == INDEXER_TVDB_X], sickbeard.indexerApi((id, INDEXER_TVDB)[id == INDEXER_TVDB_X]).config['show_url'], int(show['id']), + ([[id_names[iid], any([helpers.find_show_by_id( + sickbeard.showList, {(iid, INDEXER_TVDB)[INDEXER_TVDB_X == iid]: int(show['id'])}, + no_mapped_ids=False)]), + iid, (iid, INDEXER_TVDB)[INDEXER_TVDB_X == iid], + sickbeard.indexerApi((iid, INDEXER_TVDB)[INDEXER_TVDB_X == iid]).config['show_url'], int(show['id']), show['seriesname'], self.encode_html(show['seriesname']), show['firstaired'], show.get('network', '') or '', show.get('genres', '') or '', - re.sub(r'([,\.!][^,\.!]*?)$', '...', - re.sub(r'([!\?\.])(?=\w)', r'\1 ', + re.sub(r'([,.!][^,.!]*?)$', '...', + re.sub(r'([.!?])(?=\w)', r'\1 ', self.encode_html((show.get('overview', '') or '')[:250:].strip()))), self._get_UWRatio(term, show['seriesname'], show.get('aliases', [])), None, None, self._make_search_image_url(iid, show) ] for show in shows.itervalues()] for iid, shows in results.iteritems())) - lang_id = sickbeard.indexerApi().config['langabbv_to_id'][lang] - return json.dumps({ - 'results': sorted(final_results, reverse=True, key=lambda x: x[10]), - 'langid': lang_id}) - # return json.dumps({ - # 'results': sorted(final_results, reverse=True, key=lambda x: dateutil.parser.parse( - # re.match('^(?:19|20)\d\d$', str(x[6])) and ('%s-12-31' % str(x[6])) or (x[6] and str(x[6])) or '1900')), - # 'langid': lang_id}) + def final_order(sortby_index, data, final_sort): + idx_is_indb = 1 + for (n, x) in enumerate(data): + x[sortby_index] = n + (1000, 0)[x[idx_is_indb] and 'notop' not in sickbeard.RESULTS_SORTBY] + return data if not final_sort else sorted(data, reverse=False, key=lambda x: x[sortby_index]) + + def sort_date(data_result, is_last_sort): + idx_date_sort, idx_src, idx_aired = 13, 2, 8 + return final_order( + idx_date_sort, + sorted( + sorted(data_result, reverse=True, key=lambda x: (dateutil.parser.parse( + re.match('^(?:19|20)\d\d$', str(x[idx_aired])) and ('%s-12-31' % str(x[idx_aired])) + or (x[idx_aired] and str(x[idx_aired])) or '1900'))), + reverse=False, key=lambda x: x[idx_src]), is_last_sort) + + def sort_az(data_result, is_last_sort): + idx_az_sort, idx_src, idx_title = 14, 2, 6 + return final_order( + idx_az_sort, + sorted( + data_result, reverse=False, key=lambda x: ( + x[idx_src], + (remove_article(x[idx_title].lower()), x[idx_title].lower())[sickbeard.SORT_ARTICLE])), + is_last_sort) + + def sort_rel(data_result, is_last_sort): + idx_rel_sort, idx_src, idx_rel = 12, 2, 12 + return final_order( + idx_rel_sort, + sorted( + sorted(data_result, reverse=True, key=lambda x: x[idx_rel]), + reverse=False, key=lambda x: x[idx_src]), is_last_sort) + + if 'az' == sickbeard.RESULTS_SORTBY[:2]: + sort_results = [sort_date, sort_rel, sort_az] + elif 'date' == sickbeard.RESULTS_SORTBY[:4]: + sort_results = [sort_az, sort_rel, sort_date] + else: + sort_results = [sort_az, sort_date, sort_rel] + + for n, func in enumerate(sort_results): + final_results = func(final_results, n == len(sort_results) - 1) + + return json.dumps({'results': final_results, 'langid': sickbeard.indexerApi().config['langabbv_to_id'][lang]}) @staticmethod def _make_search_image_url(iid, show): img_url = '' if INDEXER_TRAKT == iid: - img_url = 'imagecache?path=browse/thumb/trakt&filename=%s&tmdbid=%s&tvdbid=%s' % \ + img_url = 'imagecache?path=browse/thumb/trakt&filename=%s&trans=0&tmdbid=%s&tvdbid=%s' % \ ('%s.jpg' % show['trakt_id'], show.get('tmdb_id'), show.get('id')) elif INDEXER_TVDB == iid: - img_url = 'imagecache?path=browse/thumb/tvdb&filename=%s&tvdbid=%s' % \ + img_url = 'imagecache?path=browse/thumb/tvdb&filename=%s&trans=0&tvdbid=%s' % \ ('%s.jpg' % show['id'], show['id']) return img_url @@ -4658,6 +4699,19 @@ class ConfigGeneral(Config): def saveRootDirs(self, rootDirString=None): sickbeard.ROOT_DIRS = rootDirString + def saveResultPrefs(self, ui_results_sortby=None): + + if ui_results_sortby in ('az', 'date', 'rel', 'notop', 'ontop'): + was_ontop = 'notop' not in sickbeard.RESULTS_SORTBY + if 'top' == ui_results_sortby[-3:]: + maybe_ontop = ('', ' notop')[was_ontop] + sortby = sickbeard.RESULTS_SORTBY.replace(' notop', '') + sickbeard.RESULTS_SORTBY = '%s%s' % (('rel', sortby)[any([sortby])], maybe_ontop) + else: + sickbeard.RESULTS_SORTBY = '%s%s' % (ui_results_sortby, (' notop', '')[was_ontop]) + + sickbeard.save_config() + def saveAddShowDefaults(self, default_status, any_qualities='', best_qualities='', default_wanted_begin=None, default_wanted_latest=None, default_flatten_folders=False, default_scene=False, default_subtitles=False, default_anime=False, default_tag=''): @@ -6171,7 +6225,8 @@ class CachedImages(MainHandler): self.delete_all_dummy_images(static_image_path) if not ek.ek(os.path.isfile, static_image_path): - static_image_path = ek.ek(os.path.join, sickbeard.PROG_DIR, 'gui', 'slick', 'images', 'trans.png') + static_image_path = ek.ek(os.path.join, sickbeard.PROG_DIR, 'gui', 'slick', + 'images', ('image-light.png', 'trans.png')[bool(int(kwargs.get('trans', 1)))]) else: helpers.set_file_timestamp(static_image_path, min_age=3, new_time=None)