class ApiHelper: ''' A class with common VRT NU API functionality ''' _VRT_BASE = 'https://www.vrt.be' _VRTNU_SEARCH_URL = 'https://vrtnu-api.vrt.be/search' _VRTNU_SUGGEST_URL = 'https://vrtnu-api.vrt.be/suggest' _VRTNU_SCREENSHOT_URL = 'https://vrtnu-api.vrt.be/screenshots' def __init__(self, _favorites, _resumepoints): ''' Constructor for the ApiHelper class ''' self._favorites = _favorites self._resumepoints = _resumepoints self._metadata = Metadata(_favorites, _resumepoints) install_opener(build_opener(ProxyHandler(get_proxies()))) def get_tvshows(self, category=None, channel=None, feature=None): ''' Get all TV shows for a given category, channel or feature, optionally filtered by favorites ''' params = dict() if category: params['facets[categories]'] = category cache_file = 'category.%s.json' % category if channel: params['facets[programBrands]'] = channel cache_file = 'channel.%s.json' % channel if feature: params['facets[programTags.title]'] = feature cache_file = 'featured.%s.json' % feature # If no facet-selection is done, we return the 'All programs' listing if not category and not channel and not feature: params['facets[transcodingStatus]'] = 'AVAILABLE' # Required for getting results in Suggests API cache_file = 'programs.json' tvshows = get_cache(cache_file, ttl=60 * 60) # Try the cache if it is fresh if not tvshows: from json import load querystring = '&'.join('{}={}'.format(key, value) for key, value in list(params.items())) suggest_url = self._VRTNU_SUGGEST_URL + '?' + querystring log(2, 'URL get: {url}', url=unquote(suggest_url)) tvshows = load(urlopen(suggest_url)) update_cache(cache_file, tvshows) return tvshows def list_tvshows(self, category=None, channel=None, feature=None, use_favorites=False): ''' List all TV shows for a given category, channel or feature, optionally filtered by favorites ''' # Get tvshows tvshows = self.get_tvshows(category=category, channel=channel, feature=feature) # Get oneoffs if get_setting('showoneoff', 'true') == 'true': cache_file = 'oneoff.json' oneoffs = self.get_episodes(variety='oneoff', cache_file=cache_file) else: # Return empty list oneoffs = [] return self.__map_tvshows(tvshows, oneoffs, use_favorites=use_favorites, cache_file=cache_file) def tvshow_to_listitem(self, tvshow, program, cache_file): ''' Return a ListItem based on a Suggests API result ''' label = self._metadata.get_label(tvshow) if program: context_menu, favorite_marker, _ = self._metadata.get_context_menu(tvshow, program, cache_file) label += favorite_marker return TitleItem( title=label, path=url_for('programs', program=program), art_dict=self._metadata.get_art(tvshow), info_dict=self._metadata.get_info_labels(tvshow), context_menu=context_menu, ) def list_episodes(self, program=None, season=None, category=None, feature=None, programtype=None, page=None, use_favorites=False, variety=None): ''' Construct a list of episode or season TitleItems from VRT NU Search API data and filtered by favorites ''' # Caching cache_file = None if variety: if use_favorites: cache_file = 'my-%s-%s.json' % (variety, page) else: cache_file = '%s-%s.json' % (variety, page) # Titletype titletype = None if variety: titletype = variety # Get data from api or cache episodes = self.get_episodes(program=program, season=season, category=category, feature=feature, programtype=programtype, page=page, use_favorites=use_favorites, variety=variety, cache_file=cache_file) if isinstance(episodes, tuple): seasons = episodes[0] episodes = episodes[1] return self.__map_seasons(program, seasons, episodes) return self.__map_episodes(episodes, titletype=titletype, season=season, use_favorites=use_favorites, cache_file=cache_file) def __map_episodes(self, episodes, titletype=None, season=None, use_favorites=False, cache_file=None): ''' Construct a list of TV show episodes TitleItems based on Search API query and filtered by favorites ''' episode_items = [] sort = 'episode' ascending = True content = 'episodes' if use_favorites: favorite_programs = self._favorites.programs() for episode in episodes: # VRT API workaround: seasonTitle facet behaves as a partial match regex, # so we have to filter out the episodes from seasons that don't exactly match. if season and season != 'allseasons' and episode.get('seasonTitle') != season: continue program = statichelper.url_to_program(episode.get('programUrl')) if use_favorites and program not in favorite_programs: continue # Support search highlights highlight = episode.get('highlight') if highlight: for key in highlight: episode[key] = statichelper.convert_html_to_kodilabel(highlight.get(key)[0]) list_item, sort, ascending = self.episode_to_listitem(episode, program, cache_file, titletype) episode_items.append(list_item) return episode_items, sort, ascending, content def __map_seasons(self, program, seasons, episodes): import random season_items = [] sort = 'label' ascending = True content = 'seasons' episode = random.choice(episodes) info_labels = self._metadata.get_info_labels(episode, season=True) program_type = episode.get('programType') # Reverse sort seasons if program_type is 'reeksaflopend' or 'daily' if program_type in ('daily', 'reeksaflopend'): ascending = False # Add an "* All seasons" list item if get_global_setting('videolibrary.showallitems') is True: season_items.append(TitleItem( title=localize(30133), path=url_for('programs', program=program, season='allseasons'), art_dict=self._metadata.get_art(episode, season='allseasons'), info_dict=info_labels, )) # NOTE: Sort the episodes ourselves, because Kodi does not allow to set to 'ascending' seasons = sorted(seasons, key=lambda k: k['key'], reverse=not ascending) for season in seasons: season_key = season.get('key', '') try: # If more than 300 episodes exist, we may end up with an empty season (Winteruur) episode = random.choice([e for e in episodes if e.get('seasonName') == season_key]) except IndexError: episode = episodes[0] label = '%s %s' % (localize(30131), season_key) season_items.append(TitleItem( title=label, path=url_for('programs', program=program, season=season_key), art_dict=self._metadata.get_art(episode, season=True), info_dict=info_labels, prop_dict=self._metadata.get_properties(episode), )) return season_items, sort, ascending, content def __map_tvshows(self, tvshows, oneoffs, use_favorites=False, cache_file=None): ''' Construct a list of TV show and Oneoff TitleItems and filtered by favorites ''' items = [] if use_favorites: favorite_programs = self._favorites.programs() # Create list of oneoff programs from oneoff episodes oneoff_programs = [statichelper.url_to_program(episode.get('programUrl')) for episode in oneoffs] for tvshow in tvshows: program = statichelper.url_to_program(tvshow.get('programUrl')) if use_favorites and program not in favorite_programs: continue if program in oneoff_programs: # Add the oneoff listitem(s), yes, we can't guarantee there's only one per program so attempt to list all for index in [n for n, o in enumerate(oneoff_programs) if o == program]: items.append(self.episode_to_listitem(oneoffs[index], program, cache_file, titletype='oneoff')[0]) else: # Add the tvshow listitem items.append(self.tvshow_to_listitem(tvshow, program, cache_file)) return items def episode_to_listitem(self, episode, program, cache_file, titletype): ''' Return a ListItem based on a Search API result ''' label, sort, ascending = self._metadata.get_label(episode, titletype, return_sort=True) if program: context_menu, favorite_marker, watchlater_marker = self._metadata.get_context_menu(episode, program, cache_file) label += favorite_marker + watchlater_marker info_labels = self._metadata.get_info_labels(episode) info_labels['title'] = label return TitleItem( title=label, path=url_for('play_id', video_id=episode.get('videoId'), publication_id=episode.get('publicationId')), art_dict=self._metadata.get_art(episode), info_dict=info_labels, prop_dict=self._metadata.get_properties(episode), context_menu=context_menu, is_playable=True, ), sort, ascending def list_search(self, keywords, page=0): ''' Search VRT NU content for a given string ''' episodes = self.get_episodes(keywords=keywords, page=page) return self.__map_episodes(episodes, titletype='recent') def get_upnext(self, info): ''' Get up next data from VRT Search API ''' program = info.get('program') path = info.get('path') season = None current_ep_no = None # Get current episode unique identifier ep_id = statichelper.play_url_to_id(path) # Get all episodes from current program and sort by program, seasonTitle and episodeNumber episodes = sorted(self.get_episodes(keywords=program), key=lambda k: (k.get('program'), k.get('seasonTitle'), k.get('episodeNumber'))) upnext = dict() for episode in episodes: if ep_id.get('whatson_id') == episode.get('whatsonId') or \ ep_id.get('video_id') == episode.get('videoId') or \ ep_id.get('video_url') == episode.get('url'): season = episode.get('seasonTitle') current_ep_no = episode.get('episodeNumber') program = episode.get('program') try: upnext['current'] = episode next_episode = episodes[episodes.index(episode) + 1] upnext['next'] = next_episode if next_episode.get('program') == program else None except IndexError: pass if upnext.get('next'): current_ep = upnext.get('current') next_ep = upnext.get('next') art = self._metadata.get_art(current_ep) current_episode = dict( episodeid=current_ep.get('whatsonId'), tvshowid=current_ep.get('programWhatsonId'), title=self._metadata.get_plotoutline(current_ep), art={ 'tvshow.poster': art.get('thumb'), 'thumb': art.get('thumb'), 'tvshow.fanart': art.get('fanart'), 'tvshow.landscape': art.get('thumb'), 'tvshow.clearart': None, 'tvshow.clearlogo': None, }, plot=self._metadata.get_plot(current_ep), showtitle=self._metadata.get_tvshowtitle(current_ep), playcount=info.get('playcount'), season=self._metadata.get_season(current_ep), episode=self._metadata.get_episode(current_ep), rating=info.get('rating'), firstaired=self._metadata.get_aired(current_ep), runtime=info.get('runtime'), ) art = self._metadata.get_art(next_ep) next_episode = dict( episodeid=next_ep.get('whatsonId'), tvshowid=next_ep.get('programWhatsonId'), title=self._metadata.get_plotoutline(next_ep), art={ 'tvshow.poster': art.get('thumb'), 'thumb': art.get('thumb'), 'tvshow.fanart': art.get('fanart'), 'tvshow.landscape': art.get('thumb'), 'tvshow.clearart': None, 'tvshow.clearlogo': None, }, plot=self._metadata.get_plot(next_ep), showtitle=self._metadata.get_tvshowtitle(next_ep), playcount=None, season=self._metadata.get_season(next_ep), episode=self._metadata.get_episode(next_ep), rating=None, firstaired=self._metadata.get_aired(next_ep), runtime=self._metadata.get_duration(next_ep), ) play_info = dict( whatson_id=next_ep.get('whatsonId'), ) next_info = dict( current_episode=current_episode, next_episode=next_episode, play_info=play_info, notification_time=SECONDS_MARGIN, ) return next_info if upnext.get('current'): if upnext.get('current').get('episodeNumber') == upnext.get('current').get('seasonNbOfEpisodes'): log_error(message='[Up Next] Last episode of season, next season not implemented for "{program} S{season}E{episode}"', program=program, season=season, episode=current_ep_no) return None log_error(message='[Up Next] No api data found for "{program}s S{season}E{episode}"', program=program, season=season, episode=current_ep_no) return None def get_single_episode(self, whatson_id): ''' Get single episode by whatsonId ''' video = None api_data = self.get_episodes(whatson_id=whatson_id, variety='single') if len(api_data) == 1: episode = api_data[0] video_item = TitleItem( title=self._metadata.get_label(episode), art_dict=self._metadata.get_art(episode), info_dict=self._metadata.get_info_labels(episode), prop_dict=self._metadata.get_properties(episode), ) video = dict(listitem=video_item, video_id=episode.get('videoId'), publication_id=episode.get('publicationId')) return video def get_episode_by_air_date(self, channel_name, start_date, end_date=None): ''' Get an episode of a program given the channel and the air date in iso format (2019-07-06T19:35:00) ''' channel = statichelper.find_entry(CHANNELS, 'name', channel_name) if not channel: return None from datetime import datetime, timedelta import dateutil.parser import dateutil.tz offairdate = None try: onairdate = dateutil.parser.parse(start_date, default=datetime.now(dateutil.tz.gettz('Europe/Brussels'))) except ValueError: return None if end_date: try: offairdate = dateutil.parser.parse(end_date, default=datetime.now(dateutil.tz.gettz('Europe/Brussels'))) except ValueError: return None video = None episode_guess_off = None now = datetime.now(dateutil.tz.gettz('Europe/Brussels')) if onairdate.hour < 6: schedule_date = onairdate - timedelta(days=1) else: schedule_date = onairdate schedule_datestr = schedule_date.isoformat().split('T')[0] url = 'https://www.vrt.be/bin/epg/schedule.%s.json' % schedule_datestr from json import load schedule_json = load(urlopen(url)) episodes = schedule_json.get(channel.get('id'), []) if not episodes: return None if offairdate: mindate = min(abs(offairdate - dateutil.parser.parse(episode.get('endTime'))) for episode in episodes) episode_guess_off = next((episode for episode in episodes if abs(offairdate - dateutil.parser.parse(episode.get('endTime'))) == mindate), None) mindate = min(abs(onairdate - dateutil.parser.parse(episode.get('startTime'))) for episode in episodes) episode_guess_on = next((episode for episode in episodes if abs(onairdate - dateutil.parser.parse(episode.get('startTime'))) == mindate), None) offairdate_guess = dateutil.parser.parse(episode_guess_on.get('endTime')) if (episode_guess_off and episode_guess_on.get('vrt.whatson-id') == episode_guess_off.get('vrt.whatson-id') or (not episode_guess_off and episode_guess_on)): video = self.get_single_episode(episode_guess_on.get('vrt.whatson-id')) if video: return video # Airdate live2vod feature: use livestream cache of last 24 hours if no video was found if now - timedelta(hours=24) <= dateutil.parser.parse(episode_guess_on.get('endTime')) <= now: start_date = onairdate.astimezone(dateutil.tz.UTC).isoformat()[0:19] end_date = offairdate_guess.astimezone(dateutil.tz.UTC).isoformat()[0:19] # Offairdate defined if offairdate and now - timedelta(hours=24) <= offairdate <= now: start_date = onairdate.astimezone(dateutil.tz.UTC).isoformat()[0:19] end_date = offairdate.astimezone(dateutil.tz.UTC).isoformat()[0:19] if start_date and end_date: video_item = TitleItem( title=self._metadata.get_label(episode_guess_on), art_dict=self._metadata.get_art(episode_guess_on), info_dict=self._metadata.get_info_labels(episode_guess_on, channel=channel, date=start_date), prop_dict=self._metadata.get_properties(episode_guess_on), ) video = dict( listitem=video_item, video_id=channel.get('live_stream_id'), start_date=start_date, end_date=end_date, ) return video video = dict( errorlabel=episode_guess_on.get('title') ) return video def get_latest_episode(self, program): ''' Get the latest episode of a program ''' api_data = self.get_episodes(program=program, variety='single') if len(api_data) != 1: return None episode = api_data[0] log(2, str(episode)) video_item = TitleItem( title=self._metadata.get_label(episode), art_dict=self._metadata.get_art(episode), info_dict=self._metadata.get_info_labels(episode), prop_dict=self._metadata.get_properties(episode), ) video = dict(listitem=video_item, video_id=episode.get('videoId'), publication_id=episode.get('publicationId')) return video def get_episodes(self, program=None, season=None, episodes=None, category=None, feature=None, programtype=None, keywords=None, whatson_id=None, video_id=None, video_url=None, page=None, use_favorites=False, variety=None, cache_file=None): ''' Get episodes or season data from VRT NU Search API ''' # Contruct params if page: page = statichelper.realpage(page) all_items = False params = { 'from': ((page - 1) * 50) + 1, 'i': 'video', 'size': 50, } elif variety == 'single': all_items = False params = { 'i': 'video', 'size': '1', } else: all_items = True params = { 'i': 'video', 'size': '300', } if variety: season = 'allseasons' if variety == 'offline': from datetime import datetime import dateutil.tz params['facets[assetOffTime]'] = datetime.now(dateutil.tz.gettz('Europe/Brussels')).strftime('%Y-%m-%d') if variety == 'oneoff': params['facets[programType]'] = 'oneoff' if variety == 'watchlater': self._resumepoints.refresh(ttl=5 * 60) episode_urls = self._resumepoints.watchlater_urls() params['facets[url]'] = '[%s]' % (','.join(episode_urls)) if variety == 'continue': self._resumepoints.refresh(ttl=5 * 60) episode_urls = self._resumepoints.resumepoints_urls() params['facets[url]'] = '[%s]' % (','.join(episode_urls)) if use_favorites: program_urls = [statichelper.program_to_url(p, 'medium') for p in self._favorites.programs()] params['facets[programUrl]'] = '[%s]' % (','.join(program_urls)) elif variety in ('offline', 'recent'): channel_filter = [channel.get('name') for channel in CHANNELS if get_setting(channel.get('name'), 'true') == 'true'] params['facets[programBrands]'] = '[%s]' % (','.join(channel_filter)) if program: params['facets[programUrl]'] = statichelper.program_to_url(program, 'medium') if season and season != 'allseasons': params['facets[seasonTitle]'] = season if episodes: params['facets[episodeNumber]'] = '[%s]' % (','.join(str(episode) for episode in episodes)) if category: params['facets[categories]'] = category if feature: params['facets[programTags.title]'] = feature if programtype: params['facets[programType]'] = programtype if keywords: if not season: season = 'allseasons' params['q'] = quote_plus(statichelper.from_unicode(keywords)) params['highlight'] = 'true' if whatson_id: params['facets[whatsonId]'] = whatson_id if video_id: params['facets[videoId]'] = video_id if video_url: params['facets[url]'] = video_url # Construct VRT NU Search API Url and get api data querystring = '&'.join('{}={}'.format(key, value) for key, value in list(params.items())) search_url = self._VRTNU_SEARCH_URL + '?' + querystring.replace(' ', '%20') # Only encode spaces to minimize url length from json import load if cache_file: # Get api data from cache if it is fresh search_json = get_cache(cache_file, ttl=60 * 60) if not search_json: log(2, 'URL get: {url}', url=unquote(search_url)) req = Request(search_url) try: search_json = load(urlopen(req)) except (TypeError, ValueError): # No JSON object could be decoded return [] except HTTPError as exc: url_length = len(req.get_selector()) if exc.code == 413 and url_length > 8192: ok_dialog(heading='HTTP Error 413', message=localize(30967)) log_error('HTTP Error 413: Exceeded maximum url length: ' 'VRT Search API url has a length of {length} characters.', length=url_length) return [] if exc.code == 400 and 7600 <= url_length <= 8192: ok_dialog(heading='HTTP Error 400', message=localize(30967)) log_error('HTTP Error 400: Probably exceeded maximum url length: ' 'VRT Search API url has a length of {length} characters.', length=url_length) return [] raise update_cache(cache_file, search_json) else: log(2, 'URL get: {url}', url=unquote(search_url)) search_json = load(urlopen(search_url)) # Check for multiple seasons seasons = None if 'facets[seasonTitle]' not in unquote(search_url): facets = search_json.get('facets', dict()).get('facets') seasons = next((f.get('buckets', []) for f in facets if f.get('name') == 'seasons' and len(f.get('buckets', [])) > 1), None) episodes = search_json.get('results', [{}]) show_seasons = bool(season != 'allseasons') # Return seasons if show_seasons and seasons: return (seasons, episodes) api_pages = search_json.get('meta').get('pages').get('total') api_page_size = search_json.get('meta').get('pages').get('size') total_results = search_json.get('meta').get('total_results') if all_items and total_results > api_page_size: for api_page in range(1, api_pages): api_page_url = search_url + '&from=' + str(api_page * api_page_size + 1) api_page_json = load(urlopen(api_page_url)) episodes += api_page_json.get('results', [{}]) # Return episodes return episodes def get_live_screenshot(self, channel): ''' Get a live screenshot for a given channel, only supports Eén, Canvas and Ketnet ''' url = '%s/%s.jpg' % (self._VRTNU_SCREENSHOT_URL, channel) delete_cached_thumbnail(url) return url def list_channels(self, channels=None, live=True): ''' Construct a list of channel ListItems, either for Live TV or the TV Guide listing ''' from tvguide import TVGuide _tvguide = TVGuide() channel_items = [] for channel in CHANNELS: if channels and channel.get('name') not in channels: continue context_menu = [] art_dict = dict() # Try to use the white icons for thumbnails (used for icons as well) if has_addon('resource.images.studios.white'): art_dict['thumb'] = 'resource://resource.images.studios.white/{studio}.png'.format(**channel) else: art_dict['thumb'] = 'DefaultTags.png' if not live: path = url_for('channels', channel=channel.get('name')) label = channel.get('label') plot = '[B]%s[/B]' % channel.get('label') is_playable = False info_dict = dict(title=label, plot=plot, studio=channel.get('studio'), mediatype='video') stream_dict = [] elif channel.get('live_stream') or channel.get('live_stream_id'): if channel.get('live_stream_id'): path = url_for('play_id', video_id=channel.get('live_stream_id')) elif channel.get('live_stream'): path = url_for('play_url', video_url=channel.get('live_stream')) label = localize(30141, **channel) # Channel live playing_now = _tvguide.playing_now(channel.get('name')) if playing_now: label += ' [COLOR yellow]| %s[/COLOR]' % playing_now # A single Live channel means it is the entry for channel's TV Show listing, so make it stand out if channels and len(channels) == 1: label = '[B]%s[/B]' % label is_playable = True if channel.get('name') in ['een', 'canvas', 'ketnet']: if get_setting('showfanart', 'true') == 'true': art_dict['fanart'] = self.get_live_screenshot(channel.get('name', art_dict.get('fanart'))) plot = '%s\n\n%s' % (localize(30142, **channel), _tvguide.live_description(channel.get('name'))) else: plot = localize(30142, **channel) # Watch live # NOTE: Playcount is required to not have live streams as "Watched" info_dict = dict(title=label, plot=plot, studio=channel.get('studio'), mediatype='video', playcount=0, duration=0) stream_dict = dict(duration=0) context_menu.append(( localize(30413), 'RunPlugin(%s)' % url_for('delete_cache', cache_file='channel.%s.json' % channel) )) else: # Not a playable channel continue channel_items.append(TitleItem( title=label, path=path, art_dict=art_dict, info_dict=info_dict, stream_dict=stream_dict, context_menu=context_menu, is_playable=is_playable, )) return channel_items @staticmethod def list_youtube(channels=None): ''' Construct a list of youtube ListItems, either for Live TV or the TV Guide listing ''' youtube_items = [] if not has_addon('plugin.video.youtube') or get_setting('showyoutube', 'true') == 'false': return youtube_items for channel in CHANNELS: if channels and channel.get('name') not in channels: continue context_menu = [] art_dict = dict() # Try to use the white icons for thumbnails (used for icons as well) if has_addon('resource.images.studios.white'): art_dict['thumb'] = 'resource://resource.images.studios.white/{studio}.png'.format(**channel) else: art_dict['thumb'] = 'DefaultTags.png' if channel.get('youtube'): path = channel.get('youtube') label = localize(30143, **channel) # Channel on YouTube # A single Live channel means it is the entry for channel's TV Show listing, so make it stand out if channels and len(channels) == 1: label = '[B]%s[/B]' % label plot = localize(30144, **channel) # Watch on YouTube # NOTE: Playcount is required to not have live streams as "Watched" info_dict = dict(title=label, plot=plot, studio=channel.get('studio'), mediatype='video', playcount=0) context_menu.append(( localize(30413), 'RunPlugin(%s)' % url_for('delete_cache', cache_file='channel.%s.json' % channel) )) else: # Not a playable channel continue youtube_items.append(TitleItem( title=label, path=path, art_dict=art_dict, info_dict=info_dict, context_menu=context_menu, is_playable=False, )) return youtube_items def list_featured(self): ''' Construct a list of featured Listitems ''' from data import FEATURED featured_items = [] for feature in self.localize_features(FEATURED): featured_name = feature.get('name') featured_items.append(TitleItem( title=featured_name, path=url_for('featured', feature=feature.get('id')), art_dict=dict(thumb='DefaultCountry.png'), info_dict=dict(plot='[B]%s[/B]' % feature.get('name'), studio='VRT'), )) return featured_items @staticmethod def localize_features(featured): ''' Return a localized and sorted listing ''' from copy import deepcopy features = deepcopy(featured) for feature in features: for key, val in list(feature.items()): if key == 'name': feature[key] = localize_from_data(val, featured) return sorted(features, key=lambda x: x.get('name')) def list_categories(self): ''' Construct a list of category ListItems ''' categories = [] # Try the cache if it is fresh categories = get_cache('categories.json', ttl=7 * 24 * 60 * 60) # Try to scrape from the web if not categories: try: categories = self.get_categories() except Exception: # pylint: disable=broad-except categories = [] else: update_cache('categories.json', categories) # Use the cache anyway (better than hard-coded) if not categories: categories = get_cache('categories.json', ttl=None) # Fall back to internal hard-coded categories if all else fails from data import CATEGORIES if not categories: categories = CATEGORIES category_items = [] for category in self.localize_categories(categories, CATEGORIES): if get_setting('showfanart', 'true') == 'true': thumbnail = category.get('thumbnail', 'DefaultGenre.png') else: thumbnail = 'DefaultGenre.png' category_items.append(TitleItem( title=category.get('name'), path=url_for('categories', category=category.get('id')), art_dict=dict(thumb=thumbnail, icon='DefaultGenre.png'), info_dict=dict(plot='[B]%s[/B]' % category.get('name'), studio='VRT'), )) return category_items @staticmethod def localize_categories(categories, categories2): ''' Return a localized and sorted listing ''' for category in categories: for key, val in list(category.items()): if key == 'name': category[key] = localize_from_data(val, categories2) return sorted(categories, key=lambda x: x.get('name')) def get_categories(self): ''' Return a list of categories by scraping the website ''' from bs4 import BeautifulSoup, SoupStrainer log(2, 'URL get: https://www.vrt.be/vrtnu/categorieen/') response = urlopen('https://www.vrt.be/vrtnu/categorieen/') tiles = SoupStrainer('nui-list--content') soup = BeautifulSoup(response.read(), 'html.parser', parse_only=tiles) categories = [] for tile in soup.find_all('nui-tile'): categories.append(dict( id=tile.get('href').split('/')[-2], thumbnail=self.get_category_thumbnail(tile), name=self.get_category_title(tile), )) return categories @staticmethod def get_category_thumbnail(element): ''' Return a category thumbnail, if available ''' if get_setting('showfanart', 'true') == 'true': raw_thumbnail = element.find(class_='media').get('data-responsive-image', 'DefaultGenre.png') return statichelper.add_https_method(raw_thumbnail) return 'DefaultGenre.png' @staticmethod def get_category_title(element): ''' Return a category title, if available ''' found_element = element.find('a') if found_element: return statichelper.strip_newlines(found_element.contents[0]) # FIXME: We should probably fall back to something sensible here, or raise an exception instead return ''
class ApiHelper: """A class with common VRT NU API functionality""" _VRT_BASE = 'https://www.vrt.be' _VRTNU_SEARCH_URL = 'https://vrtnu-api.vrt.be/search' _VRTNU_SUGGEST_URL = 'https://vrtnu-api.vrt.be/suggest' _VRTNU_SCREENSHOT_URL = 'https://vrtnu-api.vrt.be/screenshots' def __init__(self, _favorites, _resumepoints): """Constructor for the ApiHelper class""" self._favorites = _favorites self._resumepoints = _resumepoints self._metadata = Metadata(_favorites, _resumepoints) def get_tvshows(self, category=None, channel=None, feature=None): """Get all TV shows for a given category, channel or feature, optionally filtered by favorites""" params = {} if category: params['facets[categories]'] = category cache_file = 'category.{category}.json'.format(category=category) if channel: params['facets[programBrands]'] = channel cache_file = 'channel.{channel}.json'.format(channel=channel) if feature: params['facets[programTags.title]'] = feature cache_file = 'featured.{feature}.json'.format(feature=feature) # If no facet-selection is done, we return the 'All programs' listing if not category and not channel and not feature: params[ 'facets[transcodingStatus]'] = 'AVAILABLE' # Required for getting results in Suggests API cache_file = 'programs.json' querystring = '&'.join('{}={}'.format(key, value) for key, value in list(params.items())) suggest_url = self._VRTNU_SUGGEST_URL + '?' + querystring return get_cached_url_json(url=suggest_url, cache=cache_file, ttl=ttl('indirect'), fail=[]) def list_tvshows(self, category=None, channel=None, feature=None, use_favorites=False): """List all TV shows for a given category, channel or feature, optionally filtered by favorites""" # Get tvshows tvshows = self.get_tvshows(category=category, channel=channel, feature=feature) # Get oneoffs if get_setting_bool('showoneoff', default=True): cache_file = 'oneoff.json' oneoffs = self.get_episodes(variety='oneoff', cache_file=cache_file) else: cache_file = None # Return empty list oneoffs = [] return self.__map_tvshows(tvshows, oneoffs, use_favorites=use_favorites, cache_file=cache_file) def tvshow_to_listitem(self, tvshow, program, cache_file): """Return a ListItem based on a Suggests API result""" label = self._metadata.get_label(tvshow) if program: context_menu, favorite_marker, _ = self._metadata.get_context_menu( tvshow, program, cache_file) label += favorite_marker return TitleItem( label=label, path=url_for('programs', program=program), art_dict=self._metadata.get_art(tvshow), info_dict=self._metadata.get_info_labels(tvshow), context_menu=context_menu, ) def list_episodes(self, program=None, season=None, category=None, feature=None, programtype=None, page=None, items_per_page=None, use_favorites=False, variety=None, sort_key=None): """Construct a list of episode or season TitleItems from VRT NU Search API data and filtered by favorites""" # Caching if not variety: cache_file = None else: cache_file = '{my}{variety}{page}.json'.format( my='my-' if use_favorites else '', variety=variety, page='-{}'.format(page) if sort_key is None else '', ) # Titletype titletype = None if variety: titletype = variety # Get data from api or cache if sort_key: episodes = self.get_episodes(program=program, season=season, category=category, feature=feature, programtype=programtype, use_favorites=use_favorites, variety=variety, cache_file=cache_file) episodes = sorted( episodes, key=lambda k: k[sort_key])[(page * items_per_page) - items_per_page:page * items_per_page] else: episodes = self.get_episodes(program=program, season=season, category=category, feature=feature, programtype=programtype, page=page, use_favorites=use_favorites, variety=variety, cache_file=cache_file) if isinstance(episodes, tuple): seasons = episodes[0] episodes = episodes[1] return self.__map_seasons(program, seasons, episodes) return self.__map_episodes(episodes, titletype=titletype, season=season, use_favorites=use_favorites, cache_file=cache_file) def __map_episodes(self, episodes, titletype=None, season=None, use_favorites=False, cache_file=None): """Construct a list of TV show episodes TitleItems based on Search API query and filtered by favorites""" episode_items = [] sort = 'episode' ascending = True content = 'episodes' if use_favorites: favorite_programs = self._favorites.programs() for episode in episodes: # VRT API workaround: seasonTitle facet behaves as a partial match regex, # so we have to filter out the episodes from seasons that don't exactly match. if season and season != 'allseasons' and episode.get( 'seasonTitle') != season: continue program = url_to_program(episode.get('programUrl')) if use_favorites and program not in favorite_programs: continue # Support search highlights highlight = episode.get('highlight') if highlight: for key in highlight: episode[key] = html_to_kodi(highlight.get(key)[0]) list_item, sort, ascending = self.episode_to_listitem( episode, program, cache_file, titletype) episode_items.append(list_item) return episode_items, sort, ascending, content def __map_seasons(self, program, seasons, episodes): import random season_items = [] sort = 'label' ascending = True content = 'files' episode = random.choice(episodes) program_type = episode.get('programType') # Reverse sort seasons if program_type is 'reeksaflopend' or 'daily' if program_type in ('daily', 'reeksaflopend'): ascending = False # Add an "* All seasons" list item if get_global_setting('videolibrary.showallitems') is True: season_items.append( TitleItem( label=localize(30133), # All seasons path=url_for('programs', program=program, season='allseasons'), art_dict=self._metadata.get_art(episode, season='allseasons'), info_dict=dict( tvshowtitle=self._metadata.get_tvshowtitle(episode), plot=self._metadata.get_plot(episode, season='allseasons'), plotoutline=self._metadata.get_plotoutline( episode, season='allseasons'), tagline=self._metadata.get_plotoutline( episode, season='allseasons'), mediatype=self._metadata.get_mediatype( episode, season='allseasons'), studio=self._metadata.get_studio(episode), tag=self._metadata.get_tag(episode)), )) # NOTE: Sort the episodes ourselves, because Kodi does not allow to set to 'ascending' seasons = sorted(seasons, key=lambda k: k['key'], reverse=not ascending) for season in seasons: season_key = season.get('key', '') # If more than 300 episodes exist, we may end up with an empty season (Winteruur) try: episode = random.choice( [e for e in episodes if e.get('seasonName') == season_key]) except IndexError: episode = episodes[0] season_items.append( TitleItem( label=self._metadata.get_title(episode, season=season_key), path=url_for('programs', program=program, season=season_key), art_dict=self._metadata.get_art(episode, season=season_key), info_dict=self._metadata.get_info_labels( episode, season=season_key), prop_dict=self._metadata.get_properties(episode), )) return season_items, sort, ascending, content def __map_tvshows(self, tvshows, oneoffs, use_favorites=False, cache_file=None): """Construct a list of TV show and Oneoff TitleItems and filtered by favorites""" items = [] if use_favorites: favorite_programs = self._favorites.programs() # Create list of oneoff programs from oneoff episodes oneoff_programs = [ url_to_program(episode.get('programUrl')) for episode in oneoffs ] for tvshow in tvshows: program = url_to_program(tvshow.get('programUrl')) if use_favorites and program not in favorite_programs: continue if program in oneoff_programs: # Add the oneoff listitem(s), yes, we can't guarantee there's only one per program so attempt to list all for index in [ n for n, o in enumerate(oneoff_programs) if o == program ]: items.append( self.episode_to_listitem(oneoffs[index], program, cache_file, titletype='oneoff')[0]) else: # Add the tvshow listitem items.append( self.tvshow_to_listitem(tvshow, program, cache_file)) return items def episode_to_listitem(self, episode, program, cache_file, titletype): """Return a ListItem based on a Search API result""" label, sort, ascending = self._metadata.get_label(episode, titletype, return_sort=True) if program: context_menu, favorite_marker, watchlater_marker = self._metadata.get_context_menu( episode, program, cache_file) label += favorite_marker + watchlater_marker info_labels = self._metadata.get_info_labels(episode) # FIXME: Due to a bug in Kodi, ListItem.Title is used when Sort methods are used, not ListItem.Label info_labels['title'] = label return TitleItem( label=label, path=url_for('play_id', video_id=episode.get('videoId'), publication_id=episode.get('publicationId')), art_dict=self._metadata.get_art(episode), info_dict=info_labels, prop_dict=self._metadata.get_properties(episode), context_menu=context_menu, is_playable=True, ), sort, ascending def list_search(self, keywords, page=0): """Search VRT NU content for a given string""" episodes = self.get_episodes(keywords=keywords, page=page) return self.__map_episodes(episodes, titletype='recent') def get_upnext(self, info): """Get up next data from VRT Search API""" program = info.get('program') path = info.get('path') season = None current_ep_no = None # Get current episode unique identifier ep_id = play_url_to_id(path) # Get all episodes from current program and sort by program, seasonTitle and episodeNumber episodes = sorted( self.get_episodes(keywords=program), key=lambda k: (k.get('program'), k.get('seasonTitle'), k.get('episodeNumber'))) upnext = {} for episode in episodes: if ep_id.get('whatson_id') == episode.get('whatsonId') or \ ep_id.get('video_id') == episode.get('videoId') or \ ep_id.get('video_url') == episode.get('url'): season = episode.get('seasonTitle') current_ep_no = episode.get('episodeNumber') program = episode.get('program') upnext['current'] = episode try: next_episode = episodes[episodes.index(episode) + 1] except IndexError: pass else: if next_episode.get( 'program') == program and next_episode.get( 'episodeNumber') != current_ep_no: upnext['next'] = next_episode current_ep = upnext.get('current') next_ep = upnext.get('next') if next_ep is None: if current_ep is not None and current_ep.get( 'episodeNumber') == current_ep.get( 'seasonNbOfEpisodes') is not None: log(2, '[Up Next] Already at last episode of last season for {program} S{season}E{episode}', program=program, season=season, episode=current_ep_no) elif season and current_ep_no: log(2, '[Up Next] No api data found for {program} S{season}E{episode}', program=program, season=season, episode=current_ep_no) else: log(2, '[Up Next] No api data found for {program}', program=program) return None art = self._metadata.get_art(current_ep) current_episode = dict( episodeid=current_ep.get('videoId'), tvshowid=current_ep.get('programUrl'), title=self._metadata.get_title(current_ep), art={ 'tvshow.poster': art.get('thumb'), 'thumb': art.get('thumb'), 'tvshow.fanart': art.get('fanart'), 'tvshow.landscape': art.get('thumb'), 'tvshow.clearart': None, 'tvshow.clearlogo': None, }, plot=self._metadata.get_plot(current_ep), showtitle=self._metadata.get_tvshowtitle(current_ep), playcount=info.get('playcount'), season=self._metadata.get_season(current_ep), episode=self._metadata.get_episode(current_ep), rating=info.get('rating'), firstaired=self._metadata.get_aired(current_ep), runtime=info.get('runtime'), ) art = self._metadata.get_art(next_ep) next_episode = dict( episodeid=next_ep.get('videoId'), tvshowid=next_ep.get('programUrl'), title=self._metadata.get_title(next_ep), art={ 'tvshow.poster': art.get('thumb'), 'thumb': art.get('thumb'), 'tvshow.fanart': art.get('fanart'), 'tvshow.landscape': art.get('thumb'), 'tvshow.clearart': None, 'tvshow.clearlogo': None, }, plot=self._metadata.get_plot(next_ep), showtitle=self._metadata.get_tvshowtitle(next_ep), playcount=None, season=self._metadata.get_season(next_ep), episode=self._metadata.get_episode(next_ep), rating=None, firstaired=self._metadata.get_aired(next_ep), runtime=self._metadata.get_duration(next_ep), ) play_info = dict(video_id=next_ep.get('videoId'), ) next_info = dict( current_episode=current_episode, next_episode=next_episode, play_info=play_info, ) return next_info def get_single_episode_data(self, video_id=None, whatson_id=None, video_url=None): """Get single episode api data by videoId, whatsonId or url""" episode = None api_data = list() if video_id: api_data = self.get_episodes(video_id=video_id, variety='single') elif whatson_id: api_data = self.get_episodes(whatson_id=whatson_id, variety='single') elif video_url: api_data = self.get_episodes(video_url=video_url, variety='single') if len(api_data) == 1: episode = api_data[0] return episode def get_single_episode(self, video_id=None, whatson_id=None, video_url=None): """Get single episode by videoId, whatsonId or url""" video = None episode = self.get_single_episode_data(video_id=video_id, whatson_id=whatson_id, video_url=video_url) if episode: video_item = TitleItem( label=self._metadata.get_label(episode), art_dict=self._metadata.get_art(episode), info_dict=self._metadata.get_info_labels(episode), prop_dict=self._metadata.get_properties(episode), ) video = dict(listitem=video_item, video_id=episode.get('videoId'), publication_id=episode.get('publicationId')) return video def get_episode_by_air_date(self, channel_name, start_date, end_date=None): """Get an episode of a program given the channel and the air date in iso format (2019-07-06T19:35:00)""" channel = find_entry(CHANNELS, 'name', channel_name) if not channel: return None from datetime import datetime, timedelta import dateutil.parser import dateutil.tz offairdate = None try: onairdate = dateutil.parser.parse( start_date, default=datetime.now(dateutil.tz.gettz('Europe/Brussels'))) except ValueError: return None if end_date: try: offairdate = dateutil.parser.parse( end_date, default=datetime.now(dateutil.tz.gettz('Europe/Brussels'))) except ValueError: return None video = None now = datetime.now(dateutil.tz.gettz('Europe/Brussels')) if onairdate.hour < 6: schedule_date = onairdate - timedelta(days=1) else: schedule_date = onairdate schedule_datestr = schedule_date.isoformat().split('T')[0] url = 'https://www.vrt.be/bin/epg/schedule.{date}.json'.format( date=schedule_datestr) schedule_json = get_url_json(url, fail={}) episodes = schedule_json.get(channel.get('id'), []) if not episodes: return None # Guess the episode episode_guess = None if not offairdate: mindate = min( abs(onairdate - dateutil.parser.parse(episode.get('startTime'))) for episode in episodes) episode_guess = next((episode for episode in episodes if abs( onairdate - dateutil.parser.parse(episode.get('startTime'))) == mindate), None) else: duration = offairdate - onairdate midairdate = onairdate + timedelta( seconds=duration.total_seconds() / 2) mindate = min( abs(midairdate - (dateutil.parser.parse(episode.get('startTime')) + timedelta(seconds=(dateutil.parser.parse( episode.get('endTime')) - dateutil.parser.parse( episode.get('startTime'))).total_seconds() / 2))) for episode in episodes) episode_guess = next((episode for episode in episodes if abs( midairdate - (dateutil.parser.parse(episode.get('startTime')) + timedelta( seconds=(dateutil.parser.parse(episode.get('endTime')) - dateutil.parser.parse(episode.get('startTime')) ).total_seconds() / 2))) == mindate), None) if episode_guess and episode_guess.get('vrt.whatson-id', None): offairdate_guess = dateutil.parser.parse( episode_guess.get('endTime')) video = self.get_single_episode( whatson_id=episode_guess.get('vrt.whatson-id')) if video: return video # Airdate live2vod feature: use livestream cache of last 24 hours if no video was found if now - timedelta(hours=24) <= dateutil.parser.parse( episode_guess.get('endTime')) <= now: start_date = onairdate.astimezone( dateutil.tz.UTC).isoformat()[0:19] end_date = offairdate_guess.astimezone( dateutil.tz.UTC).isoformat()[0:19] # Offairdate defined if offairdate and now - timedelta(hours=24) <= offairdate <= now: start_date = onairdate.astimezone( dateutil.tz.UTC).isoformat()[:19] end_date = offairdate.astimezone( dateutil.tz.UTC).isoformat()[:19] if start_date and end_date: info = self._metadata.get_info_labels(episode_guess, channel=channel, date=start_date) live2vod_title = '{} ({})'.format( info.get('tvshowtitle'), localize(30454)) # from livestream cache info.update(tvshowtitle=live2vod_title) video_item = TitleItem( label=self._metadata.get_label(episode_guess), art_dict=self._metadata.get_art(episode_guess), info_dict=info, prop_dict=self._metadata.get_properties(episode_guess), ) video = dict( listitem=video_item, video_id=channel.get('live_stream_id'), start_date=start_date, end_date=end_date, ) return video video = dict(errorlabel=episode_guess.get('title')) return video def get_latest_episode(self, program): """Get the latest episode of a program""" api_data = self.get_episodes(program=program, variety='single') if len(api_data) != 1: return None episode = api_data[0] log(2, str(episode)) video_item = TitleItem( label=self._metadata.get_label(episode), art_dict=self._metadata.get_art(episode), info_dict=self._metadata.get_info_labels(episode), prop_dict=self._metadata.get_properties(episode), ) video = dict(listitem=video_item, video_id=episode.get('videoId'), publication_id=episode.get('publicationId')) return video def get_episodes(self, program=None, season=None, episodes=None, category=None, feature=None, programtype=None, keywords=None, whatson_id=None, video_id=None, video_url=None, page=None, use_favorites=False, variety=None, cache_file=None): """Get episodes or season data from VRT NU Search API""" # Contruct params if page: page = realpage(page) all_items = False items_per_page = get_setting_int('itemsperpage', default=50) params = { 'from': ((page - 1) * items_per_page) + 1, 'i': 'video', 'size': items_per_page, } elif variety == 'single': all_items = False params = { 'i': 'video', 'size': '1', } else: all_items = True params = { 'i': 'video', 'size': '300', } if variety: season = 'allseasons' if variety == 'offline': from datetime import datetime, timedelta import dateutil.tz now = datetime.now(dateutil.tz.gettz('Europe/Brussels')) off_dates = [(now + timedelta(days=day)).strftime('%Y-%m-%d') for day in range(0, 7)] params['facets[assetOffTime]'] = '[%s]' % (','.join(off_dates)) if variety == 'oneoff': params[ 'facets[episodeNumber]'] = '[0,1]' # This to avoid VRT NU metadata errors (see #670) params['facets[programType]'] = 'oneoff' if variety == 'watchlater': self._resumepoints.refresh(ttl=ttl('direct')) episode_urls = self._resumepoints.watchlater_urls() params['facets[url]'] = '[%s]' % (','.join(episode_urls)) if variety == 'continue': self._resumepoints.refresh(ttl=ttl('direct')) episode_urls = self._resumepoints.resumepoints_urls() params['facets[url]'] = '[%s]' % (','.join(episode_urls)) if use_favorites: program_urls = [ program_to_url(p, 'medium') for p in self._favorites.programs() ] params['facets[programUrl]'] = '[%s]' % ( ','.join(program_urls)) elif variety in ('offline', 'recent'): channel_filter = [] for channel in CHANNELS: if channel.get('vod') is True and get_setting_bool( channel.get('name'), default=True): channel_filter.append(channel.get('name')) params['facets[programBrands]'] = '[%s]' % ( ','.join(channel_filter)) if program: params['facets[programUrl]'] = program_to_url(program, 'medium') if season and season != 'allseasons': params['facets[seasonTitle]'] = season if episodes: params['facets[episodeNumber]'] = '[%s]' % (','.join( str(episode) for episode in episodes)) if category: params['facets[categories]'] = category if feature: params['facets[programTags.title]'] = feature if programtype: params['facets[programType]'] = programtype if keywords: if not season: season = 'allseasons' params['q'] = quote_plus(from_unicode(keywords)) params['highlight'] = 'true' if whatson_id: params['facets[whatsonId]'] = whatson_id if video_id: params['facets[videoId]'] = video_id if video_url: params['facets[url]'] = video_url # Construct VRT NU Search API Url and get api data querystring = '&'.join('{}={}'.format(key, value) for key, value in list(params.items())) search_url = self._VRTNU_SEARCH_URL + '?' + querystring.replace( ' ', '%20') # Only encode spaces to minimize url length if cache_file: search_json = get_cached_url_json(url=search_url, cache=cache_file, ttl=ttl('indirect'), fail={}) else: search_json = get_url_json(url=search_url, fail={}) # Check for multiple seasons seasons = [] if 'facets[seasonTitle]' not in unquote(search_url): facets = search_json.get('facets', {}).get('facets') if facets: seasons = next((f.get('buckets', []) for f in facets if f.get('name') == 'seasons' and len(f.get('buckets', [])) > 1), None) # Experimental: VRT Search API only returns a maximum of 10 seasons, to get all seasons we need to use the "model.json" API if seasons and program and len(seasons) == 10: season_json = get_url_json( 'https://www.vrt.be/vrtnu/a-z/%s.model.json' % program) season_items = None try: season_items = season_json.get(':items').get('parsys').get(':items').get('container') \ .get(':items').get('banner').get(':items').get('navigation').get(':items') except AttributeError: pass if season_items: seasons = [] for item in season_items: seasons.append(dict(key=item.lstrip('0'))) episodes = search_json.get('results', [{}]) show_seasons = bool(season != 'allseasons') # Return seasons if show_seasons and seasons: return (seasons, episodes) api_pages = search_json.get('meta').get('pages').get('total') api_page_size = search_json.get('meta').get('pages').get('size') total_results = search_json.get('meta').get('total_results') if all_items and total_results > api_page_size: for api_page in range(1, api_pages): api_page_url = search_url + '&from=' + str(api_page * api_page_size + 1) api_page_json = get_url_json(api_page_url) if api_page_json is not None: episodes += api_page_json.get('results', [{}]) # Return episodes return episodes def get_live_screenshot(self, channel): """Get a live screenshot for a given channel, only supports Eén, Canvas and Ketnet""" url = '%s/%s.jpg' % (self._VRTNU_SCREENSHOT_URL, channel) delete_cached_thumbnail(url) return url def list_channels(self, channels=None, live=True): """Construct a list of channel ListItems, either for Live TV or the TV Guide listing""" from tvguide import TVGuide _tvguide = TVGuide() channel_items = [] for channel in CHANNELS: if channels and channel.get('name') not in channels: continue context_menu = [] art_dict = {} # Try to use the white icons for thumbnails (used for icons as well) if has_addon('resource.images.studios.white'): art_dict[ 'thumb'] = 'resource://resource.images.studios.white/{studio}.png'.format( **channel) else: art_dict['thumb'] = 'DefaultTags.png' if not live: path = url_for('channels', channel=channel.get('name')) label = channel.get('label') plot = '[B]%s[/B]' % channel.get('label') is_playable = False info_dict = dict(title=label, plot=plot, studio=channel.get('studio'), mediatype='video') stream_dict = [] prop_dict = {} elif channel.get('live_stream') or channel.get('live_stream_id'): if channel.get('live_stream_id'): path = url_for('play_id', video_id=channel.get('live_stream_id')) elif channel.get('live_stream'): path = url_for('play_url', video_url=channel.get('live_stream')) label = localize(30141, **channel) # Channel live playing_now = _tvguide.playing_now(channel.get('name')) if playing_now: label += ' [COLOR=yellow]| %s[/COLOR]' % playing_now # A single Live channel means it is the entry for channel's TV Show listing, so make it stand out if channels and len(channels) == 1: label = '[B]%s[/B]' % label is_playable = True if channel.get('name') in ['een', 'canvas', 'ketnet']: if get_setting_bool('showfanart', default=True): art_dict['fanart'] = self.get_live_screenshot( channel.get('name', art_dict.get('fanart'))) plot = '%s\n\n%s' % (localize(30142, **channel), _tvguide.live_description( channel.get('name'))) else: plot = localize(30142, **channel) # Watch live # NOTE: Playcount and resumetime are required to not have live streams as "Watched" and resumed info_dict = dict(title=label, plot=plot, studio=channel.get('studio'), mediatype='video', playcount=0, duration=0) prop_dict = dict(resumetime=0) stream_dict = dict(duration=0) context_menu.append(( localize(30413), # Refresh menu 'RunPlugin(%s)' % url_for('delete_cache', cache_file='channel.{channel}.json'.format( channel=channel)), )) else: # Not a playable channel continue channel_items.append( TitleItem( label=label, path=path, art_dict=art_dict, info_dict=info_dict, prop_dict=prop_dict, stream_dict=stream_dict, context_menu=context_menu, is_playable=is_playable, )) return channel_items @staticmethod def list_youtube(channels=None): """Construct a list of youtube ListItems, either for Live TV or the TV Guide listing""" youtube_items = [] if not has_addon('plugin.video.youtube') or not get_setting_bool( 'showyoutube', default=True): return youtube_items for channel in CHANNELS: if channels and channel.get('name') not in channels: continue art_dict = {} # Try to use the white icons for thumbnails (used for icons as well) if has_addon('resource.images.studios.white'): art_dict[ 'thumb'] = 'resource://resource.images.studios.white/{studio}.png'.format( **channel) else: art_dict['thumb'] = 'DefaultTags.png' for youtube in channel.get('youtube', []): path = youtube_to_plugin_url(youtube['url']) label = localize(30143, **youtube) # Channel on YouTube # A single Live channel means it is the entry for channel's TV Show listing, so make it stand out if channels and len(channels) == 1: label = '[B]%s[/B]' % label plot = localize(30144, **youtube) # Watch on YouTube # NOTE: Playcount is required to not have live streams as "Watched" info_dict = dict(title=label, plot=plot, studio=channel.get('studio'), mediatype='video', playcount=0) context_menu = [( localize(30413), # Refresh menu 'RunPlugin(%s)' % url_for('delete_cache', cache_file='channel.{channel}.json'.format( channel=channel)), )] youtube_items.append( TitleItem( label=label, path=path, art_dict=art_dict, info_dict=info_dict, context_menu=context_menu, is_playable=False, )) return youtube_items def list_featured(self): """Construct a list of featured Listitems""" from data import FEATURED featured_items = [] for feature in self.localize_features(FEATURED): featured_name = feature.get('name') featured_items.append( TitleItem( label=featured_name, path=url_for('featured', feature=feature.get('id')), art_dict=dict(thumb='DefaultCountry.png'), info_dict=dict(plot='[B]%s[/B]' % feature.get('name'), studio='VRT'), )) return featured_items @staticmethod def localize_features(featured): """Return a localized and sorted listing""" from copy import deepcopy features = deepcopy(featured) for feature in features: for key, val in list(feature.items()): if key == 'name': feature[key] = localize_from_data(val, featured) return sorted(features, key=lambda x: x.get('name')) @staticmethod def valid_categories(categories): """Check if categories contain all necessary keys and values""" return bool(categories) and all( item.get('id') and item.get('name') for item in categories) @staticmethod def get_online_categories(): """Return a list of categories from the VRT NU website""" categories = [] categories_json = get_url_json( 'https://www.vrt.be/vrtnu/categorieen/jcr:content/par/categories.model.json' ) if categories_json is not None: categories = [] for category in categories_json.get('items'): categories.append( dict( id=category.get('name'), thumbnail=add_https_proto( category.get('image').get('src')), name=category.get('title'), )) return categories def get_categories(self): """Return a list of categories""" cache_file = 'categories.json' # Try the cache if it is fresh categories = get_cache(cache_file, ttl=7 * 24 * 60 * 60) if self.valid_categories(categories): return categories # Try online categories json categories = self.get_online_categories() if self.valid_categories(categories): from json import dumps update_cache(cache_file, dumps(categories)) return categories # Fall back to internal hard-coded categories from data import CATEGORIES log(2, 'Fall back to internal hard-coded categories') return CATEGORIES def list_categories(self): """Construct a list of category ListItems""" categories = self.get_categories() category_items = [] from data import CATEGORIES for category in self.localize_categories(categories, CATEGORIES): if get_setting_bool('showfanart', default=True): thumbnail = category.get('thumbnail', 'DefaultGenre.png') else: thumbnail = 'DefaultGenre.png' category_items.append( TitleItem( label=category.get('name'), path=url_for('categories', category=category.get('id')), art_dict=dict(thumb=thumbnail, icon='DefaultGenre.png'), info_dict=dict(plot='[B]%s[/B]' % category.get('name'), studio='VRT'), )) return category_items @staticmethod def localize_categories(categories, categories2): """Return a localized and sorted listing""" for category in categories: for key, val in list(category.items()): if key == 'name': category[key] = localize_from_data(val, categories2) return sorted(categories, key=lambda x: x.get('name'))