diff --git a/CHANGES.md b/CHANGES.md
index 8e7b4f9..a2f45ce 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -79,6 +79,9 @@
* Add fetch extra data fallback from TMDB for persons
* Change fanart icon
* Add provider TorrentDB
+* Add menu Shows/"TVmaze Cards"
+* Add show name/networks card user input filter
+* Change only auto refresh card view if a recoverable error occurs
[develop changelog]
diff --git a/gui/slick/css/style.css b/gui/slick/css/style.css
index 175be1f..b2fc538 100644
--- a/gui/slick/css/style.css
+++ b/gui/slick/css/style.css
@@ -707,7 +707,8 @@ inc_top.tmpl
}
.sgicon-tvmaze:before{
- content:"\e89a"
+ content:"\e89a";
+ margin-right:14px
}
.sgicon-emby:before{
diff --git a/gui/slick/interfaces/default/home_browseShows.tmpl b/gui/slick/interfaces/default/home_browseShows.tmpl
index bec0627..8ae6865 100644
--- a/gui/slick/interfaces/default/home_browseShows.tmpl
+++ b/gui/slick/interfaces/default/home_browseShows.tmpl
@@ -8,6 +8,8 @@
<% def sg_str(varname, default=''): return getattr(sickbeard, varname, default) %>#slurp#
##
#set $mode = $kwargs and $kwargs.get('mode', '')
+#set $use_network = $kwargs.get('use_networks', False)
+#set $use_returning = 'returning' == mode
#set $use_votes = $kwargs and $kwargs.get('use_votes', True)
#set $use_ratings = $kwargs and $kwargs.get('use_ratings', True)
##
@@ -20,7 +22,11 @@
##
#import os.path
#include $os.path.join($sg_str('PROG_DIR'), 'gui/slick/interfaces/default/inc_top.tmpl')
-
+
+ #end if
+
+
+
+#end if
diff --git a/gui/slick/interfaces/default/inc_top.tmpl b/gui/slick/interfaces/default/inc_top.tmpl
index 97cbeb5..8e1518e 100644
--- a/gui/slick/interfaces/default/inc_top.tmpl
+++ b/gui/slick/interfaces/default/inc_top.tmpl
@@ -188,6 +188,10 @@
#set $tvc_mode = $tvc_modes.get($sg_var('TVC_MRU'), 'new shows')
TV Calendar Cards
+#set $tvm_modes = dict(tvm_premieres='new shows', tvm_returning='returning')
+#set $tvm_mode = $tvm_modes.get($sg_var('TVM_MRU'), 'new shows')
+ TVmaze Cards
+
#set $ne_modes = dict(ne_newpop='new popular', ne_newtop='new top rated', ne_upcoming='upcoming', ne_trending='trending')
#set $ne_mode = $ne_modes.get($sg_var('NE_MRU'), 'new popular')
Next Episode Cards
diff --git a/lib/tvmaze_api/tvmaze_api.py b/lib/tvmaze_api/tvmaze_api.py
index c1614c7..8c0b468 100644
--- a/lib/tvmaze_api/tvmaze_api.py
+++ b/lib/tvmaze_api/tvmaze_api.py
@@ -6,18 +6,21 @@ __author__ = 'Prinz23'
__version__ = '1.0'
__api_version__ = '1.0.0'
-import logging
import datetime
+import logging
+import re
+
import requests
-from requests.packages.urllib3.util.retry import Retry
+from urllib3.util.retry import Retry
from requests.adapters import HTTPAdapter
from six import iteritems
from sg_helpers import get_url, try_int
from lib.dateutil.parser import parser
+# noinspection PyProtectedMember
from lib.dateutil.tz.tz import _datetime_to_timestamp
from lib.exceptions_helper import ConnectionSkipException, ex
-from .tvmaze_exceptions import *
+# from .tvmaze_exceptions import *
from lib.tvinfo_base import TVInfoBase, TVInfoImage, TVInfoImageSize, TVInfoImageType, Character, Crew, \
crew_type_names, Person, RoleTypes, TVInfoShow, TVInfoEpisode, TVInfoIDs, TVInfoSeason, PersonGenders, \
TVINFO_TVMAZE, TVINFO_TVDB, TVINFO_IMDB
@@ -25,7 +28,7 @@ from lib.pytvmaze import tvmaze
# noinspection PyUnreachableCode
if False:
- from typing import Any, AnyStr, Dict, List, Optional, Union
+ from typing import Any, AnyStr, Dict, List, Optional
from six import integer_types
log = logging.getLogger('tvmaze.api')
@@ -38,8 +41,10 @@ def tvmaze_endpoint_standard_get(url):
retries = Retry(total=5,
backoff_factor=0.1,
status_forcelist=[429])
+ # noinspection HttpUrlsUsage
s.mount('http://', HTTPAdapter(max_retries=retries))
s.mount('https://', HTTPAdapter(max_retries=retries))
+ # noinspection PyProtectedMember
return get_url(url, json=True, session=s, hooks={'response': tvmaze._record_hook}, raise_skip_exception=True)
@@ -139,10 +144,10 @@ class TvMaze(TVInfoBase):
return {'seriesname': s.name, 'id': s.id, 'firstaired': s.premiered,
'network': s.network and s.network.name,
'genres': s.genres, 'overview': s.summary,
- 'aliases': [a.name for a in s.akas], 'image': s.image and s.image.get('original'),
- 'ids': TVInfoIDs(tvdb=s.externals.get('thetvdb'), rage=s.externals.get('tvrage'), tvmaze=s.id,
- imdb=s.externals.get('imdb') and try_int(s.externals.get('imdb').replace('tt', ''),
- None))}
+ 'aliases': [a.name for a in s.akas], 'image': s.image and s.image.get('original'),
+ 'ids': TVInfoIDs(
+ tvdb=s.externals.get('thetvdb'), rage=s.externals.get('tvrage'), tvmaze=s.id,
+ imdb=s.externals.get('imdb') and try_int(s.externals.get('imdb').replace('tt', ''), None))}
results = []
if ids:
for t, p in iteritems(ids):
@@ -201,11 +206,11 @@ class TvMaze(TVInfoBase):
return results
def _set_episode(self, sid, ep_obj):
- for _k, _s in [('seasonnumber', 'season_number'), ('episodenumber', 'episode_number'),
- ('episodename', 'title'), ('overview', 'summary'), ('firstaired', 'airdate'),
- ('airtime', 'airtime'), ('runtime', 'runtime'),
- ('seriesid', 'maze_id'), ('id', 'maze_id'), ('is_special', 'special'),
- ('filename', 'image')]:
+ for _k, _s in (
+ ('seasonnumber', 'season_number'), ('episodenumber', 'episode_number'),
+ ('episodename', 'title'), ('overview', 'summary'), ('firstaired', 'airdate'),
+ ('airtime', 'airtime'), ('runtime', 'runtime'),
+ ('seriesid', 'maze_id'), ('id', 'maze_id'), ('is_special', 'special'), ('filename', 'image')):
if 'filename' == _k:
image = getattr(ep_obj, _s, {}) or {}
image = image.get('original') or image.get('medium')
@@ -221,7 +226,8 @@ class TvMaze(TVInfoBase):
except (BaseException, Exception):
pass
- def _set_network(self, show_obj, network, is_stream):
+ @staticmethod
+ def _set_network(show_obj, network, is_stream):
show_obj['network'] = network.name
show_obj['network_timezone'] = network.timezone
show_obj['network_country'] = network.country
@@ -229,17 +235,21 @@ class TvMaze(TVInfoBase):
show_obj['network_id'] = network.maze_id
show_obj['network_is_stream'] = is_stream
- def _get_show_data(self, sid, language, get_ep_info=False, banners=False, posters=False, seasons=False,
- seasonwides=False, fanart=False, actors=False, **kwargs):
- log.debug('Getting all series data for %s' % sid)
+ def _get_tvm_show(self, show_id, get_ep_info):
try:
self.show_not_found = False
- show_data = tvm_obj.get_show(maze_id=sid, embed='cast%s' % ('', ',episodeswithspecials')[get_ep_info])
+ return tvm_obj.get_show(maze_id=show_id, embed='cast%s' % ('', ',episodeswithspecials')[get_ep_info])
except tvmaze.ShowNotFound:
self.show_not_found = True
- return False
- except (BaseException, Exception) as e:
- log.debug('Error getting data for tvmaze show id: %s' % sid)
+ except (BaseException, Exception):
+ log.debug('Error getting data for tvmaze show id: %s' % show_id)
+
+ def _get_show_data(self, sid, language, get_ep_info=False, banners=False, posters=False, seasons=False,
+ seasonwides=False, fanart=False, actors=False, **kwargs):
+ log.debug('Getting all series data for %s' % sid)
+
+ show_data = self._get_tvm_show(sid, get_ep_info)
+ if not show_data:
return False
show_obj = self.shows[sid].__dict__
@@ -330,19 +340,20 @@ class TvMaze(TVInfoBase):
except (BaseException, Exception):
print('error')
pass
- existing_person.p_id, existing_person.name, existing_person.image, existing_person.gender, \
- existing_person.birthdate, existing_person.deathdate, existing_person.country, \
- existing_person.country_code, existing_person.country_timezone, existing_person.thumb_url, \
- existing_person.url, existing_person.ids = \
- ch.person.id, ch.person.name, ch.person.image and ch.person.image.get('original'), \
- PersonGenders.named.get(ch.person.gender and ch.person.gender.lower(),
- PersonGenders.unknown),\
- person.birthdate, person.deathdate,\
- ch.person.country and ch.person.country.get('name'),\
- ch.person.country and ch.person.country.get('code'),\
- ch.person.country and ch.person.country.get('timezone'),\
- ch.person.image and ch.person.image.get('medium'),\
- ch.person.url, {TVINFO_TVMAZE: ch.person.id}
+ (existing_person.p_id, existing_person.name, existing_person.image, existing_person.gender,
+ existing_person.birthdate, existing_person.deathdate, existing_person.country,
+ existing_person.country_code, existing_person.country_timezone, existing_person.thumb_url,
+ existing_person.url, existing_person.ids) = \
+ (ch.person.id, ch.person.name,
+ ch.person.image and ch.person.image.get('original'),
+ PersonGenders.named.get(
+ ch.person.gender and ch.person.gender.lower(), PersonGenders.unknown),
+ person.birthdate, person.deathdate,
+ ch.person.country and ch.person.country.get('name'),
+ ch.person.country and ch.person.country.get('code'),
+ ch.person.country and ch.person.country.get('timezone'),
+ ch.person.image and ch.person.image.get('medium'),
+ ch.person.url, {TVINFO_TVMAZE: ch.person.id})
else:
existing_character.person.append(person)
else:
@@ -396,7 +407,7 @@ class TvMaze(TVInfoBase):
show_obj['ids'] = TVInfoIDs(tvdb=show_data.externals.get('thetvdb'),
rage=show_data.externals.get('tvrage'),
imdb=show_data.externals.get('imdb') and
- try_int(show_data.externals.get('imdb').replace('tt', ''), None))
+ try_int(show_data.externals.get('imdb').replace('tt', ''), None))
if show_data.network:
self._set_network(show_obj, show_data.network, False)
@@ -406,15 +417,8 @@ class TvMaze(TVInfoBase):
if get_ep_info and not getattr(self.shows.get(sid), 'ep_loaded', False):
log.debug('Getting all episodes of %s' % sid)
if None is show_data:
- try:
- self.show_not_found = False
- show_data = tvm_obj.get_show(maze_id=sid, embed='cast%s' % ('', ',episodeswithspecials')[
- get_ep_info])
- except tvmaze.ShowNotFound:
- self.show_not_found = True
- return False
- except (BaseException, Exception) as e:
- log.debug('Error getting data for tvmaze show id: %s' % sid)
+ show_data = self._get_tvm_show(sid, get_ep_info)
+ if not show_data:
return False
if show_data.episodes:
@@ -543,3 +547,21 @@ class TvMaze(TVInfoBase):
p = None
if p:
return self._convert_person(p)
+
+ def get_premieres(self):
+ # type: (...) -> List[tvmaze.Episode]
+ return self.filtered_schedule(lambda e: all([1 == e.season_number, 1 == e.episode_number]))
+
+ def get_returning(self):
+ # type: (...) -> List[tvmaze.Episode]
+ return self.filtered_schedule(lambda e: all([1 != e.season_number, 1 == e.episode_number]))
+
+ @staticmethod
+ def filtered_schedule(condition):
+ try:
+ return sorted([
+ e for e in tvmaze.get_full_schedule()
+ if condition(e) and (None is e.show.language or re.search('(?i)eng|jap', e.show.language))],
+ key=lambda x: x.show.premiered or x.airstamp)
+ except(BaseException, Exception):
+ return []
diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py
index 7d6aa38..f0f20b3 100755
--- a/sickbeard/__init__.py
+++ b/sickbeard/__init__.py
@@ -618,6 +618,7 @@ else:
MC_MRU = ''
TVC_MRU = ''
+TVM_MRU = ''
NE_MRU = ''
COOKIE_SECRET = b64encodestring(uuid.uuid4().bytes + uuid.uuid4().bytes)
@@ -763,7 +764,7 @@ def init_stage_1(console_logging):
global USE_TRAKT, TRAKT_CONNECTED_ACCOUNT, TRAKT_ACCOUNTS, TRAKT_MRU, TRAKT_VERIFY, \
TRAKT_USE_WATCHLIST, TRAKT_REMOVE_WATCHLIST, TRAKT_TIMEOUT, TRAKT_METHOD_ADD, TRAKT_START_PAUSED, \
TRAKT_SYNC, TRAKT_DEFAULT_INDEXER, TRAKT_REMOVE_SERIESLIST, TRAKT_UPDATE_COLLECTION, \
- MC_MRU, TVC_MRU, NE_MRU, \
+ MC_MRU, TVC_MRU, TVM_MRU, NE_MRU, \
USE_SLACK, SLACK_NOTIFY_ONSNATCH, SLACK_NOTIFY_ONDOWNLOAD, SLACK_NOTIFY_ONSUBTITLEDOWNLOAD, \
SLACK_CHANNEL, SLACK_AS_AUTHED, SLACK_BOT_NAME, SLACK_ICON_URL, SLACK_ACCESS_TOKEN, \
USE_DISCORD, DISCORD_NOTIFY_ONSNATCH, DISCORD_NOTIFY_ONDOWNLOAD, \
@@ -1207,6 +1208,7 @@ def init_stage_1(console_logging):
MC_MRU = check_setting_str(CFG, 'Metacritic', 'mc_mru', '')
TVC_MRU = check_setting_str(CFG, 'TVCalendar', 'tvc_mru', '')
+ TVM_MRU = check_setting_str(CFG, 'TVmaze', 'tvm_mru', '')
NE_MRU = check_setting_str(CFG, 'NextEpisode', 'ne_mru', '')
USE_PYTIVO = bool(check_setting_int(CFG, 'pyTivo', 'use_pytivo', 0))
@@ -1692,7 +1694,7 @@ def init_stage_2():
run_delay=datetime.timedelta(minutes=5),
threadName='PLEXWATCHEDSTATE')
- MEMCACHE['history_tab_limit'] = 10
+ MEMCACHE['history_tab_limit'] = 11
MEMCACHE['history_tab'] = History.menu_tab(MEMCACHE['history_tab_limit'])
try:
@@ -2212,6 +2214,9 @@ def save_config():
('TVCalendar', [
('mru', TVC_MRU)
]),
+ ('TVmaze', [
+ ('mru', TVM_MRU)
+ ]),
('NextEpisode', [
('mru', NE_MRU)
]),
diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py
index f5d3018..5162452 100644
--- a/sickbeard/webserve.py
+++ b/sickbeard/webserve.py
@@ -93,6 +93,7 @@ from lib.dateutil.relativedelta import relativedelta
from lib.fuzzywuzzy import fuzz
from lib.libtrakt import TraktAPI
from lib.libtrakt.exceptions import TraktException, TraktAuthException
+from lib.tvmaze_api.tvmaze_api import TvMaze
import lib.rarfile.rarfile as rarfile
@@ -5168,6 +5169,157 @@ class AddShows(Home):
return self.new_show('|'.join(['', '', '', show_name]), use_show_name=True)
+ def tvm_default(self):
+
+ return self.redirect('/add-shows/%s' % ('tvm_premieres', sickbeard.TVM_MRU)[any(sickbeard.TVM_MRU)])
+
+ def tvm_premieres(self, **kwargs):
+ return self.browse_tvm(
+ 'New Shows at TVmaze', mode='premieres', **kwargs)
+
+ def tvm_returning(self, **kwargs):
+ return self.browse_tvm(
+ 'Returning Shows at TVmaze', mode='returning', **kwargs)
+
+ def browse_tvm(self, browse_title, **kwargs):
+
+ browse_type = 'TVmaze'
+
+ footnote = None
+ filtered = []
+
+ def card_cache(mem_key):
+ # noinspection PyProtectedMember
+ from lib.dateutil.tz.tz import _datetime_to_timestamp
+
+ if (int(_datetime_to_timestamp(datetime.datetime.now()))
+ < sickbeard.MEMCACHE.get(mem_key, {}).get('last_update', 0)):
+ return sickbeard.MEMCACHE.get(mem_key).get('data')
+ if 'prem' in mem_key:
+ data = TvMaze().get_premieres()
+ else:
+ data = TvMaze().get_returning()
+ sickbeard.MEMCACHE[mem_key] = dict(
+ last_update=(30*60) + int(_datetime_to_timestamp(datetime.datetime.now())), data=data)
+ return data
+
+ if 'New' in browse_title:
+ episodes = card_cache('tvmaze_premiere')
+ else:
+ episodes = card_cache('tvmaze_returning')
+
+ oldest, newest, oldest_dt, newest_dt, use_networks = None, None, 9999999, 0, False
+ dedupe = []
+ parseinfo = dateutil.parser.parserinfo(dayfirst=False, yearfirst=True)
+ for cur_episode_info in episodes:
+ if cur_episode_info.show.maze_id in dedupe:
+ continue
+ dedupe += [cur_episode_info.show.maze_id]
+
+ try:
+ if cur_episode_info.airtime:
+ airtime = dateutil.parser.parse(cur_episode_info.airtime).time()
+ else:
+ airtime = cur_episode_info.airstamp and dateutil.parser.parse(cur_episode_info.airstamp).time()
+ if (0, 0) == (airtime.hour, airtime.minute):
+ airtime = dateutil.parser.parse('23:59').time()
+ dt = datetime.datetime.combine(
+ dateutil.parser.parse(
+ (cur_episode_info.show.premiered or cur_episode_info.airdate), parseinfo).date(), airtime)
+ dt_ordinal = dt.toordinal()
+ now_ordinal = datetime.datetime.now().toordinal()
+ when_past = dt_ordinal < now_ordinal
+ dt_string = SGDatetime.sbfdate(dt)
+
+ if dt_ordinal < oldest_dt:
+ oldest_dt = dt_ordinal
+ oldest = dt_string
+ if dt_ordinal > newest_dt:
+ newest_dt = dt_ordinal
+ newest = dt_string
+
+ returning = returning_str = None
+ if 'Return' in browse_title:
+ returning = '9'
+ returning_str = 'TBC'
+ if cur_episode_info.airdate:
+ returning = cur_episode_info.airdate
+ dt_returning = datetime.datetime.combine(
+ dateutil.parser.parse(returning, parseinfo).date(), airtime)
+ when_past = dt_returning.toordinal() < now_ordinal
+ returning_str = SGDatetime.sbfdate(dt_returning)
+
+ try:
+ img_uri = next(i for i in cur_episode_info.show.images
+ if i.main and 'poster' == i.type).resolutions['original']['url']
+ images = dict(poster=dict(thumb='imagecache?path=browse/thumb/tvmaze&source=%s' % img_uri))
+ sickbeard.CACHE_IMAGE_URL_LIST.add_url(img_uri)
+ except(BaseException, Exception):
+ images = {}
+
+ ids = dict(tvmaze=cur_episode_info.maze_id)
+ imdb_id = cur_episode_info.show.externals.get('imdb')
+ if imdb_id:
+ ids.update(dict(imdb=imdb_id))
+ tvdb_id = cur_episode_info.show.externals.get('thetvdb')
+ if tvdb_id:
+ ids.update(dict(tvdb=tvdb_id))
+
+ network_name = (getattr(cur_episode_info.show.network, 'name', None)
+ or getattr(cur_episode_info.show.web_channel, 'name', None) or '')
+ cc = 'US'
+ if network_name:
+ use_networks = True
+ cc = (getattr(cur_episode_info.show.network, 'code', None)
+ or getattr(cur_episode_info.show.web_channel, 'code', None) or 'US')
+
+ language = ((cur_episode_info.show.language and 'jap' in cur_episode_info.show.language.lower())
+ and 'jp' or 'en')
+ filtered.append(dict(
+ ids=ids,
+ premiered=dt_ordinal,
+ premiered_str=dt_string,
+ returning=returning,
+ returning_str=returning_str,
+ when_past=when_past,
+ episode_number=cur_episode_info.episode_number,
+ episode_season=cur_episode_info.season_number,
+ episode_overview='' if not cur_episode_info.summary else cur_episode_info.summary.strip(),
+ genres=(', '.join(['%s' % v for v in cur_episode_info.show.genres])
+ or cur_episode_info.show.type or ''),
+ images=images,
+ overview=('No overview yet' if not cur_episode_info.show.summary
+ else helpers.xhtml_escape(cur_episode_info.show.summary.strip()[:250:])
+ .strip('*').strip()),
+ title=cur_episode_info.show.name,
+ language=language,
+ language_img=sickbeard.MEMCACHE_FLAG_IMAGES.get(language, False),
+ country=cc,
+ country_img=sickbeard.MEMCACHE_FLAG_IMAGES.get(cc.lower(), False),
+ network=network_name,
+ rating=cur_episode_info.show.weight or 0,
+ url_src_db=cur_episode_info.show.url,
+ ))
+ except (BaseException, Exception):
+ pass
+ kwargs.update(dict(oldest=oldest, newest=newest))
+
+ kwargs.update(dict(footnote=footnote, use_votes=False, use_networks=use_networks))
+
+ mode = kwargs.get('mode', '')
+ if mode:
+ func = 'tvm_%s' % mode
+ if callable(getattr(self, func, None)):
+ sickbeard.TVM_MRU = func
+ sickbeard.save_config()
+ return self.browse_shows(browse_type, browse_title, filtered, **kwargs)
+
+ # noinspection PyUnusedLocal
+ def info_tvmaze(self, ids, show_name):
+
+ if not filter_list(lambda tvid_prodid: helpers.find_show_by_id(tvid_prodid), ids.split(' ')):
+ return self.new_show('|'.join(['', '', '', ' '.join([ids, show_name])]), use_show_name=True)
+
def tvc_default(self):
return self.redirect('/add-shows/%s' % ('tvc_newshows', sickbeard.TVC_MRU)[any(sickbeard.TVC_MRU)])
@@ -5525,7 +5677,7 @@ class AddShows(Home):
@staticmethod
def browse_mru(browse_type, **kwargs):
save_config = False
- if browse_type in ('AniDB', 'IMDb', 'Metacritic', 'Trakt', 'TVCalendar', 'Nextepisode'):
+ if browse_type in ('AniDB', 'IMDb', 'Metacritic', 'Trakt', 'TVCalendar', 'TVmaze', 'Nextepisode'):
save_config = True
sickbeard.BROWSELIST_MRU[browse_type] = dict(
showfilter=kwargs.get('showfilter', ''), showsort=kwargs.get('showsort', ''))
@@ -5547,7 +5699,8 @@ class AddShows(Home):
showsort = t.saved_showsort.split(',')
t.saved_showsort_sortby = 3 == len(showsort) and showsort[2] or 'by_order'
t.reset_showsort_sortby = ('votes' in t.saved_showsort_sortby and not kwargs.get('use_votes', True)
- or 'rating' in t.saved_showsort_sortby and not kwargs.get('use_ratings', True))
+ or 'rating' in t.saved_showsort_sortby and not kwargs.get('use_ratings', True)
+ or 'returning' in t.saved_showsort_sortby and 'Returning' not in browse_title)
t.is_showsort_desc = ('desc' == (2 <= len(showsort) and showsort[1] or 'asc')) and not t.reset_showsort_sortby
t.saved_showsort_view = 1 <= len(showsort) and showsort[0] or '*'
t.all_shows = []
@@ -5635,6 +5788,7 @@ class AddShows(Home):
('order', lambda _x: _x['order']),
('name', lambda _x: _title(_x['title'])),
('premiered', lambda _x: (_x['premiered'], _title(_x['title']))),
+ ('returning', lambda _x: (_x['returning'], _title(_x['title']))),
('votes', lambda _x: (helpers.try_int(_x['votes']), _title(_x['title']))),
('rating', lambda _x: (helpers.try_float(_x['rating']), _title(_x['title']))),
('rating_votes', lambda _x: (helpers.try_float(_x['rating']), helpers.try_int(_x['votes']),