def login(self, refresh=False, token_variant=None): """Kodi GUI login flow""" # If no credentials, ask user for credentials if not has_credentials(): if refresh: return open_settings() open_settings() if not self._credentials_changed(): return None # Check credentials login_json = self._get_login_json() # Bad credentials while login_json.get('errorCode') != 0: # Show localized login error messages in Kodi GUI message = login_json.get('errorDetails') log_error('Login failed: {msg}', msg=message) if message == 'invalid loginID or password': message = localize(30953) # Invalid login! elif message == 'loginID must be provided': message = localize(30955) # Please fill in username elif message == 'Missing required parameter: password': message = localize(30956) # Please fill in password ok_dialog(heading=localize(30951), message=message) # Login failed! if refresh: return open_settings() open_settings() if not self._credentials_changed(): return None login_json = self._get_login_json() # Get token return self._get_new_xvrttoken(login_json, token_variant)
def _webscrape_api_data(self, video_url): """Scrape api data from VRT NU html page""" from webscraper import get_video_attributes video_data = get_video_attributes(video_url) # Web scraping failed, log error if not video_data: log_error('Web scraping api data failed, empty video_data') return None # Store required html data attributes client = video_data.get('client') or self._CLIENT media_api_url = video_data.get('mediaapiurl') video_id = video_data.get('videoid') publication_id = video_data.get('publicationid', '') # Live stream or on demand if video_id is None: is_live_stream = True video_id = video_data.get('livestream') else: is_live_stream = False publication_id += quote('$') if client is None or media_api_url is None or (video_id is None and publication_id is None): log_error( 'Web scraping api data failed, required attributes missing') return None return ApiData(client, media_api_url, video_id, publication_id, is_live_stream)
def test_debug_logging(): xbmc.settings['debug.showloginfo'] = True addon.settings['max_log_level'] = '3' log(0, 'Logging as quiet') log(1, 'Logging as info') log(2, 'Logging as verbose') log(3, 'Logging as debug') log_error('Logging as error')
def play_latest_episode(self, program): """A hidden feature in the VRT NU add-on to play the latest episode of a program""" video = self._apihelper.get_latest_episode(program) if not video: log_error('Play latest episode failed, program {program}', program=program) ok_dialog(message=localize(30954)) end_of_directory() return self.play(video)
def play_episode_by_whatson_id(self, whatson_id): """Play an episode of a program given the whatson_id""" video = self._apihelper.get_single_episode(whatson_id=whatson_id) if not video: log_error('Play episode by whatson_id failed, whatson_id {whatson_id}', whatson_id=whatson_id) ok_dialog(message=localize(30954)) end_of_directory() return self.play(video)
def play_upnext(self, video_id): """Play the next episode of a program by video_id""" video = self._apihelper.get_single_episode(video_id=video_id) if not video: log_error('Play Up Next with video_id {video_id} failed', video_id=video_id) ok_dialog(message=localize(30954)) end_of_directory() return self.play(video)
def play_whatson(self, whatson_id): ''' Play a video by whatson_id ''' video = self._apihelper.get_single_episode(whatson_id) if not video: log_error('Play by whatson_id {id} failed', id=whatson_id) ok_dialog(message=localize(30954)) end_of_directory() return self.play(video)
def delete_online(self, asset_id): """Delete resumepoint online""" try: result = open_url('https://video-user-data.vrt.be/resume_points/{asset_id}'.format(asset_id=asset_id), headers=self.resumepoint_headers(), method='DELETE', raise_errors='all') log(3, "[Resumepoints] '{asset_id}' online deleted: {code}", asset_id=asset_id, code=result.getcode()) except HTTPError as exc: log_error("Failed to remove '{asset_id}' from resumepoints: {error}", asset_id=asset_id, error=exc) return False return True
def update_online(self, asset_id, title, url, payload): """Update resumepoint online""" from json import dumps try: get_url_json('https://video-user-data.vrt.be/resume_points/{asset_id}'.format(asset_id=asset_id), headers=self.resumepoint_headers(url), data=dumps(payload).encode()) except HTTPError as exc: log_error('Failed to (un)watch episode {title} at VRT NU ({error})', title=title, error=exc) notification(message=localize(30977, title=title)) return False return True
def play_episode_by_air_date(self, channel, start_date, end_date): """Play an episode of a program given the channel and the air date in iso format (2019-07-06T19:35:00)""" video = self._apihelper.get_episode_by_air_date(channel, start_date, end_date) if video and video.get('errorlabel'): ok_dialog(message=localize(30986, title=video.get('errorlabel'))) end_of_directory() return if not video: log_error('Play episode by air date failed, channel {channel}, start_date {start}', channel=channel, start=start_date) ok_dialog(message=localize(30954)) end_of_directory() return self.play(video)
def get_video_attributes(vrtnu_url): """Return a dictionary with video attributes by scraping the VRT NU website""" # Get cache cache_file = 'web_video_attrs_multi.json' video_attrs_multi = get_cache(cache_file, ttl=ttl('indirect')) if not video_attrs_multi: video_attrs_multi = {} if vrtnu_url in video_attrs_multi: return video_attrs_multi[vrtnu_url] # Scrape video attributes from bs4 import BeautifulSoup, SoupStrainer try: response = open_url(vrtnu_url, raise_errors='all') except HTTPError as exc: log_error('Web scraping video attributes failed: {error}', error=exc) return None if response is None: return None html_page = response.read() strainer = SoupStrainer( ['section', 'div'], {'class': ['video-detail__player', 'livestream__inner']}) soup = BeautifulSoup(html_page, 'html.parser', parse_only=strainer) item = None epg_channel = None if '#epgchannel=' in vrtnu_url: epg_channel = vrtnu_url.split('#epgchannel=')[1] for item in soup: if epg_channel and epg_channel == item.get('data-epgchannel'): break if not epg_channel and len(soup) > 1: return None try: video_attrs = item.find(name='nui-media').attrs except AttributeError as exc: log_error('Web scraping video attributes failed: {error}', error=exc) return None # Update cache if vrtnu_url in video_attrs_multi: # Update existing video_attrs_multi[vrtnu_url] = video_attrs else: # Create new video_attrs_multi.update({vrtnu_url: video_attrs}) from json import dumps update_cache(cache_file, dumps(video_attrs_multi)) return video_attrs
def resumepoints_headers(): """Generate http headers for VRT NU Resumepoint API""" from tokenresolver import TokenResolver vrtlogin_at = TokenResolver().get_token('vrtlogin-at') headers = {} if vrtlogin_at: headers = { 'Authorization': 'Bearer ' + vrtlogin_at, 'Content-Type': 'application/json', } else: log_error('Failed to get access token from VRT NU') notification(message=localize(30975)) return headers
def update(self, program, title, value=True): """Set a program as favorite, and update local copy""" # Survive any recent updates self.refresh(ttl=5) if value is self.is_favorite(program): # Already followed/unfollowed, nothing to do return True from tokenresolver import TokenResolver xvrttoken = TokenResolver().get_xvrttoken(token_variant='user') if xvrttoken is None: log_error('Failed to get favorites token from VRT NU') notification(message=localize(30975)) return False headers = { 'authorization': 'Bearer ' + xvrttoken, 'content-type': 'application/json', 'Referer': 'https://www.vrt.be/vrtnu', } from json import dumps from utils import program_to_url payload = dict(isFavorite=value, programUrl=program_to_url(program, 'short'), title=title) data = dumps(payload).encode('utf-8') program_id = program_to_id(program) try: get_url_json( 'https://video-user-data.vrt.be/favorites/{program_id}'.format( program_id=program_id), headers=headers, data=data) except HTTPError as exc: log_error( "Failed to (un)follow program '{program}' at VRT NU ({error})", program=program, error=exc) notification(message=localize(30976, program=program)) return False # NOTE: Updates to favorites take a longer time to take effect, so we keep our own cache and use it self._data[program_id] = dict(value=payload) update_cache('favorites.json', dumps(self._data)) invalidate_caches('my-offline-*.json', 'my-recent-*.json') return True
def resumepoint_headers(url=None): """Generate http headers for VRT NU Resumepoints API""" from tokenresolver import TokenResolver xvrttoken = TokenResolver().get_xvrttoken(token_variant='user') headers = {} if xvrttoken: url = 'https://www.vrt.be' + url if url else 'https://www.vrt.be/vrtnu' headers = { 'authorization': 'Bearer ' + xvrttoken, 'content-type': 'application/json', 'Referer': url, } else: log_error('Failed to get usertoken from VRT NU') notification(message=localize(30975)) return headers
def watchlater_headers(url=None): """Generate http headers for VRT NU watchLater API""" from tokenresolver import TokenResolver xvrttoken = TokenResolver().get_token('X-VRT-Token', variant='user') headers = {} if xvrttoken: url = 'https://www.vrt.be' + url if url else 'https://www.vrt.be/vrtnu' headers = { 'Authorization': 'Bearer ' + xvrttoken, 'Content-Type': 'application/json', 'Referer': url, } else: log_error('Failed to get usertoken from VRT NU') notification(message=localize(30975)) return headers
def virtualsubclip_seektozero(self): """VRT NU already offers some programs (mostly current affairs programs) as video on demand while the program is still being broadcasted live. To do so, a start timestamp is added to the livestream url so the Unified Origin streaming platform knows it should return a time bounded manifest file that indicates the beginning of the program. This is called a Live-to-VOD stream or virtual subclip: https://docs.unified-streaming.com/documentation/vod/player-urls.html#virtual-subclips e.g. https://live-cf-vrt.akamaized.net/groupc/live/8edf3bdf-7db3-41c3-a318-72cb7f82de66/live.isml/.mpd?t=2020-07-20T11:07:00 For some unclear reason the virtual subclip defined by a single start timestamp still behaves as a ordinary livestream and starts at the live edge of the stream. It seems this is not a Kodi or Inputstream Adaptive bug, because other players like THEOplayer or DASH-IF's reference player treat this kind of manifest files the same way. The only difference is that seeking to the beginning of the program is possible. So if the url contains a single start timestamp, we can work around this problem by automatically seeking to the beginning of the program. """ playing_file = self.getPlayingFile() if any(param in playing_file for param in ('?t=', '&t=')): try: # Python 3 from urllib.parse import parse_qs, urlsplit except ImportError: # Python 2 from urlparse import parse_qs, urlsplit import re # Detect single start timestamp timestamp = parse_qs(urlsplit(playing_file).query).get('t')[0] rgx = re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$') is_single_start_timestamp = bool(re.match(rgx, timestamp)) if is_single_start_timestamp: # Check resume status resume_info = jsonrpc(method='Player.GetItem', params=dict(playerid=1, properties=['resume' ])).get('result') if resume_info: resume_position = resume_info.get('item').get( 'resume').get('position') is_resumed = abs(resume_position - self.getTime()) < 1 # Seek to zero if the user didn't resume the program if not is_resumed: log(3, '[PlayerInfo {id}] Virtual subclip: seeking to the beginning of the program', id=self.thread_id) self.seekTime(0) else: log_error( 'Failed to start virtual subclip {playing_file} at start timestamp', playing_file=playing_file)
def _handle_bad_stream_error(protocol, code=None, reason=None): """Show a localized error message in Kodi GUI for a failing VRT NU stream based on protocol: hls, hls_aes, mpeg_dash) message: VRT NU stream <stream_type> problem, try again with (InputStream Adaptive) (and) (DRM) enabled/disabled: 30959=and DRM, 30960=disabled, 30961=enabled """ # HLS AES DRM failed if protocol == 'hls_aes' and not supports_drm(): message = localize(30962, protocol=protocol.upper(), version=kodi_version_major()) elif protocol == 'hls_aes' and not has_inputstream_adaptive() and not get_setting_bool('usedrm', default=True): message = localize(30958, protocol=protocol.upper(), component=localize(30959), state=localize(30961)) elif protocol == 'hls_aes' and has_inputstream_adaptive(): message = localize(30958, protocol=protocol.upper(), component='Widevine DRM', state=localize(30961)) elif protocol == 'hls_aes' and get_setting_bool('usedrm', default=True): message = localize(30958, protocol=protocol.upper(), component='InputStream Adaptive', state=localize(30961)) else: message = localize(30958, protocol=protocol.upper(), component='InputStream Adaptive', state=localize(30960)) heading = 'HTTP Error {code}: {reason}'.format(code=code, reason=reason) if code and reason else None log_error('Unable to play stream. {error}', error=heading) ok_dialog(heading=heading, message=message) end_of_directory()
def delete_online(self, asset_id): """Delete resumepoint online""" req = Request( 'https://video-user-data.vrt.be/resume_points/{asset_id}'.format( asset_id=asset_id), headers=self.resumepoint_headers()) req.get_method = lambda: 'DELETE' try: result = urlopen(req) log(3, "[Resumepoints] '{asset_id}' online deleted: {code}", asset_id=asset_id, code=result.getcode()) except HTTPError as exc: log_error( "Failed to remove '{asset_id}' from resumepoints: {error}", asset_id=asset_id, error=exc) return False return True
def update(self, program, title, value=True): ''' Set a program as favorite, and update local copy ''' self.refresh(ttl=60) if value is self.is_favorite(program): # Already followed/unfollowed, nothing to do return True from tokenresolver import TokenResolver xvrttoken = TokenResolver().get_xvrttoken(token_variant='user') if xvrttoken is None: log_error('Failed to get favorites token from VRT NU') notification(message=localize(30975)) return False headers = { 'authorization': 'Bearer ' + xvrttoken, 'content-type': 'application/json', 'Referer': 'https://www.vrt.be/vrtnu', } from statichelper import program_to_url payload = dict(isFavorite=value, programUrl=program_to_url(program, 'short'), title=title) from json import dumps data = dumps(payload).encode('utf-8') program_uuid = self.program_to_uuid(program) log(2, 'URL post: https://video-user-data.vrt.be/favorites/{uuid}', uuid=program_uuid) req = Request('https://video-user-data.vrt.be/favorites/%s' % program_uuid, data=data, headers=headers) result = urlopen(req) if result.getcode() != 200: log_error("Failed to (un)follow program '{program}' at VRT NU", program=program) notification(message=localize(30976, program=program)) return False # NOTE: Updates to favorites take a longer time to take effect, so we keep our own cache and use it self._favorites[program_uuid] = dict(value=payload) update_cache('favorites.json', self._favorites) invalidate_caches('my-offline-*.json', 'my-recent-*.json') return True
def _webscrape_api_data(self, video_url): ''' Scrape api data from VRT NU html page ''' from bs4 import BeautifulSoup, SoupStrainer log(2, 'URL get: {url}', url=unquote(video_url)) html_page = urlopen(video_url).read() strainer = SoupStrainer( ['section', 'div'], {'class': ['video-player', 'livestream__player']}) soup = BeautifulSoup(html_page, 'html.parser', parse_only=strainer) try: video_data = soup.find(lambda tag: tag.name == 'nui-media').attrs except Exception as exc: # pylint: disable=broad-except # Web scraping failed, log error log_error('Web scraping api data failed: {error}', error=exc) return None # Web scraping failed, log error if not video_data: log_error('Web scraping api data failed, empty video_data') return None # Store required html data attributes client = video_data.get('client') or self._CLIENT media_api_url = video_data.get('mediaapiurl') video_id = video_data.get('videoid') publication_id = video_data.get('publicationid', '') # Live stream or on demand if video_id is None: is_live_stream = True video_id = video_data.get('livestream') else: is_live_stream = False publication_id += quote('$') if client is None or media_api_url is None or (video_id is None and publication_id is None): log_error( 'Web scraping api data failed, required attributes missing') return None return ApiData(client, media_api_url, video_id, publication_id, is_live_stream)
def update(self, uuid, title, url, watch_later=None, position=None, total=None): ''' Set program resumepoint or watchLater status and update local copy ''' # The video has no assetPath, so we cannot update resumepoints if uuid is None: return True if position is not None and position >= total - 30: watch_later = False self.refresh(ttl=0) if watch_later is not None and position is None and total is None and watch_later is self.is_watchlater( uuid): # watchLater status is not changed, nothing to do return True if watch_later is None and position == self.get_position( uuid) and total == self.get_total(uuid): # resumepoint is not changed, nothing to do return True # Collect header info for POST Request from tokenresolver import TokenResolver xvrttoken = TokenResolver().get_xvrttoken(token_variant='user') if xvrttoken is None: log_error('Failed to get usertoken from VRT NU') notification(message=localize(30975) + title) return False headers = { 'authorization': 'Bearer ' + xvrttoken, 'content-type': 'application/json', 'Referer': 'https://www.vrt.be' + url, } if uuid in self._resumepoints: # Update existing resumepoint values payload = self._resumepoints[uuid]['value'] payload['url'] = url else: # Create new resumepoint values payload = dict(position=0, total=100, url=url, watchLater=False) if position is not None: payload['position'] = position if total is not None: payload['total'] = total removes = [] if position is not None or total is not None: removes.append('continue-*.json') if watch_later is not None: # Add watchLater status to payload payload['watchLater'] = watch_later removes.append('watchlater-*.json') from json import dumps data = dumps(payload).encode() log(2, 'URL post: https://video-user-data.vrt.be/resume_points/{uuid}', uuid=uuid) log(2, 'URL post data:: {data}', data=data) try: req = Request('https://video-user-data.vrt.be/resume_points/%s' % uuid, data=data, headers=headers) urlopen(req) except HTTPError as exc: log_error('Failed to (un)watch episode at VRT NU ({error})', error=exc) notification(message=localize(30977)) return False # NOTE: Updates to resumepoints take a longer time to take effect, so we keep our own cache and use it self._resumepoints[uuid] = dict(value=payload) update_cache('resume_points.json', self._resumepoints) invalidate_caches(*removes) return True
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_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 _handle_stream_api_error(message, video_json=None): ''' Show localized stream api error messages in Kodi GUI ''' if video_json: log_error(video_json.get('message')) ok_dialog(message=message) end_of_directory()
def update_resumepoint(self, video_id, asset_str, title, position=None, total=None, path=None, episode_id=None, episode_title=None): """Set episode resumepoint and update local copy""" if video_id is None: return True menu_caches = [] self.refresh_resumepoints(ttl=5) # Add existing position and total if None if video_id in self._resumepoints and position is None and total is None: position = self.get_position(video_id) total = self.get_total(video_id) # Update if (self.still_watching(position, total) or (path and path.startswith('plugin://plugin.video.vrt.nu/play/upnext'))): # Normally, VRT NU resumepoints are deleted when an episode is (un)watched and Kodi GUI automatically sets # the (un)watched status when Kodi Player exits. This mechanism doesn't work with "Up Next" episodes because # these episodes are not initiated from a ListItem in Kodi GUI. # For "Up Next" episodes, we should never delete the VRT NU resumepoints to make sure the watched status # can be forced in Kodi GUI using the playcount infolabel. log(3, "[Resumepoints] Update resumepoint '{video_id}' {position}/{total}", video_id=video_id, position=position, total=total) if position == self.get_position( video_id) and total == self.get_total(video_id): # Resumepoint is not changed, nothing to do return True menu_caches.append('continue-*.json') # Update online gdpr = '{asset_str} gekeken tot {at} seconden.'.format( asset_str=asset_str, at=position) payload = dict( at=position, total=total, gdpr=gdpr, ) from json import dumps try: resumepoint_json = get_url_json( '{api}/{video_id}'.format(api=self.RESUMEPOINTS_URL, video_id=video_id), headers=self.resumepoints_headers(), data=dumps(payload).encode()) except HTTPError as exc: log_error( 'Failed to update resumepoint of {title} at VRT NU ({error})', title=title, error=exc) notification(message=localize(30977, title=title)) return False # Update local for idx, item in enumerate(self._resumepoints.get('items')): if item.get('mediaId') == video_id: self._resumepoints.get('items')[idx] = resumepoint_json break update_cache(self.RESUMEPOINTS_CACHE_FILE, dumps(self._resumepoints)) if menu_caches: invalidate_caches(*menu_caches) else: # Delete log(3, "[Resumepoints] Delete resumepoint '{asset_str}' {position}/{total}", asset_str=asset_str, position=position, total=total) # Delete watchlater self.update_watchlater(episode_id, episode_title, watch_later=False) # Do nothing if there is no resumepoint for this video_id from json import dumps if video_id not in dumps(self._resumepoints): log(3, "[Resumepoints] '{video_id}' not present, nothing to delete", video_id=video_id) return True # Add menu caches menu_caches.append('continue-*.json') # Delete online try: result = open_url('{api}/{video_id}'.format( api=self.RESUMEPOINTS_URL, video_id=video_id), headers=self.resumepoints_headers(), method='DELETE', raise_errors='all') log(3, "[Resumepoints] '{video_id}' online deleted: {code}", video_id=video_id, code=result.getcode()) except HTTPError as exc: log_error( "Failed to remove resumepoint of '{video_id}': {error}", video_id=video_id, error=exc) return False # Delete local representation and cache for item in self._resumepoints.get('items'): if item.get('mediaId') == video_id: self._resumepoints.get('items').remove(item) break update_cache(self.RESUMEPOINTS_CACHE_FILE, dumps(self._resumepoints)) if menu_caches: invalidate_caches(*menu_caches) return True
def get_stream(self, video, roaming=False, api_data=None): """Main streamservice function""" if not api_data: api_data = self._get_api_data(video) stream_json = self._get_stream_json(api_data, roaming) if not stream_json: # Roaming token failed if roaming: message = localize( 30964 ) # Geoblock error: Cannot be played, need Belgian phone number validation return self._handle_stream_api_error(message) # X-VRT-Token failed message = localize( 30963) # You need a VRT NU account to play this stream. return self._handle_stream_api_error(message) if 'targetUrls' in stream_json: # DRM support for ketnet junior/uplynk streaming service uplynk = 'uplynk.com' in stream_json.get('targetUrls')[0].get( 'url') vudrm_token = stream_json.get('drm') vualto_license_url = stream_json.get( 'vualto_license_url') or self._get_vualto_license_url() drm_stream = (vudrm_token or uplynk) # Select streaming protocol if not drm_stream and has_inputstream_adaptive(): protocol = 'mpeg_dash' elif drm_stream and self._can_play_drm: protocol = 'mpeg_dash' elif vudrm_token: protocol = 'hls_aes' else: protocol = 'hls' # Get stream manifest url manifest_url = next((stream.get('url') for stream in stream_json.get('targetUrls') if stream.get('type') == protocol), None) # Failed to get compatible manifest url, ask user to toggle "Use Widevine DRM" if manifest_url is None: available_protocols = [ stream.get('type') for stream in stream_json.get('targetUrls') ] if protocol not in available_protocols: error_json = { 'message': '%s is not available for this stream, please try toggling the "Use Widevine DRM" setting' % protocol } message = localize( 30989) # Failed to load a compatible stream return self._handle_stream_api_error(message, error_json) else: # External virtual subclip, live-to-VOD from past 24 hours archived livestream (airdate feature) if video.get('start_date') and video.get('end_date'): manifest_url += '?t=' + video.get( 'start_date') + '-' + video.get('end_date') # Fix virtual subclip from datetime import timedelta duration = timedelta( milliseconds=stream_json.get('duration', 0)) manifest_url = self._fix_virtualsubclip(manifest_url, duration) # Prepare stream for Kodi player if protocol == 'mpeg_dash' and drm_stream: log(2, 'Protocol: mpeg_dash drm') if vudrm_token: encryption_json = '{{"token":"{0}","drm_info":[D{{SSM}}],"kid":"{{KID}}"}}'.format( vudrm_token) license_key = self._get_license_key( key_url=vualto_license_url, key_type='D', key_value=encryption_json, key_headers={ 'Content-Type': 'text/plain;charset=UTF-8' }) else: license_key = self._get_license_key( key_url=self._UPLYNK_LICENSE_URL, key_type='R') stream = StreamURLS(manifest_url, license_key=license_key, use_inputstream_adaptive=True) elif protocol == 'mpeg_dash': log(2, 'Protocol: mpeg_dash') stream = StreamURLS(manifest_url, use_inputstream_adaptive=True) else: log(2, 'Protocol: {protocol}', protocol=protocol) # Fix 720p quality for HLS livestreams manifest_url = manifest_url.replace( '.m3u8?', '.m3u8?hd&' ) if '.m3u8?' in manifest_url else manifest_url + '?hd' # Play HLS directly in Kodi Player on Kodi 17 if kodi_version_major( ) < 18 or not has_inputstream_adaptive(): stream = self._select_hls_substreams( manifest_url, protocol) else: stream = StreamURLS(manifest_url, use_inputstream_adaptive=True) return stream # VRT Geoblock: failed to get stream, now try again with roaming enabled if stream_json.get('code') in self._GEOBLOCK_ERROR_CODES: log_error('VRT Geoblock: {msg}', msg=stream_json.get('message')) if not roaming: return self.get_stream(video, roaming=True, api_data=api_data) if stream_json.get('code') == self._INVALID_LOCATION: message = localize( 30965 ) # Geoblock error: Blocked on your geographical location based on your IP address return self._handle_stream_api_error(message, stream_json) if stream_json.get('code') == self._BELGIUM_ONLY: message = localize( 30973 ) # Geoblock error: This program can only be played from EU return self._handle_stream_api_error(message, stream_json) message = localize( 30964 ) # Geoblock error: Cannot be played, need Belgian phone number validation return self._handle_stream_api_error(message, stream_json) if stream_json.get('code') == 'VIDEO_NOT_FOUND': # Refresh listing invalidate_caches('*.json') container_reload() message = localize(30987) # No stream found return self._handle_stream_api_error(message, stream_json) if stream_json.get('code') == 'ERROR_AGE_RESTRICTED': message = localize( 30990 ) # Cannot be played, VRT NU account not allowed to access 12+ content return self._handle_stream_api_error(message, stream_json) # Failed to get stream, handle error message = localize(30954) # Whoops something went wrong return self._handle_stream_api_error(message, stream_json)