From 31f5b3ea2360cca099ecbaa87233ad450bdd01ff Mon Sep 17 00:00:00 2001 From: JackDandy Date: Sun, 16 Jun 2024 22:27:15 +0100 Subject: [PATCH 1/3] Move the dynamic list of roles from the fix height cards into the flexible height popup. Tweaks to order of stuff and fix issue where images to the right flowed badly on different height images on the left on the person view Change darker popup title for improve visual contrast Start of fix scale image --- gui/slick/interfaces/default/cast_person.tmpl | 11 ++++--- .../interfaces/default/home_browseShows.tmpl | 31 +++++++++++++------ 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/gui/slick/interfaces/default/cast_person.tmpl b/gui/slick/interfaces/default/cast_person.tmpl index 1c5eddae..30bf5b0c 100644 --- a/gui/slick/interfaces/default/cast_person.tmpl +++ b/gui/slick/interfaces/default/cast_person.tmpl @@ -45,6 +45,7 @@ #person-content .thumb{display:block} #person-content > .main-image{margin-bottom:19px} #person-content > .cast .cast-bg{height:300px; margin:0 auto; background:url(/images/poster-person.jpg) center center no-repeat} +#character-content{margin-left:235px} <% def param(visible=True, rid=None, cache_person=None, cache_char=None, person=None, role=None, tvid_prodid=None, thumb=None, oid=None, pid=None): @@ -64,10 +65,6 @@ def param(visible=True, rid=None, cache_person=None, cache_char=None, person=Non %>
-
- -
-
#slurp #set $gender = '' #if $PersonGenders.female == $person.gender# @@ -78,6 +75,10 @@ def param(visible=True, rid=None, cache_person=None, cache_char=None, person=Non

$person.name#if $age #($age)#end if##if $gender #$gender#end if##if $person.deathday # †#end if#

+
+ +
+
@@ -486,8 +488,10 @@ $(function() { #end if #if $p_ref -
- +
+
+ +
#end if #end if diff --git a/lib/api_tmdb/tmdb_api.py b/lib/api_tmdb/tmdb_api.py index 38ce0bed..64516a8d 100644 --- a/lib/api_tmdb/tmdb_api.py +++ b/lib/api_tmdb/tmdb_api.py @@ -310,8 +310,9 @@ class TmdbIndexer(TVInfoBase): self.img_base_url, self.size_map[TVInfoImageType.person_poster][TVInfoImageSize.medium], tmdb_person_obj['profile_path']) + clean_person_name = clean_data(tmdb_person_obj.get('name')) _it_person_obj = TVInfoPerson( - p_id=tmdb_person_obj.get('id'), ids=TVInfoIDs(ids=person_ids), name=clean_data(tmdb_person_obj.get('name')), + p_id=tmdb_person_obj.get('id'), ids=TVInfoIDs(ids=person_ids), name=clean_person_name, akas=clean_data(set(tmdb_person_obj.get('also_known_as') or [])), bio=clean_data(tmdb_person_obj.get('biography')), gender=gender, image=main_image, images=image_list, thumb_url=main_thumb, @@ -331,6 +332,10 @@ class TmdbIndexer(TVInfoBase): ti_show.overview = self._enforce_text(character.get('overview')) ti_show.firstaired = clean_data(character.get('first_air_date')) ti_show.language = clean_data(character.get('original_language')) + ti_show.popularity = character.get('popularity') + ti_show.vote_count = character.get('vote_count') + ti_show.vote_average = character.get('vote_average') + ti_show.rating = ti_show.vote_average ti_show.genre_list = [] for g in character.get('genre_ids') or []: if g in self.tv_genres: @@ -350,9 +355,13 @@ class TmdbIndexer(TVInfoBase): (self.img_base_url, self.size_map[TVInfoImageType.person_poster][TVInfoImageSize.original], character['backdrop_path']) + clean_char_name = clean_data(character.get('character')) + clean_lower_person_name = (clean_person_name or '').lower() or None characters.append( - TVInfoCharacter(name=clean_data(character.get('character')), ti_show=ti_show, person=[_it_person_obj], - episode_count=character.get('episode_count')) + TVInfoCharacter(name=clean_char_name, ti_show=ti_show, person=[_it_person_obj], + episode_count=character.get('episode_count'), + plays_self=clean_char_name and + (clean_char_name or '').lower() in ('self', clean_lower_person_name)) ) _it_person_obj.characters = characters @@ -754,11 +763,16 @@ class TmdbIndexer(TVInfoBase): for character in sorted(list(filter(lambda b: b['credit_id'] in main_cast_credit_ids, person_obj.get('roles', []) or [])), key=lambda c: c['episode_count'], reverse=True): + clean_char_name = clean_data(character['character']) + clean_person_name = clean_data(person_obj['name']) + clean_lower_person_name = (clean_person_name or '').lower() or None character_obj = TVInfoCharacter( - name=clean_data(character['character']), + name=clean_char_name, + plays_self=clean_char_name and + (clean_char_name or '').lower() in ('self', clean_lower_person_name), person=[ TVInfoPerson( - p_id=person_obj['id'], name=clean_data(person_obj['name']), + p_id=person_obj['id'], name=clean_person_name, ids=TVInfoIDs(ids={TVINFO_TMDB: person_obj['id']}), image='%s%s%s' % ( self.img_base_url, diff --git a/lib/api_trakt/indexerapiinterface.py b/lib/api_trakt/indexerapiinterface.py index 7b279dbd..0318901c 100644 --- a/lib/api_trakt/indexerapiinterface.py +++ b/lib/api_trakt/indexerapiinterface.py @@ -6,7 +6,7 @@ from exceptions_helper import ConnectionSkipException, ex from six import iteritems from .trakt import TraktAPI from lib.tvinfo_base.exceptions import BaseTVinfoShownotfound -from lib.tvinfo_base import TVInfoBase, TVINFO_TRAKT, TVINFO_TMDB, TVINFO_TVDB, TVINFO_TVRAGE, TVINFO_IMDB, \ +from lib.tvinfo_base import PersonGenders, TVInfoBase, TVINFO_TRAKT, TVINFO_TMDB, TVINFO_TVDB, TVINFO_TVRAGE, TVINFO_IMDB, \ TVINFO_SLUG, TVInfoPerson, TVINFO_TWITTER, TVINFO_FACEBOOK, TVINFO_WIKIPEDIA, TVINFO_INSTAGRAM, TVInfoCharacter, \ TVInfoShow, TVInfoIDs, TVInfoSocialIDs, TVINFO_TRAKT_SLUG, TVInfoEpisode, TVInfoSeason, RoleTypes from sg_helpers import clean_data, enforce_type, try_int @@ -262,6 +262,7 @@ class TraktIndexer(TVInfoBase): deathdate=deathdate, homepage=person_obj['homepage'], birthplace=person_obj['birthplace'], + gender=PersonGenders.trakt_map.get(person_obj['gender'], PersonGenders.unknown), social_ids=TVInfoSocialIDs( ids={TVINFO_TWITTER: person_obj['social_ids']['twitter'], TVINFO_FACEBOOK: person_obj['social_ids']['facebook'], @@ -308,6 +309,7 @@ class TraktIndexer(TVInfoBase): if resp: if show_credits: pc = [] + clean_lower_person_name = (result.name or '').lower() for c in resp.get('cast') or []: ti_show = TVInfoShow() ti_show.id = c['show']['ids'].get('trakt') @@ -327,9 +329,11 @@ class TraktIndexer(TVInfoBase): ti_show.rating = c['show'].get('rating') ti_show.vote_count = c['show'].get('votes') for ch in c.get('characters') or []: - _ti_character = TVInfoCharacter(name=ch, regular=c.get('series_regular'), - ti_show=ti_show, person=[result], - episode_count=c.get('episode_count')) + clean_ch = clean_data(ch) + _ti_character = TVInfoCharacter( + name=clean_ch, regular=c.get('series_regular'), ti_show=ti_show, person=[result], + episode_count=c.get('episode_count'), + plays_self=(clean_ch or '').lower() in ('self', clean_lower_person_name)) pc.append(_ti_character) ti_show.cast[(RoleTypes.ActorGuest, RoleTypes.ActorMain)[ c.get('series_regular', False)]].append(_ti_character) diff --git a/lib/api_tvmaze/tvmaze_api.py b/lib/api_tvmaze/tvmaze_api.py index 56609e0c..14ffbbf9 100644 --- a/lib/api_tvmaze/tvmaze_api.py +++ b/lib/api_tvmaze/tvmaze_api.py @@ -57,6 +57,8 @@ empty_ep = TVInfoEpisode() empty_se = TVInfoSeason() tz_p = parser() +character_clean_regex = re.compile(r'^tb(a|d)$', flags=re.I) + img_type_map = { 'poster': TVInfoImageType.poster, 'banner': TVInfoImageType.banner, @@ -397,6 +399,14 @@ class TvMaze(TVInfoBase): # type: (...) -> Dict[integer_types, integer_types] return {sid: v.seconds_since_epoch for sid, v in iteritems(tvmaze.show_updates().updates)} + @staticmethod + def _clean_character_name(name): + # type: (Optional[str]) -> str + name = clean_data(name) + if isinstance(name, str): + return enforce_type(character_clean_regex.sub('', name), str, '') + return enforce_type(name, str, '') + def _convert_person(self, tvmaze_person_obj, load_credits=True): # type: (tvmaze.Person, bool) -> TVInfoPerson ch = [] @@ -410,7 +420,11 @@ class TvMaze(TVInfoBase): ti_show.ids = TVInfoIDs(ids={TVINFO_TVMAZE: ti_show.id}) ti_show.overview = clean_data(c.show.summary) ti_show.status = clean_data(c.show.status) + ti_show.vote_average = clean_data((c.show.rating and c.show.rating.get('average'))) or None + ti_show.rating = ti_show.vote_average net = c.show.network or c.show.web_channel + ti_show.genre_list = clean_data(c.show.genres or []) + ti_show.genre = '|'.join(ti_show.genre_list or []) if net: ti_show.network = clean_data(net.name) ti_show.network_id = net.maze_id @@ -418,7 +432,18 @@ class TvMaze(TVInfoBase): ti_show.network_country_code = clean_data(net.code) ti_show.network_timezone = clean_data(net.timezone) ti_show.network_is_stream = None is not c.show.web_channel - ch.append(TVInfoCharacter(name=clean_data(c.character.name), ti_show=ti_show, episode_count=1)) + _images = None + if c.character.image and all(i_s in c.character.image and c.character.image[i_s] + for i_s in ('original', 'medium')): + _images = [TVInfoImage(TVInfoImageType.poster, + sizes={TVInfoImageSize.original: c.character.image['original'], + TVInfoImageSize.medium: c.character.image['medium']})] + ch.append(TVInfoCharacter(name=self._clean_character_name(c.character.name), + ti_show=ti_show, episode_count=1, plays_self=c.character.plays_self, + voice=c.character.voice, + image= c.character.image and c.character.image.get('original'), + thumb_url= c.character.image and c.character.image.get('medium'), + p_id=c.character.id, images=_images)) try: birthdate = tvmaze_person_obj.birthday and tz_p.parse(tvmaze_person_obj.birthday).date() except (BaseException, Exception): @@ -446,7 +471,7 @@ class TvMaze(TVInfoBase): (tvmaze_person_obj.guestcastcredits or [], False)]: for c in c_t: # type: tvmaze.CastCredit _show = c.show or c.episode.show - _clean_char_name = clean_data(c.character.name) + _clean_char_name = self._clean_character_name(c.character.name) ti_show = TVInfoShow() if None is not _show: _clean_show_name = clean_data(_show.name) @@ -478,6 +503,8 @@ class TvMaze(TVInfoBase): ti_show.ids = TVInfoIDs(ids={TVINFO_TVMAZE: ti_show.id}) ti_show.overview = enforce_type(clean_data(_show.summary), str, '') ti_show.status = clean_data(_show.status) + ti_show.vote_average = clean_data(_show.rating and _show.rating.get('average')) or None + ti_show.rating = ti_show.vote_average net = _show.network or _show.web_channel if net: ti_show.network = clean_data(net.name) @@ -499,8 +526,18 @@ class TvMaze(TVInfoBase): _g_kw = {'guest_episodes_numbers': {c.episode.season_number: [c.episode.episode_number or 0]}} else: _g_kw = {} + _images = None + if c.character.image and all(i_s in c.character.image and c.character.image[i_s] + for i_s in ('original', 'medium')): + _images = [TVInfoImage(TVInfoImageType.poster, + sizes={TVInfoImageSize.original: c.character.image['original'], + TVInfoImageSize.medium: c.character.image['medium']})] ch.append(TVInfoCharacter(name=_clean_char_name, ti_show=ti_show, regular=regular, episode_count=1, - person=[_ti_person_obj], **_g_kw)) + person=[_ti_person_obj], plays_self=c.character.plays_self, + voice=c.character.voice, + image=c.character.image and c.character.image.get('original'), + thumb_url=c.character.image and c.character.image.get('medium'), + p_id=c.character.id, images=_images, **_g_kw)) _ti_person_obj.characters = ch return _ti_person_obj @@ -588,7 +625,7 @@ class TvMaze(TVInfoBase): else: _s_o.cast[RoleTypes.ActorMain].append( TVInfoCharacter(image=cur_ch.image and cur_ch.image.get('original'), - name=clean_data(cur_ch.name), + name=self._clean_character_name(cur_ch.name), ids=TVInfoIDs({TVINFO_TVMAZE: cur_ch.id}), p_id=cur_ch.id, person=[person], plays_self=cur_ch.plays_self, thumb_url=cur_ch.image and cur_ch.image.get('medium'), diff --git a/lib/pytvmaze/tvmaze.py b/lib/pytvmaze/tvmaze.py index 3e4d46dc..150f225e 100644 --- a/lib/pytvmaze/tvmaze.py +++ b/lib/pytvmaze/tvmaze.py @@ -12,6 +12,18 @@ if False: from typing import Any, AnyStr, Dict, List, Optional, Union +url_maze_id_regex = re.compile(r'^https?://(?:(?:api|wwww)\.)?tvmaze\.com/shows/(\d+)', flags=re.I) + + +def _parse_show_id_from_url(url): + # type: (str) -> Optional[int] + if isinstance(url, str): + try: + return int(url_maze_id_regex.search(url).group(1)) + except (BaseException, Exception): + pass + + class Show(object): def __init__(self, data): self.status = data.get('status') # type: Optional[AnyStr] @@ -36,7 +48,7 @@ class Show(object): self.runtime = data.get('runtime') # type: Optional[int] self.average_runtime = data.get('averageRuntime') self.type = data.get('type') # type: Optional[AnyStr] - self.id = data.get('id') # type: int + self.id = data.get('id') or ('href' in data and _parse_show_id_from_url(data['href'])) or None # type: int self.maze_id = self.id # type: int if data.get('network'): self.network = Network(data.get('network')) # type: Optional[Network] @@ -428,9 +440,12 @@ class CastCredit(object): def populate(self, data): if data.get('_embedded'): if data['_embedded'].get('character'): - self.character = Character(data['_embedded']['character']) + self.character = Character(data['_embedded']['character'], base_data=data) if data['_embedded'].get('show'): self.show = Show(data['_embedded']['show']) + elif ('episode' in data['_embedded'] and '_links' in data['_embedded']['episode'] and + 'show' in data['_embedded']['episode']['_links']): + self.show = Show(data['_embedded']['episode']['_links']['show']) if data['_embedded'].get('episode'): self.episode = Episode(data['_embedded']['episode']) diff --git a/lib/tvinfo_base/base.py b/lib/tvinfo_base/base.py index 9e210ab5..9b148c44 100644 --- a/lib/tvinfo_base/base.py +++ b/lib/tvinfo_base/base.py @@ -884,6 +884,7 @@ class PersonGenders(object): tmdb_map = {0: unknown, 1: female, 2: male} imdb_map = {'female': female, 'male': male} tvdb_map = {0: unknown, 1: male, 2: female, 3: unknown} # 3 is technically: other + trakt_map = {'female': female, 'male': male} class Crew(PersonBase): diff --git a/sickgear/tv.py b/sickgear/tv.py index c3df7072..aa723c32 100644 --- a/sickgear/tv.py +++ b/sickgear/tv.py @@ -724,7 +724,7 @@ class Person(Referential): self._data_fetched = True tvsrc_result, found_persons, found_on_src, search_sources, \ found_ids, ids_to_check, imdb_confirmed, source_confirmed = \ - None, {}, set(), [TVINFO_TRAKT, TVINFO_TMDB, TVINFO_IMDB, TVINFO_TVDB], \ + None, {}, set(), [TVINFO_TRAKT, TVINFO_TMDB, TVINFO_IMDB, TVINFO_TVDB, TVINFO_TVMAZE], \ set([_k for _k, _v in iteritems(self.ids) if _v] + ['text']), {}, False, {} # confirmed_character = False max_search_src = len(search_sources) diff --git a/sickgear/webserve.py b/sickgear/webserve.py index 541fa2c4..e99efec7 100644 --- a/sickgear/webserve.py +++ b/sickgear/webserve.py @@ -5319,6 +5319,13 @@ class AddShows(Home): return self.new_show('|'.join(['', '', '', show_name]), use_show_name=True) + @staticmethod + def _make_char_person_list(cur_show_info): + # type: (TVInfoShow) -> List[Tuple[str, int, str, int]] + return [(ch.name.replace('"', "'"), r_t, RoleTypes.reverse[r_t], ch.episode_count) + for r_t in cur_show_info.cast or [] for ch in cur_show_info.cast[r_t]] + + def tmdb_default(self): method = getattr(self, sickgear.TMDB_MRU, None) if not callable(method): @@ -5376,11 +5383,12 @@ class AddShows(Home): p_ref = f'{TVINFO_TMDB}:{p_item.id}' dup = {} # type: Dict[int, TVInfoShow] for c in p_item.characters: # type: TVInfoCharacter + c.ti_show.cast[RoleTypes.ActorMain].append(c) if c.ti_show.id not in dup: dup[c.ti_show.id] = c.ti_show items.append(c.ti_show) else: - dup[c.ti_show.id].cast.update(c.ti_show.cast) + dup[c.ti_show.id].cast[RoleTypes.ActorMain].extend(c.ti_show.cast[RoleTypes.ActorMain]) del dup else: p_item = None @@ -5400,7 +5408,10 @@ class AddShows(Home): airtime = cur_show_info.airs_time if not airtime or (0, 0) == (airtime.hour, airtime.minute): airtime = dateutil.parser.parse('23:59').time() - dt = datetime.combine(dateutil.parser.parse(cur_show_info.firstaired, parseinfo).date(), airtime) + try: + dt = datetime.combine(dateutil.parser.parse(cur_show_info.firstaired, parseinfo).date(), airtime) + except (BaseException, Exception): + dt = None ord_premiered, str_premiered, started_past, oldest_dt, newest_dt, oldest, newest, _, _, _, _ \ = self.sanitise_dates(dt, oldest_dt, newest_dt, oldest, newest) @@ -5417,8 +5428,7 @@ class AddShows(Home): and 'jp' or 'en') filtered.append(dict( p_ref=p_ref, - p_chars=[(ch.name, r_t, RoleTypes.reverse[r_t], ch.episode_count) - for r_t in cur_show_info.cast or [] for ch in cur_show_info.cast[r_t]], + p_chars=self._make_char_person_list(cur_show_info), ord_premiered=ord_premiered, str_premiered=str_premiered, started_past=started_past, @@ -5602,8 +5612,6 @@ class AddShows(Home): if c.ti_show.id not in dup: dup[c.ti_show.id] = c.ti_show items.append(c.ti_show) - else: - dup[c.ti_show.id].cast.update(c.ti_show.cast) del dup else: p_item = None @@ -5664,8 +5672,7 @@ class AddShows(Home): filtered.append(dict( p_ref=p_ref, p_item=p_item, - p_chars=[(ch.name, r_t, RoleTypes.reverse[r_t], ch.episode_count) - for r_t in cur_show_info.cast or [] for ch in cur_show_info.cast[r_t]], + p_chars=self._make_char_person_list(cur_show_info), ord_premiered=ord_premiered, str_premiered=str_premiered, ord_returning=ord_returning, @@ -6009,11 +6016,13 @@ class AddShows(Home): p_ref = f'{TVINFO_TVMAZE}:{p_item.id}' dup = {} # type: Dict[int, TVInfoShow] for c in p_item.characters: # type: TVInfoCharacter + c.ti_show.cast[(RoleTypes.ActorGuest, RoleTypes.ActorMain)[True is c.regular]].append(c) if c.ti_show.id not in dup: dup[c.ti_show.id] = c.ti_show items.append(c.ti_show) else: - dup[c.ti_show.id].cast.update(c.ti_show.cast) + dup[c.ti_show.id].cast[RoleTypes.ActorMain].extend(c.ti_show.cast[RoleTypes.ActorMain]) + dup[c.ti_show.id].cast[RoleTypes.ActorGuest].extend(c.ti_show.cast[RoleTypes.ActorGuest]) del dup else: p_item = None @@ -6063,8 +6072,7 @@ class AddShows(Home): filtered.append(dict( p_ref=p_ref, - p_chars=[(ch.name, r_t, RoleTypes.reverse[r_t], ch.episode_count) - for r_t in cur_show_info.cast or [] for ch in cur_show_info.cast[r_t]], + p_chars=self._make_char_person_list(cur_show_info), ord_premiered=ord_premiered, str_premiered=str_premiered, ord_returning=ord_returning, @@ -6110,6 +6118,9 @@ class AddShows(Home): @staticmethod def sanitise_dates(date, oldest_dt, newest_dt, oldest, newest, episode_info=None, combine_ep_airtime=False): + # in case of person search (tvmaze) guest starring entires have only show name/id, no dates + if None is date: + return 9, '', True, oldest_dt, newest_dt, oldest, newest, True, 9, 'TBC', False parseinfo = dateutil.parser.parserinfo(dayfirst=False, yearfirst=True) dt = date if isinstance(date, datetime) else dateutil.parser.parse(date) if episode_info: From 53cc79ee8c4ec316584614831b81bd4c1a48897d Mon Sep 17 00:00:00 2001 From: JackDandy Date: Wed, 19 Jun 2024 15:04:04 +0100 Subject: [PATCH 3/3] Change popup on browse shows person view to hide on unfocus when long user adjustable scrollable lists of character roles are used, e.g. Evan Peters in AHS. Change hide ratings on browse shows cards in person view for TVmaze as not provided. Change replace test code for consistency at "Other shows" for sub title across all sources. Make overview parser centralised for consistent reusability. Fix add dynamic fetch to fill show data on person view when a role is non main and the API does not expose. Add logic for when there are no genres. Fix mitigate MRU issue where browse default and param person view conflicted. Change make TMDB person genre lowercase for consistency with other sources. Filter out TMDB roles that have no actual name. Add expose `show_type` to TVmaze api for cases where genres are None (e.g. Taylor Swift vs. Scooter Braun). Fix images on Shows -> MC cards view. Change ensure genres on NE browse shows view are lowercase consistent with other sources. Change browse shows view, replace \r\n in show overviews to a single whitespace on cards. Change TMDB browse shows expose ratings. Fix TMDB browse shows use alternative term_vote in drop down. --- .../interfaces/default/home_browseShows.tmpl | 84 +++++-- lib/api_tvmaze/tvmaze_api.py | 4 + sickgear/webserve.py | 218 +++++++++++++----- 3 files changed, 232 insertions(+), 74 deletions(-) diff --git a/gui/slick/interfaces/default/home_browseShows.tmpl b/gui/slick/interfaces/default/home_browseShows.tmpl index a8215764..c5f01738 100644 --- a/gui/slick/interfaces/default/home_browseShows.tmpl +++ b/gui/slick/interfaces/default/home_browseShows.tmpl @@ -42,6 +42,62 @@ $(this).css('cursor', 'help'); $(this).qtip({ show: {solo:true}, + // Change qTip to manual hide when it contains many roles to scroll + hide: {event:(5 < $(this).closest('div.show-card').attr('data-nroles')) ? 'unfocus' : 'mouseleave'}, + events: { // Callback events + render: function(event, api) { + // Grab the tooltip element from the API + var tooltip = api.elements.tooltip + tooltip.bind('tooltipshow', function(event, api) { + var showcardEl = $(api.target).closest('div.show-card') + if ('1' === showcardEl.attr('data-ajax')) { // do a one time fetch + var qtipEl = $(this).find('.qtip-content'), + premiereEl = qtipEl.find('.premiere'), + genreEl = qtipEl.find('.genre'), + overviewEl = qtipEl.find('.overview'), + oldestEl = $('#oldest'), + newestEl = $('#newest'); + + // Set initial text + overviewEl.html('Fetching overview...'); + $.getJSON($.SickGear.Root + '/add-shows/tvm-get-showinfo', { + tvid_prodid: showcardEl.attr('data-id'), + oldest_dt: $('#oldest').attr('data-oldest-dt'), + newest_dt: $('#newest').attr('data-newest-dt'), + }, + function (data) { + if (undefined !== data.overview) { + showcardEl.attr('data-ajax', '0'); // mark one time fetch as completed + if (undefined !== data.oldest) { + oldestEl.attr('data-oldest-dt', data.oldest_dt) + oldestEl.html(data.oldest); + } else if (undefined !== data.newest) { + newestEl.attr('data-newest-dt', data.newest_dt) + newestEl.html(data.newest); + } + var premiere = ''; + if (data.str_premiered.length) { + showcardEl.attr('data-premiered', data.ord_premiered); + premiere = "First air" + (data.started_past ? 'ed' : 's') + ": " + data.str_premiered + ""; + } + if (data.genres) { + genreEl.css('display', 'block'); + genreEl.find('em').html(data.genres); + } + overviewEl.html(data.overview); + if (data.network.length) { + premiere += "On: " + data.network + ""; + } + premiereEl.html(premiere); + } else { + overviewEl.html('Failed to fetch TVmaze overview' ); + } + } + ) + } + }) + } + }, position: {viewport:$(window), my:'left center', adjust:{y: -10,x: 2 }}, style: {tip: {corner:true, method:'polygon'}, classes:'qtip-rounded qtip-bootstrap qtip-shadow ui-tooltip-sb'} }); @@ -264,7 +320,7 @@ $(function() { } }); - $('.service, .browse-image').each(addQTip); + $('.service, a.browse-image').each(addQTip); if (config.homeSearchFocus) { $('#search_show_name').focus(); @@ -309,9 +365,10 @@ $(function() {
@@ -356,7 +413,7 @@ $(function() { #end if #if $use_ratings and $use_votes - + #end if @@ -483,7 +540,7 @@ $(function() {

$browse_title

#if $kwargs and $kwargs.get('oldest')
- First aired from $kwargs['oldest'] until $kwargs['newest'] + First aired from $kwargs['oldest'] until $kwargs['newest']
#end if @@ -506,7 +563,7 @@ $(function() { #if 'returning' == $mode #set $overview = '%s: %s' % ( 'Season %s' % $this_show['episode_season'], - $this_show['episode_overview'] or $this_show['overview']) + $this_show[('episode_overview', 'overview')['No overview yet' == $this_show['episode_overview']]]) #else #set $overview = $this_show['overview'] #end if @@ -517,14 +574,13 @@ $(function() { #if $use_ratings: #set $data_rating = $try_float($this_show['rating']) #end if - -
+
#if $use_ratings or $use_votes

#if $use_ratings#$this_show['rating']#if $re.search(r'^\d+(\.\d+)?$', (str($this_show['rating'])))#%#end if##end if##if $use_votes#$this_show['votes'] $term_vote.lower()#end if#

#slurp# + #else +

 

#end if #if 'url_tvdb' in $this_show and $this_show['url_tvdb']
List[Tuple[str, int, str, int]] return [(ch.name.replace('"', "'"), r_t, RoleTypes.reverse[r_t], ch.episode_count) - for r_t in cur_show_info.cast or [] for ch in cur_show_info.cast[r_t]] + for r_t in cur_show_info.cast or [] for ch in cur_show_info.cast[r_t] if ch.name] + @staticmethod + def allow_browse_mru(mode_or_mru): + # Fix an issue where a default view mixed with a deriviative view that requires a param will break the default + # Disallows default views from using derivative mru's + return 'person' not in mode_or_mru def tmdb_default(self): method = getattr(self, sickgear.TMDB_MRU, None) - if not callable(method): + if not callable(method) or not self.allow_browse_mru(sickgear.TMDB_MRU): return self.tmdb_upcoming() return method() @@ -5354,7 +5390,7 @@ class AddShows(Home): def tmdb_person(self, person_tmdb_id=None, **kwargs): return self.browse_tmdb( - 'Person at TMDB', mode='get_person', p_id=person_tmdb_id, **kwargs) + 'Person at TMDB', mode='person', p_id=person_tmdb_id, **kwargs) def browse_tmdb(self, browse_title, **kwargs): @@ -5376,7 +5412,7 @@ class AddShows(Home): items = t.get_trending() elif 'trending_week' == mode: items = t.get_trending(time_window='week') - elif 'get_person' == mode: + elif 'person' == mode: items = [] p_item = t.get_person(get_show_credits=True, **kwargs) # type: TVInfoPerson if p_item: @@ -5390,8 +5426,6 @@ class AddShows(Home): else: dup[c.ti_show.id].cast[RoleTypes.ActorMain].extend(c.ti_show.cast[RoleTypes.ActorMain]) del dup - else: - p_item = None else: items = t.discover() @@ -5427,19 +5461,17 @@ class AddShows(Home): language = ((cur_show_info.language and 'jap' in cur_show_info.language.lower()) and 'jp' or 'en') filtered.append(dict( - p_ref=p_ref, - p_chars=self._make_char_person_list(cur_show_info), ord_premiered=ord_premiered, str_premiered=str_premiered, started_past=started_past, - episode_overview=helpers.xhtml_escape(cur_show_info.overview[:250:]).strip('*').strip(), + episode_overview=self.clean_overview(cur_show_info), episode_season=cur_show_info.season, - genres=', '.join(cur_show_info.genre_list) - or (cur_show_info.genre and (cur_show_info.genre.strip('|').replace('|', ', ')) or ''), + genres=(', '.join(cur_show_info.genre_list) + or (cur_show_info.genre and (cur_show_info.genre.strip('|').replace('|', ', ')) or '') + ).lower(), ids=cur_show_info.ids.__dict__, images=images, - overview=(helpers.xhtml_escape(cur_show_info.overview[:250:]).strip('*').strip() - or 'No overview yet'), + overview=self.clean_overview(cur_show_info), title=cur_show_info.seriesname, language=language, language_img=sickgear.MEMCACHE_FLAG_IMAGES.get(language, False), @@ -5447,15 +5479,23 @@ class AddShows(Home): country_img=sickgear.MEMCACHE_FLAG_IMAGES.get(cc.lower(), False), network=network_name, url_src_db=base_url % cur_show_info.id, - votes=cur_show_info.popularity or 0, + rating=0 < (cur_show_info.rating or 0) and + ('%.2f' % (cur_show_info.rating * 10)).replace('.00', '') or 0, + votes=('%.2f' % cur_show_info.popularity) or 0, )) + if p_ref: + filtered[-1].update(dict( + p_name=p_item.name, + p_ref=p_ref, + p_chars=self._make_char_person_list(cur_show_info) + )) except (BaseException, Exception): pass - kwargs.update(dict(oldest=oldest, newest=newest, use_ratings=False, use_filter=True, term_vote='Score')) + kwargs.update(dict(oldest=oldest, newest=newest, use_filter=True, term_vote='Score')) kwargs.update(dict(footnote=footnote, use_networks=use_networks)) - if mode: + if mode and self.allow_browse_mru(mode): func = 'tmdb_%s' % mode if callable(getattr(self, func, None)): sickgear.TMDB_MRU = func @@ -5470,7 +5510,7 @@ class AddShows(Home): def trakt_default(self): method = getattr(self, sickgear.TRAKT_MRU, None) - if not callable(method): + if not callable(method) or not self.allow_browse_mru(sickgear.TMDB_MRU): return self.trakt_trending() return method() @@ -5602,7 +5642,7 @@ class AddShows(Home): if not items: error_msg = 'No items in watchlist. Use the "Add to watchlist" button at the Trakt website' raise ValueError(error_msg) - elif 'get_person' == api_method: + elif 'person' == mode: items = [] p_item = t.get_person(get_show_credits=True, **kwargs) # type: TVInfoPerson if p_item: @@ -5613,8 +5653,6 @@ class AddShows(Home): dup[c.ti_show.id] = c.ti_show items.append(c.ti_show) del dup - else: - p_item = None else: items = t.get_trending() except TraktAuthException as e: @@ -5670,9 +5708,6 @@ class AddShows(Home): images = {} if not image else dict(poster=dict(thumb=image)) filtered.append(dict( - p_ref=p_ref, - p_item=p_item, - p_chars=self._make_char_person_list(cur_show_info), ord_premiered=ord_premiered, str_premiered=str_premiered, ord_returning=ord_returning, @@ -5680,14 +5715,13 @@ class AddShows(Home): started_past=started_past, # air time not yet available 16.11.2015 return_past=return_past, episode_number=episode_info.episodenumber, - episode_overview=helpers.xhtml_escape(episode_info.overview[:250:]).strip('*').strip(), + episode_overview=self.clean_overview(episode_info), episode_season=getattr(episode_info.season, 'number', 1), genres=(', '.join(['%s' % v for v in cur_show_info.genre_list])), ids=cur_show_info.ids.__dict__, images=images, network=network_name, - overview=(helpers.xhtml_escape(cur_show_info.overview[:250:]).strip('*').strip() - or 'No overview yet'), + overview=self.clean_overview(cur_show_info), rating=0 < (cur_show_info.rating or 0) and ('%.2f' % (cur_show_info.rating * 10)).replace('.00', '') or 0, title=(cur_show_info.seriesname or '').strip(), @@ -5699,7 +5733,14 @@ class AddShows(Home): url_tvdb=( '' if not (isinstance(cur_show_info.ids.tvdb, integer_types) and 0 < cur_show_info.ids.tvdb) else sickgear.TVInfoAPI(TVINFO_TVDB).config['show_url'] % cur_show_info.ids.tvdb), - votes=cur_show_info.vote_count or '0')) + votes=cur_show_info.vote_count or '0' + )) + if p_ref: + filtered[-1].update(dict( + p_name=p_item.name, + p_ref=p_ref, + p_chars=self._make_char_person_list(cur_show_info) + )) except (BaseException, Exception): pass @@ -5712,7 +5753,7 @@ class AddShows(Home): return self.browse_trakt( 'get_person', - 'Person on Trakt', + 'Person at Trakt', mode='person', footnote='Note; Expect default placeholder images in this list', p_id=person_trakt_id @@ -5735,14 +5776,11 @@ class AddShows(Home): error_msg = 'No items in watchlist. Use the "Add to watchlist" button at the Trakt website' return self.browse_shows(browse_type, browse_title, filtered, error_msg=error_msg, show_header=1, **kwargs) - if 'get_person' == api_method and filtered: - browse_title = f'{getattr(filtered[0]["p_item"], "name", "")} (Person) on Trakt' - kwargs.update(dict(oldest=oldest, newest=newest, error_msg=error_msg, use_networks=use_networks)) if not any(m in mode for m in ('recommended', 'watchlist', 'person')): mode = mode.split('-') - if mode: + if mode and self.allow_browse_mru(mode): func = 'trakt_%s' % mode[0] if callable(getattr(self, func, None)): param = '' if 1 == len(mode) or mode[1] not in ['year', 'month', 'week', 'all'] else \ @@ -5951,7 +5989,7 @@ class AddShows(Home): network=network or None, ids=ids, images='' if not img_uri else images, - overview='No overview yet' if not overview else helpers.xhtml_escape(overview[:250:]), + overview=self.clean_overview(overview), rating=None, title=title, url_src_db='https://www.pogdesign.co.uk/%s' % url_path.strip('/'), @@ -5979,7 +6017,7 @@ class AddShows(Home): def tvm_default(self): method = getattr(self, sickgear.TVM_MRU, None) - if not callable(method): + if not callable(method) or not self.allow_browse_mru(sickgear.TMDB_MRU): return self.tvm_premieres() return method() @@ -5993,7 +6031,52 @@ class AddShows(Home): def tvm_person(self, person_tvm_id=None, **kwargs): return self.browse_tvm( - 'Person at TVmaze', mode='get_person', p_id=person_tvm_id, **kwargs) + 'Person at TVmaze', mode='person', p_id=person_tvm_id, **kwargs) + + @staticmethod + def clean_overview(info=None): + # type (AnyStr, TVInfoShow) -> AnyStr + text = info if isinstance(info, str) else info.overview + if text: + result = helpers.xhtml_escape(re.sub(r'[\r\n]+', ' ', text[:250:])).strip('*').strip() + result = re.sub(r'([!?.])(?=\w)', r'\1 ', result) + result = re.sub(r'([,.!][^,.!]*?)$', '...', result) + return result.replace('.....', '...') + return 'No overview yet' + + def tvm_get_showinfo(self, tvid_prodid=None, oldest_dt=9999999, newest_dt=0): + result = {} + if 'tvmaze' in tvid_prodid: + tvid = TVINFO_TVMAZE + tvinfo_config = sickgear.TVInfoAPI(tvid).api_params.copy() + t = sickgear.TVInfoAPI(tvid).setup(**tvinfo_config) # type: Union[TvmazeIndexer, TVInfoBase] + show_info = t.get_show(int(tvid_prodid.replace('tvmaze:','')), load_episodes=False) + + oldest_dt, newest_dt = int(oldest_dt), int(newest_dt) + ord_premiered, str_premiered, started_past, old_dt, new_dt, oldest, newest, \ + ok_returning, ord_returning, str_returning, return_past \ + = self.sanitise_dates(show_info.firstaired, oldest_dt, newest_dt, None, None) + result = dict( + ord_premiered=ord_premiered, + str_premiered=str_premiered, + #ord_returning=ord_returning, + #str_returning=str_returning, + started_past=started_past, + #return_past=return_past, + genres=((show_info.genre or '') + or ', '.join(show_info.genre_list) + or ', '.join(show_info.show_type) or '').strip('|').replace('|', ', ').lower(), + overview=self.clean_overview(show_info), + network=show_info.network or ', '.join(show_info.networks) or '', + ) + if old_dt < oldest_dt: + result['oldest_dt'] = old_dt + result['oldest'] = oldest + elif new_dt > newest_dt: + result['newest_dt'] = old_dt + result['newest'] = newest, + + return json_dumps(result) def browse_tvm(self, browse_title, **kwargs): @@ -6009,7 +6092,7 @@ class AddShows(Home): t = sickgear.TVInfoAPI(tvid).setup(**tvinfo_config) # type: Union[TvmazeIndexer, TVInfoBase] if 'premieres' == mode: items = t.get_premieres() - elif 'get_person' == mode: + elif 'person' == mode: items = [] p_item = t.get_person(get_show_credits=True, **kwargs) # type: TVInfoPerson if p_item: @@ -6024,17 +6107,16 @@ class AddShows(Home): dup[c.ti_show.id].cast[RoleTypes.ActorMain].extend(c.ti_show.cast[RoleTypes.ActorMain]) dup[c.ti_show.id].cast[RoleTypes.ActorGuest].extend(c.ti_show.cast[RoleTypes.ActorGuest]) del dup - else: - p_item = None else: items = t.get_returning() # handle switching between returning and premieres sickgear.BROWSELIST_MRU.setdefault(browse_type, dict()) - showfilter = ('by_returning', 'by_premiered')['premieres' == mode] - saved_showsort = sickgear.BROWSELIST_MRU[browse_type].get('tvm_%s' % mode) or '*,asc' - showsort = saved_showsort + (',%s' % showfilter, '')[3 == len(saved_showsort.split(','))] - sickgear.BROWSELIST_MRU[browse_type].update(dict(showfilter=showfilter, showsort=showsort)) + if mode in ('premieres', 'returning'): + showfilter = ('by_returning', 'by_premiered')['premieres' == mode] + saved_showsort = sickgear.BROWSELIST_MRU[browse_type].get('tvm_%s' % mode) or '*,asc' + showsort = saved_showsort + (f',{showfilter}', '')[3 == len(saved_showsort.split(','))] + sickgear.BROWSELIST_MRU[browse_type].update(dict(showfilter=showfilter, showsort=showsort)) oldest, newest, oldest_dt, newest_dt, dedupe = None, None, 9999999, 0, [] use_networks = False @@ -6057,7 +6139,7 @@ class AddShows(Home): if 'returning' == mode and not ok_returning: continue - image = self._make_cache_image_url(tvid, cur_show_info, use_source_id='get_person' == mode) + image = self._make_cache_image_url(tvid, cur_show_info, use_source_id='person' == mode) images = {} if not image else dict(poster=dict(thumb=image)) network_name = cur_show_info.network @@ -6070,9 +6152,11 @@ class AddShows(Home): language = (('jap' in (cur_show_info.language or '').lower()) and 'jp' or 'en') + overview = self.clean_overview(cur_show_info) + overview_ajax = ("No overview yet" == overview + and p_ref and not bool(cur_show_info.cast[RoleTypes.ActorMain])) + filtered.append(dict( - p_ref=p_ref, - p_chars=self._make_char_person_list(cur_show_info), ord_premiered=ord_premiered, str_premiered=str_premiered, ord_returning=ord_returning, @@ -6080,14 +6164,15 @@ class AddShows(Home): started_past=started_past, return_past=return_past, episode_number=episode_info.episodenumber or '', - episode_overview=helpers.xhtml_escape(episode_info.overview[:250:]).strip(), + episode_overview=self.clean_overview(episode_info), episode_season=getattr(episode_info.season, 'number', episode_info.seasonnumber), - genres=((cur_show_info.genre or '').strip('|').replace('|', ', ') - or ', '.join(cur_show_info.show_type) or ''), + genres=((cur_show_info.genre or '') + or ', '.join(cur_show_info.genre_list) + or ', '.join(cur_show_info.show_type) or '').strip('|').replace('|', ', ').lower(), ids=cur_show_info.ids.__dict__, images=images, - overview=(helpers.xhtml_escape(cur_show_info.overview[:250:]).strip('*').strip() - or 'No overview yet'), + overview_ajax=(0, 1)[overview_ajax], + overview=overview, rating=cur_show_info.rating or cur_show_info.popularity or 0, title=cur_show_info.seriesname, language=language, @@ -6097,13 +6182,23 @@ class AddShows(Home): network=network_name, url_src_db=base_url % cur_show_info.id, )) + if p_ref: + filtered[-1].update(dict( + p_name=p_item.name or None, + p_ref=p_ref, + p_chars=self._make_char_person_list(cur_show_info) + )) except (BaseException, Exception): pass - kwargs.update(dict(oldest=oldest, newest=newest)) - kwargs.update(dict(footnote=footnote, use_votes=False, use_networks=use_networks)) + kwargs.update(dict(oldest=oldest, newest=newest, oldest_dt=oldest_dt, newest_dt=newest_dt)) - if mode: + params = dict(footnote=footnote, use_votes=False, use_networks=use_networks) + if p_ref: + params.update(dict(use_ratings=False)) + kwargs.update(params) + + if mode and self.allow_browse_mru(mode): func = 'tvm_%s' % mode if callable(getattr(self, func, None)): sickgear.TVM_MRU = func @@ -6200,7 +6295,8 @@ class AddShows(Home): t = PageTemplate(web_handler=self, file='home_browseShows.tmpl') t.submenu = self.home_menu() t.browse_type = browse_type - t.browse_title = browse_title + t.browse_title = browse_title if ('person' != kwargs.get('mode') or not shows) \ + else f'{shows[0].get("p_name", "")} (Person) on {browse_type}' t.p_ref = (0 < len(shows) and shows[0].get('p_ref')) or None t.saved_showfilter = sickgear.BROWSELIST_MRU.get(browse_type, {}).get('showfilter', '') t.saved_showsort = sickgear.BROWSELIST_MRU.get(browse_type, {}).get('showsort', '*,asc,by_order')