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 manage(self): """Allow the user to unselect favorites to be removed from the listing""" self.refresh(ttl=0) if not self._favorites: ok_dialog(heading=localize(30418), message=localize(30419)) # No favorites found return def by_title(tup): """Sort by title""" _, value = tup return value.get('title') items = [ dict(program_id=value.get('program_id'), program_name=key, title=unquote(value.get('title'))) for key, value in sorted(self._favorites.items(), key=by_title) ] titles = [item['title'] for item in items] preselect = list(range(0, len(items))) selected = multiselect( localize(30420), options=titles, preselect=preselect) # Please select/unselect to follow/unfollow if selected is not None: for idx in set(preselect).difference(set(selected)): self.unfollow(program_name=items[idx]['program_name'], title=items[idx]['title'], program_id=items[idx]['program_id']) for idx in set(selected).difference(set(preselect)): self.follow(program_name=items[idx]['program_name'], title=items[idx]['title'], program_id=items[idx]['program_id'])
def manage(self): """Allow the user to unselect favorites to be removed from the listing""" from utils import url_to_program self.refresh(ttl=0) if not self._data: ok_dialog(heading=localize(30418), message=localize(30419)) # No favorites found return def by_title(item): """Sort by title""" return item.get('value').get('title') items = [ dict(program=url_to_program(value.get('value').get('programUrl')), title=unquote(value.get('value').get('title')), enabled=value.get('value').get('isFavorite')) for value in list(sorted(list(self._data.values()), key=by_title)) ] titles = [item['title'] for item in items] preselect = [ idx for idx in range(0, len(items) - 1) if items[idx]['enabled'] ] selected = multiselect( localize(30420), options=titles, preselect=preselect) # Please select/unselect to follow/unfollow if selected is not None: for idx in set(preselect).difference(set(selected)): self.unfollow(program=items[idx]['program'], title=items[idx]['title']) for idx in set(selected).difference(set(preselect)): self.follow(program=items[idx]['program'], title=items[idx]['title'])
def show_favorites_menu(self): ''' The VRT NU addon 'My programs' menu ''' self._favorites.refresh(ttl=60 * 60) favorites_items = [ TitleItem( title=localize(30040), # My programs path=url_for('favorites_programs'), art_dict=dict(thumb='DefaultMovieTitle.png'), info_dict=dict(plot=localize(30041))), TitleItem( title=localize(30046), # My recent items path=url_for('favorites_recent'), art_dict=dict(thumb='DefaultRecentlyAddedEpisodes.png'), info_dict=dict(plot=localize(30047))), TitleItem( title=localize(30048), # My soon offline path=url_for('favorites_offline'), art_dict=dict(thumb='DefaultYear.png'), info_dict=dict(plot=localize(30049))), ] # Only add 'My watch later' and 'Continue watching' when it has been activated if self._resumepoints.is_activated(): favorites_items.append( TitleItem( title=localize(30050), # My watch later path=url_for('resumepoints_watchlater'), art_dict=dict(thumb='DefaultVideoPlaylists.png'), info_dict=dict(plot=localize(30051)), )) favorites_items.append( TitleItem( title=localize(30052), # Continue Watching path=url_for('resumepoints_continue'), art_dict=dict(thumb='DefaultInProgressShows.png'), info_dict=dict(plot=localize(30053)), )) if get_setting('addmymovies', 'true') == 'true': favorites_items.append( TitleItem( title=localize(30042), # My movies path=url_for('categories', category='films'), art_dict=dict(thumb='DefaultAddonVideo.png'), info_dict=dict(plot=localize(30043))), ) if get_setting('addmydocu', 'true') == 'true': favorites_items.append( TitleItem( title=localize(30044), # My documentaries path=url_for('favorites_docu'), art_dict=dict(thumb='DefaultMovies.png'), info_dict=dict(plot=localize(30045))), ) show_listing(favorites_items, category=30010, cache=False) # My favorites # Show dialog when no favorites were found if not self._favorites.titles(): ok_dialog(heading=localize(30415), message=localize(30416))
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 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 _select_hls_substreams(self, master_hls_url, protocol): """Select HLS substreams to speed up Kodi player start, workaround for slower Kodi selection""" hls_variant_url = None subtitle_url = None hls_audio_id = None hls_subtitle_id = None hls_base_url = master_hls_url.split('.m3u8')[0] try: response = open_url(master_hls_url, raise_errors=[415]) except HTTPError as exc: self._handle_bad_stream_error(protocol, exc.code, exc.reason) return None if response is None: return None hls_playlist = to_unicode(response.read()) max_bandwidth = get_max_bandwidth() stream_bandwidth = None # Get hls variant url based on max_bandwidth setting import re hls_variant_regex = re.compile(r'#EXT-X-STREAM-INF:[\w\-.,=\"]*?BANDWIDTH=(?P<BANDWIDTH>\d+),' r'[\w\-.,=\"]+\d,(?:AUDIO=\"(?P<AUDIO>[\w\-]+)\",)?(?:SUBTITLES=\"' r'(?P<SUBTITLES>\w+)\",)?[\w\-.,=\"]+?[\r\n](?P<URI>[\w:\/\-.=?&]+)') # reverse sort by bandwidth for match in sorted(re.finditer(hls_variant_regex, hls_playlist), key=lambda m: int(m.group('BANDWIDTH')), reverse=True): stream_bandwidth = int(match.group('BANDWIDTH')) // 1000 if max_bandwidth == 0 or stream_bandwidth < max_bandwidth: if match.group('URI').startswith('http'): hls_variant_url = match.group('URI') else: hls_variant_url = hls_base_url + match.group('URI') hls_audio_id = match.group('AUDIO') hls_subtitle_id = match.group('SUBTITLES') break if stream_bandwidth > max_bandwidth and not hls_variant_url: message = localize(30057, max=max_bandwidth, min=stream_bandwidth) ok_dialog(message=message) open_settings() # Get audio url if hls_audio_id: audio_regex = re.compile(r'#EXT-X-MEDIA:TYPE=AUDIO[\w\-=,\.\"\/]+?GROUP-ID=\"' + hls_audio_id + '' r'\"[\w\-=,\.\"\/]+?URI=\"(?P<AUDIO_URI>[\w\-=]+)\.m3u8\"') match_audio = re.search(audio_regex, hls_playlist) if match_audio: hls_variant_url = hls_base_url + match_audio.group('AUDIO_URI') + '-' + hls_variant_url.split('-')[-1] # Get subtitle url, works only for on demand streams if get_setting_bool('showsubtitles', default=True) and '/live/' not in master_hls_url and hls_subtitle_id: subtitle_regex = re.compile(r'#EXT-X-MEDIA:TYPE=SUBTITLES[\w\-=,\.\"\/]+?GROUP-ID=\"' + hls_subtitle_id + '' r'\"[\w\-=,\.\"\/]+URI=\"(?P<SUBTITLE_URI>[\w\-=]+)\.m3u8\"') match_subtitle = re.search(subtitle_regex, hls_playlist) if match_subtitle: subtitle_url = hls_base_url + match_subtitle.group('SUBTITLE_URI') + '.webvtt' return StreamURLS(hls_variant_url, subtitle_url)
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 update_repos(): """Force an update of the repositories""" if get_global_setting( 'general.addonupdates') == 0: # Automatic updates is enabled execute_builtin('UpdateAddonRepos') ok_dialog(heading=localize(30450), message=localize(30451)) # Repositories are being updated else: ok_dialog(heading=localize(30452), message=localize(30453)) # Automatic updates is disabled show_settings_addons()
def _get_fresh_token(self, refresh_token, name): """Refresh an expired X-VRT-Token, vrtlogin-at or vrtlogin-rt token""" refresh_url = self._TOKEN_GATEWAY_URL + '/refreshtoken?legacy=true' cookie_value = 'vrtlogin-rt=' + refresh_token headers = {'Cookie': cookie_value} cookiejar = cookielib.CookieJar() try: open_url(refresh_url, headers=headers, cookiejar=cookiejar, raise_errors=[401]) except HTTPError: ok_dialog(heading=localize(30970), message=localize(30971)) return TokenResolver._create_token_dictionary(cookiejar, name)
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 _version_check(self): first_run, settings_version, addon_version = self._first_run() if first_run: # 2.0.0 version: changed plugin:// url interface: show warning that Kodi favourites and what-was-watched will break if settings_version == '' and has_credentials(): ok_dialog(localize(30978), localize(30979)) if addon_version == '2.2.1': # 2.2.1 version: changed artwork: delete old cached artwork delete_cached_thumbnail( get_addon_info('fanart').replace('.png', '.jpg')) delete_cached_thumbnail(get_addon_info('icon')) # 2.2.1 version: moved tokens: delete old tokens from tokenresolver import TokenResolver TokenResolver().delete_tokens()
def _get_usertoken(self, name=None, login_json=None, roaming=False): """Get a user X-VRT-Token, vrtlogin-at, vrtlogin-expiry, vrtlogin-rt, SESSION, OIDCXSRF or state token""" if not login_json: login_json = self._get_login_json() cookiejar = cookielib.CookieJar() open_url(self._USER_TOKEN_GATEWAY_URL, cookiejar=cookiejar) xsrf = next( (cookie for cookie in cookiejar if cookie.name == 'OIDCXSRF'), None) if xsrf is None: return None payload = dict(UID=login_json.get('UID'), UIDSignature=login_json.get('UIDSignature'), signatureTimestamp=login_json.get('signatureTimestamp'), client_id='vrtnu-site', _csrf=xsrf.value) data = urlencode(payload).encode() response = open_url(self._VRT_LOGIN_URL, data=data, cookiejar=cookiejar) if response is None: return None destination = response.geturl() usertoken = TokenResolver._create_token_dictionary(cookiejar, name) if not usertoken and not destination.startswith( 'https://www.vrt.be/vrtnu'): if roaming is False: ok_dialog(heading=localize(30970), message=localize(30972)) return None # Cache additional tokens for later use refreshtoken = TokenResolver._create_token_dictionary( cookiejar, cookie_name='vrtlogin-rt') accesstoken = TokenResolver._create_token_dictionary( cookiejar, cookie_name='vrtlogin-at') if refreshtoken is not None: from json import dumps cache_file = self._get_token_filename('vrtlogin-rt') update_cache(cache_file, dumps(refreshtoken), self._TOKEN_CACHE_DIR) if accesstoken is not None: from json import dumps cache_file = self._get_token_filename('vrtlogin-at') update_cache(cache_file, dumps(accesstoken), self._TOKEN_CACHE_DIR) return usertoken
def _version_check(self): first_run, settings_version, addon_version = self._first_run() if first_run: # 2.0.0 version: changed plugin:// url interface: show warning that Kodi favourites and what-was-watched will break if settings_version == '' and has_credentials(): ok_dialog(localize(30978), localize(30979)) if addon_version == '2.2.1': # 2.2.1 version: changed artwork: delete old cached artwork delete_cached_thumbnail(get_addon_info('fanart').replace('.png', '.jpg')) delete_cached_thumbnail(get_addon_info('icon')) # 2.2.1 version: moved tokens: delete old tokens from tokenresolver import TokenResolver TokenResolver().delete_tokens() # Make user aware that timeshift functionality will not work without ISA when user starts up the first time if settings_version == '' and kodi_version_major() > 17 and not has_inputstream_adaptive(): ok_dialog(message=localize(30988))
def search(self, keywords=None, page=None): ''' The VRT NU add-on Search functionality and results ''' if keywords is None: keywords = get_search_string() if not keywords: end_of_directory() return from statichelper import realpage page = realpage(page) self.add(keywords) from apihelper import ApiHelper search_items, sort, ascending, content = ApiHelper( self._favorites, self._resumepoints).list_search(keywords, page=page) if not search_items: ok_dialog(heading=localize(30135), message=localize(30136, keywords=keywords)) end_of_directory() return # Add 'More...' entry at the end from helperobjects import TitleItem if len(search_items) == 50: search_items.append( TitleItem( title=localize(30300), path=url_for('search_query', keywords=keywords, page=page + 1), art_dict=dict(thumb='DefaultAddonSearch.png'), info_dict=dict(), )) self._favorites.refresh(ttl=60 * 60) show_listing(search_items, category=30032, sort=sort, ascending=ascending, content=content, cache=False)
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 build_menu(items_url): from xbmcplugin import addDirectoryItem, endOfDirectory, setContent setContent(plugin.handle, 'videos') list_items = [] try: content = redbull.get_content(items_url) except IOError: ok_dialog(localize(30220), localize(30221)) # Error getting data from Redbull server return if content.get('links'): for link in content.get('links'): list_items.append(generate_list_item(link, PRODUCT)) if content.get('collections'): collections = content.get('collections') if collections[0].get( 'collection_type') == 'top_results': # Handle search results content['items'] = collections[0].get('items') else: for collection in collections: list_items.append(generate_list_item(collection, COLLECTION)) if content.get('items'): for item in content.get('items'): list_items.append(generate_list_item(item, PRODUCT)) if not list_items: ok_dialog(localize(30222), localize(30223)) # No results found return for list_item in list_items: addDirectoryItem(handle=plugin.handle, url=list_item.getPath(), listitem=list_item, isFolder=('/play/' not in list_item.getPath())) endOfDirectory(plugin.handle)
def search(self, keywords=None, page=0, edit=False): """The VRT NU add-on Search functionality and results""" if keywords is None or edit is True: keywords = get_search_string(keywords) if not keywords: end_of_directory() return if edit is True: container_update(url_for('search_query', keywords=keywords)) return from apihelper import ApiHelper from utils import realpage page = realpage(page) self.add(keywords) search_items, sort, ascending, content = ApiHelper(self._favorites, self._resumepoints).list_search(keywords, page=page) if not search_items: ok_dialog(heading=localize(30135), message=localize(30136, keywords=keywords)) end_of_directory() return # Add 'More…' entry at the end from helperobjects import TitleItem if len(search_items) == get_setting_int('itemsperpage', default=50): search_items.append(TitleItem( label=colour(localize(30300)), # More… path=url_for('search_query', keywords=keywords, page=page + 1), art_dict=dict(thumb='DefaultAddonSearch.png'), info_dict={}, )) self._favorites.refresh(ttl=ttl('indirect')) show_listing(search_items, category=30032, sort=sort, ascending=ascending, content=content, cache=False)
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 _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 notify(msg): ok_dialog(msg)