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 += '
'
+ + (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(
''
);
+
+ 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)