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 00000000..59e658b1 Binary files /dev/null and b/gui/slick/images/image-light.png differ diff --git a/gui/slick/interfaces/default/home_newShow.tmpl b/gui/slick/interfaces/default/home_newShow.tmpl index 9fa70d38..bbb23756 100644 --- a/gui/slick/interfaces/default/home_newShow.tmpl +++ b/gui/slick/interfaces/default/home_newShow.tmpl @@ -15,7 +15,11 @@ #set indexer_count = len([$i for $i in $sickbeard.indexerApi().indexers if $sickbeard.indexerApi(i).config.get('active', False) and not $sickbeard.indexerApi(i).config.get('defunct', False)]) + 1 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)