def _get_login_json(self): """Get login json""" payload = dict( loginID=from_unicode(get_setting('username')), password=from_unicode(get_setting('password')), sessionExpiration='-2', APIKey=self._API_KEY, targetEnv='jssdk', ) data = urlencode(payload).encode() return get_url_json(self._LOGIN_URL, data=data, fail={})
def log_error(message, **kwargs): """Log error messages to Kodi""" if kwargs: from string import Formatter message = Formatter().vformat(message, (), SafeDict(**kwargs)) message = '[{addon}] {message}'.format(addon=addon_id(), message=message) xbmc.log(from_unicode(message), 4)
def unwatchlater(asset_id, title, url): """The API interface to unwatch an episode used by the context menu""" from resumepoints import ResumePoints ResumePoints().unwatchlater(asset_id=asset_id, title=to_unicode( unquote_plus(from_unicode(title))), url=url)
def unfollow(program, title): """The API interface to unfollow a program used by the context menu""" move_down = bool(plugin.args.get('move_down')) from favorites import Favorites Favorites().unfollow(program=program, title=to_unicode(unquote_plus(from_unicode(title))), move_down=move_down)
def _get_xvrttoken(self, login_json=None): """Get a one year valid X-VRT-Token""" from json import dumps if not login_json: login_json = self._get_login_json() login_token = login_json.get('sessionInfo', {}).get('login_token') if not login_token: return None login_cookie = 'glt_{api_key}={token}'.format(api_key=self._API_KEY, token=login_token) payload = dict(uid=login_json.get('UID'), uidsig=login_json.get('UIDSignature'), ts=login_json.get('signatureTimestamp'), email=from_unicode(get_setting('username'))) data = dumps(payload).encode() headers = {'Content-Type': 'application/json', 'Cookie': login_cookie} response = open_url(self._TOKEN_GATEWAY_URL, data=data, headers=headers) if response is None: return None setcookie_header = response.info().get('Set-Cookie') xvrttoken = TokenResolver._create_token_dictionary(setcookie_header) if xvrttoken is None: return None notification(message=localize(30952)) # Login succeeded. return xvrttoken
def _get_new_xvrttoken(self, login_json, token_variant=None): """Get new X-VRT-Token from VRT NU website""" if token_variant == 'roaming': xvrttoken = self._get_roaming_xvrttoken() else: login_token = login_json.get('sessionInfo', {}).get('login_token') if not login_token: return None from json import dumps login_cookie = 'glt_{api_key}={token}'.format(api_key=self._API_KEY, token=login_token) payload = dict( uid=login_json.get('UID'), uidsig=login_json.get('UIDSignature'), ts=login_json.get('signatureTimestamp'), email=from_unicode(get_setting('username')), ) data = dumps(payload).encode() headers = {'Content-Type': 'application/json', 'Cookie': login_cookie} log(2, 'URL post: {url}', url=unquote(self._TOKEN_GATEWAY_URL)) req = Request(self._TOKEN_GATEWAY_URL, data=data, headers=headers) try: # Python 3 setcookie_header = urlopen(req).info().get('Set-Cookie') except AttributeError: # Python 2 setcookie_header = urlopen(req).info().getheader('Set-Cookie') xvrttoken = TokenResolver._create_token_dictionary(setcookie_header) if xvrttoken is None: return None self._set_cached_token(xvrttoken, token_variant) notification(message=localize(30952)) # Login succeeded. return xvrttoken.get('X-VRT-Token')
def log(level=1, message='', **kwargs): """Log info messages to Kodi""" debug_logging = get_global_setting( 'debug.showloginfo') # Returns a boolean max_log_level = get_setting_int('max_log_level', default=0) if not debug_logging and not (level <= max_log_level and max_log_level != 0): return if kwargs: from string import Formatter message = Formatter().vformat(message, (), SafeDict(**kwargs)) message = '[{addon}] {message}'.format(addon=addon_id(), message=message) xbmc.log(from_unicode(message), level % 3 if debug_logging else 2)
def get_properties(self, api_data): """Get properties from single item json api data""" properties = {} # Only fill in properties when using VRT NU resumepoints because setting resumetime/totaltime breaks standard Kodi watched status if self._resumepoints.is_activated(): asset_id = self.get_asset_id(api_data) if asset_id: # We need to ensure forward slashes are quoted program_title = to_unicode( quote_plus(from_unicode(api_data.get('program')))) url = reformat_url(api_data.get('url', ''), 'medium') properties.update(asset_id=asset_id, url=url, title=program_title) position = self._resumepoints.get_position(asset_id) total = self._resumepoints.get_total(asset_id) # Master over Kodi watch status if position and total and SECONDS_MARGIN < position < total - SECONDS_MARGIN: properties['resumetime'] = position properties['totaltime'] = total log(2, '[Metadata] manual resumetime set to {position}', position=position) episode = self.get_episode(api_data) season = self.get_season(api_data) if episode and season: properties['episodeno'] = 's%se%s' % (season, episode) year = self.get_year(api_data) if year: properties['year'] = year return properties
def set_property(key, value, window_id=10000): """Set a Window property""" from xbmcgui import Window return Window(window_id).setProperty(key, from_unicode(value))
def set_setting(key, value): """Set an add-on setting""" return ADDON.setSetting(key, from_unicode(str(value)))
def show_listing(list_items, category=None, sort='unsorted', ascending=True, content=None, cache=None, selected=None): """Show a virtual directory in Kodi""" from xbmcgui import ListItem from addon import plugin set_property('container.url', 'plugin://' + addon_id() + plugin.path) xbmcplugin.setPluginFanart(handle=plugin.handle, image=from_unicode(addon_fanart())) usemenucaching = get_setting_bool('usemenucaching', default=True) if cache is None: cache = usemenucaching elif usemenucaching is False: cache = False if content: # content is one of: files, songs, artists, albums, movies, tvshows, episodes, musicvideos xbmcplugin.setContent(plugin.handle, content=content) # Jump through hoops to get a stable breadcrumbs implementation category_label = '' if category: if not content: category_label = 'VRT NU / ' if plugin.path.startswith(('/favorites/', '/resumepoints/')): category_label += localize(30428) + ' / ' # My if isinstance(category, int): category_label += localize(category) else: category_label += category elif not content: category_label = 'VRT NU' xbmcplugin.setPluginCategory(handle=plugin.handle, category=category_label) # FIXME: Since there is no way to influence descending order, we force it here if not ascending: sort = 'unsorted' # NOTE: When showing tvshow listings and 'showoneoff' was set, force 'unsorted' if get_setting_bool( 'showoneoff', default=True) and sort == 'label' and content == 'tvshows': sort = 'unsorted' # Add all sort methods to GUI (start with preferred) xbmcplugin.addSortMethod(handle=plugin.handle, sortMethod=SORT_METHODS[sort]) for key in sorted(SORT_METHODS): if key != sort: xbmcplugin.addSortMethod(handle=plugin.handle, sortMethod=SORT_METHODS[key]) # FIXME: This does not appear to be working, we have to order it ourselves # xbmcplugin.setProperty(handle=plugin.handle, key='sort.ascending', value='true' if ascending else 'false') # if ascending: # xbmcplugin.setProperty(handle=plugin.handle, key='sort.order', value=str(SORT_METHODS[sort])) # else: # # NOTE: When descending, use unsorted # xbmcplugin.setProperty(handle=plugin.handle, key='sort.order', value=str(SORT_METHODS['unsorted'])) listing = [] showfanart = get_setting_bool('showfanart', default=True) for title_item in list_items: # Three options: # - item is a virtual directory/folder (not playable, path) # - item is a playable file (playable, path) # - item is non-actionable item (not playable, no path) is_folder = bool(not title_item.is_playable and title_item.path) is_playable = bool(title_item.is_playable and title_item.path) list_item = ListItem(label=title_item.label) prop_dict = dict( IsInternetStream='true' if is_playable else 'false', IsPlayable='true' if is_playable else 'false', IsFolder='false' if is_folder else 'true', ) if title_item.prop_dict: title_item.prop_dict.update(prop_dict) else: title_item.prop_dict = prop_dict # NOTE: The setProperties method is new in Kodi18 try: list_item.setProperties(title_item.prop_dict) except AttributeError: for key, value in list(title_item.prop_dict.items()): list_item.setProperty(key=key, value=str(value)) # FIXME: The setIsFolder method is new in Kodi18, so we cannot use it just yet # list_item.setIsFolder(is_folder) if showfanart: # Add add-on fanart when fanart is missing if not title_item.art_dict: title_item.art_dict = dict(fanart=addon_fanart()) elif not title_item.art_dict.get('fanart'): title_item.art_dict.update(fanart=addon_fanart()) list_item.setArt(title_item.art_dict) if title_item.info_dict: # type is one of: video, music, pictures, game list_item.setInfo(type='video', infoLabels=title_item.info_dict) if title_item.stream_dict: # type is one of: video, audio, subtitle list_item.addStreamInfo('video', title_item.stream_dict) if title_item.context_menu: list_item.addContextMenuItems(title_item.context_menu) url = None if title_item.path: url = title_item.path listing.append((url, list_item, is_folder)) # Jump to specific item if selected is not None: pass # from xbmcgui import getCurrentWindowId, Window # wnd = Window(getCurrentWindowId()) # wnd.getControl(wnd.getFocusId()).selectItem(selected) succeeded = xbmcplugin.addDirectoryItems(plugin.handle, listing, len(listing)) xbmcplugin.endOfDirectory(plugin.handle, succeeded, updateListing=False, cacheToDisc=cache)
def translate_path(path): """Converts a Kodi special:// path to the corresponding OS-specific path""" return to_unicode(translatePath(from_unicode(path)))
def watchlater(episode_id, title): """The API interface to watch an episode used by the context menu""" from resumepoints import ResumePoints ResumePoints().watchlater(episode_id=episode_id, title=to_unicode( unquote_plus(from_unicode(title))))
def follow(program_name, title, program_id=None): """The API interface to follow a program used by the context menu""" from favorites import Favorites Favorites().follow(program_name=program_name, title=to_unicode(unquote_plus(from_unicode(title))), program_id=program_id)
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_context_menu(self, api_data, program, cache_file): """Get context menu""" from addon import plugin favorite_marker = '' watchlater_marker = '' context_menu = [] # WATCH LATER if self._resumepoints.is_activated(): asset_id = self.get_asset_id(api_data) # VRT NU Search API if api_data.get('type') == 'episode': program_title = api_data.get('program') # VRT NU Schedule API (some are missing vrt.whatson-id) elif api_data.get('vrt.whatson-id') or api_data.get('startTime'): program_title = api_data.get('title') if asset_id is not None: # We need to ensure forward slashes are quoted program_title = to_unicode( quote_plus(from_unicode(program_title))) url = url_to_episode(api_data.get('url', '')) if self._resumepoints.is_watchlater(asset_id): extras = {} # If we are in a watchlater menu, move cursor down before removing a favorite if plugin.path.startswith('/resumepoints/watchlater'): extras = dict(move_down=True) # Unwatch context menu context_menu.append( (capitalize(localize(30402)), 'RunPlugin(%s)' % url_for('unwatchlater', asset_id=asset_id, title=program_title, url=url, **extras))) watchlater_marker = '[COLOR={highlighted}]ᶫ[/COLOR]' else: # Watch context menu context_menu.append( (capitalize(localize(30401)), 'RunPlugin(%s)' % url_for('watchlater', asset_id=asset_id, title=program_title, url=url))) # FOLLOW PROGRAM if self._favorites.is_activated(): # VRT NU Search API if api_data.get('type') == 'episode': program_title = api_data.get('program') program_type = api_data.get('programType') follow_suffix = localize( 30410) if program_type != 'oneoff' else '' # program follow_enabled = True # VRT NU Suggest API elif api_data.get('type') == 'program': program_title = api_data.get('title') follow_suffix = '' follow_enabled = True # VRT NU Schedule API (some are missing vrt.whatson-id) elif api_data.get('vrt.whatson-id') or api_data.get('startTime'): program_title = api_data.get('title') follow_suffix = localize(30410) # program follow_enabled = bool(api_data.get('url')) if follow_enabled and program: program_title = to_unicode( quote_plus(from_unicode(program_title)) ) # We need to ensure forward slashes are quoted if self._favorites.is_favorite(program): extras = {} # If we are in a favorites menu, move cursor down before removing a favorite if plugin.path.startswith('/favorites'): extras = dict(move_down=True) context_menu.append(( localize(30412, title=follow_suffix), # Unfollow 'RunPlugin(%s)' % url_for('unfollow', program=program, title=program_title, **extras))) favorite_marker = '[COLOR={highlighted}]ᵛ[/COLOR]' else: context_menu.append(( localize(30411, title=follow_suffix), # Follow 'RunPlugin(%s)' % url_for( 'follow', program=program, title=program_title))) # GO TO PROGRAM if api_data.get('programType') != 'oneoff' and program: if plugin.path.startswith( ('/favorites/offline', '/favorites/recent', '/offline', '/recent', '/resumepoints/continue', '/resumepoints/watchlater', '/tvguide')): context_menu.append(( localize(30417), # Go to program 'Container.Update(%s)' % url_for('programs', program=program, season='allseasons'))) # REFRESH MENU context_menu.append(( localize(30413), # Refresh menu 'RunPlugin(%s)' % url_for('delete_cache', cache_file=cache_file))) return context_menu, colour(favorite_marker), colour(watchlater_marker)