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']),