From 273181a7207e1e2484fdce5ef7ceeb30dd242eb7 Mon Sep 17 00:00:00 2001 From: HypnOS-Haxxor Date: Sat, 18 Jul 2020 23:30:02 -0700 Subject: [PATCH] Performance improvements * Implemented caching of session and social tokens. * Refactored API calls to use a central callAPI function. * Implemented caching of API responses based upon caching hints and rules returned from the F1TV APIs. --- addon.xml | 4 +- changelog.txt | 5 + resources/lib/F1TVParser/AccountManager.py | 92 +++++++++-- resources/lib/F1TVParser/F1TV_Minimal_API.py | 159 +++++++++---------- resources/lib/plugin.py | 34 ++-- 5 files changed, 175 insertions(+), 119 deletions(-) diff --git a/addon.xml b/addon.xml index 01a8aa9..ac4ccc7 100644 --- a/addon.xml +++ b/addon.xml @@ -1,9 +1,11 @@ - + + + video diff --git a/changelog.txt b/changelog.txt index 28d6c11..1934632 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,8 @@ +v0.1.4 +- Performance improvements through caching +- Implemented caching of session and social tokens +- Implemented caching of API responses based upon caching hints and rules returned from the F1TV APIs + v0.1.3 - Fixed crash in 'Sets' - Fixed crash in 'List by Circuit' diff --git a/resources/lib/F1TVParser/AccountManager.py b/resources/lib/F1TVParser/AccountManager.py index 2725d90..b47fc77 100644 --- a/resources/lib/F1TVParser/AccountManager.py +++ b/resources/lib/F1TVParser/AccountManager.py @@ -1,6 +1,13 @@ import requests import json +import os +import pyjwt as jwt import re +import xbmc +import xbmcaddon +from cache import Store + +from datetime import datetime __ACCOUNT_API__='https://api.formula1.com/v1/account/' __ACCOUNT_CREATE_SESSION__=__ACCOUNT_API__+'Subscriber/CreateSession' @@ -40,7 +47,7 @@ def exteractSessionData(self): return self.auth_headers['apikey'], self.auth_headers['cd-systemid'] - def __createSession__(self): + def __requestSessionToken(self): login_dict = {"Login": self.username, "Password": self.password} r = self.session.post(__ACCOUNT_CREATE_SESSION__, headers=self.auth_headers, data=json.dumps(login_dict)) @@ -50,24 +57,87 @@ def __createSession__(self): self.session_token = r.json()["data"]["subscriptionToken"] + # Save the token + try: + session_token_store = Store("app://tokens/session/{username}".format(username=self.username)) + session_token_store.clear() + session_token_store.append(self.session_token) + except: + pass + else: + raise ValueError('Account Authentication failed.') + + def __requestSocialToken(self): + dict = {"identity_provider_url": __ACCOUNT_IDENTITY_PROVIDER_URL__, "access_token": self.session_token} + + token_request = self.session.post(__ACCOUNT_SOCIAL_AUTHENTICATE__, data=json.dumps(dict)) + if token_request.ok: + self.session.headers["Authorization"] = "JWT " + token_request.json()["token"] + + # Save the token + try: + session_token_store = Store("app://tokens/social/{username}".format(username=self.username)) + session_token_store.clear() + session_token_store.append(token_request.json()["token"]) + except: + pass + + + + def __createSession__(self): + # Try to load a cached token + try: + session_token_store = Store("app://tokens/session/{username}".format(username=self.username)) + session_tokens = session_token_store.retrieve() + self.session_token = None + if len(session_tokens): + for cached_token in session_tokens: + cached_token_expiration_time = datetime.fromtimestamp(jwt.decode(cached_token, verify=False)['exp']) + + token_validity_time_remaining = cached_token_expiration_time - datetime.now() + + if token_validity_time_remaining.total_seconds() <= 60 * 60 * 24: + self.session_token = None + else: + self.session_token = cached_token + else: + self.session_token = None + except: + self.session_token = None + + if self.session_token is None: + self.__requestSessionToken() else: - raise ValueError('Account Authentification failed.') + pass def __createAuthorization__(self): if self.session_token is not None: - dict = {"identity_provider_url": __ACCOUNT_IDENTITY_PROVIDER_URL__, "access_token": self.session_token} + # Try to load a cached social token + try: + social_token_store = Store("app://tokens/social/{username}".format(username=self.username)) + social_tokens = social_token_store.retrieve() + if len(social_tokens): + for cached_token in social_tokens: + cached_token_expiration_time = datetime.fromtimestamp(jwt.decode(cached_token, verify=False)['exp']) - token_request = self.session.post(__ACCOUNT_SOCIAL_AUTHENTICATE__, data=json.dumps(dict)) - if token_request.ok: - self.session.headers["Authorization"] = "JWT " + token_request.json()["token"] + token_validity_time_remaining = cached_token_expiration_time - datetime.now() + + if token_validity_time_remaining.total_seconds() <= 60 * 60 * 24: + self.__requestSocialToken() + else: + self.session.headers["Authorization"] = "JWT " + cached_token + else: + self.__requestSocialToken() + + except: + self.__requestSocialToken() + + def __init__(self): - def __init__(self, username = None, password = None, token = None): - self.username = username - self.password = password - self.session_token = token self.auth_headers = {"CD-Language": "de-DE", "Content-Type": "application/json"} self.session = requests.session() + self.session_token = None def getSession(self): if self.session_token is None: @@ -84,4 +154,4 @@ def setSessionData(self, apikey, system_id): def login(self, username, password): self.username = username self.password = password - return self.getSession() \ No newline at end of file + return self.getSession() diff --git a/resources/lib/F1TVParser/F1TV_Minimal_API.py b/resources/lib/F1TVParser/F1TV_Minimal_API.py index 25242f1..eff4390 100644 --- a/resources/lib/F1TVParser/F1TV_Minimal_API.py +++ b/resources/lib/F1TVParser/F1TV_Minimal_API.py @@ -1,10 +1,11 @@ import AccountManager import json +import xbmc +import os +import urllib +from cache import Cache, conditional_headers -''' General Entry point for F1TV API''' -__TV_API__='https://f1tv-api.formula1.com/agl/1.0/gbr/en/all_devices/global/' -__OLD_TV_API__='https://f1tv.formula1.com' ''' Parameters for different F1TV API calls''' __TV_API_PARAMS__ = {"event-occurrence": {"fields_to_expand": "image_urls,sessionoccurrence_urls,sessionoccurrence_urls__image_urls", @@ -22,10 +23,55 @@ } - class F1TV_API: """ Main API Object - is used to retrieve API information """ + def callAPI(self, endpoint, method="GET", api_ver=2, params=None, data=None): + if int(api_ver) == 1: + complete_url = 'https://f1tv.formula1.com' + endpoint + elif int(api_ver) == 2: + complete_url = 'https://f1tv-api.formula1.com/agl/1.0/gbr/en/all_devices/global/' + endpoint + else: + xbmc.log("Unable to make an API with invalid API version: {}".format(api_ver), xbmc.LOGERROR) + return + + if method.upper() == 'GET': + # Check to see if we've cached the response + with Cache() as c: + if params: + url_with_parameters = "{complete_url}?{parameters}".format(complete_url=complete_url, + parameters=urllib.urlencode(params)) + else: + url_with_parameters = complete_url + cached = c.get(url_with_parameters) + if cached: + # If we have a fresh cached version, return it. + if cached["fresh"]: + return json.loads(cached["blob"]) + # otherwise append applicable "If-None-Match"/"If-Modified-Since" headers + self.account_manager.getSession().headers.update(conditional_headers(cached)) + # request a new version of the data + r = self.account_manager.getSession().get(complete_url, params=params, data=data) + if 200 == r.status_code: + # add the new data and headers to the cache + c.set(url_with_parameters, r.content, r.headers) + return r.json() + if 304 == r.status_code: + # the data hasn't been modified so just touch the cache with the new headers + # and return the existing data + c.touch(url_with_parameters, r.headers) + return json.loads(cached["blob"]) + + + elif method.upper() == 'POST': + r = self.account_manager.getSession().post(complete_url, params=params, data=data) + if r.ok: + return r.json() + else: + return + else: + return + def getFields(self, url): for key in __TV_API_PARAMS__: if key in url: @@ -42,114 +88,63 @@ def login(self, username, password): def getStream(self, url): """ Get stream for supplied viewings item This will get the m3u8 url for Content and Channel.""" - complete_url = __OLD_TV_API__ + "/api/viewings/" item_dict = {"asset_url" if 'ass' in url else "channel_url": url} - viewing = self.account_manager.getSession().post(complete_url, data=json.dumps(item_dict)) + viewing_json = self.callAPI("/api/viewings/", api_ver=1, method='POST', data=json.dumps(item_dict)) - if viewing.ok: - viewing_json = viewing.json() - if 'chan' in url: - return viewing_json["tokenised_url"] - else: - return viewing_json["objects"][0]["tata"]["tokenised_url"] + if 'chan' in url: + return viewing_json["tokenised_url"] + else: + return viewing_json["objects"][0]["tata"]["tokenised_url"] def getSession(self, url): """ Get Session Object from API by supplying an url""" - complete_url = __TV_API__ + url - r = self.account_manager.getSession().get(complete_url, params=__TV_API_PARAMS__["session-occurrence"]) - - if r.ok: - return r.json() + session = self.callAPI(url, params=__TV_API_PARAMS__["session-occurrence"]) + return session def getEvent(self, url, season = None): """ Get Event object from API by supplying an url""" - complete_url = __TV_API__ + url - r = self.account_manager.getSession().get(complete_url, params=__TV_API_PARAMS__["event-occurrence"]) - - if r.ok: - return r.json() + event = self.callAPI(url, params=__TV_API_PARAMS__["event-occurrence"]) + return event def getSeason(self, url): """ Get Season object from API by supplying an url""" - complete_url = __OLD_TV_API__ + url - r = self.account_manager.getSession().get(complete_url, params=self.getFields(url)) #__TV_API_PARAMS__["season"]) - - if r.ok: - return r.json() + season = self.callAPI(url, api_ver=1, params=self.getFields(url)) + return season def getSeasons(self): """ Get all season urls that are available at API""" - complete_url = __OLD_TV_API__ + "/api/race-season/" - r = self.account_manager.getSession().get(complete_url, params={'order': '-year'}) - - if r.ok: - return r.json() + seasons = self.callAPI("/api/race-season/", api_ver=1, params={'order': '-year'}) + return seasons def getCircuits(self): """ Get all Circuit urls that are available at API""" - complete_url = __OLD_TV_API__ + "/api/circuit/" - r = self.account_manager.getSession().get(complete_url, params={"fields": "name,eventoccurrence_urls,self"}) - - if r.ok: - return r.json() + circuits = self.callAPI("/api/circuit/", api_ver=1, params={"fields": "name,eventoccurrence_urls,self"}) + return circuits def getCircuit(self, url): """ Get Circuit object from API by supplying an url""" - complete_url = __OLD_TV_API__ + url - r = self.account_manager.getSession().get(complete_url, params=__TV_API_PARAMS__["circuit"]) - - if r.ok: - return r.json() + circuit = self.callAPI(url, api_ver=1, params=__TV_API_PARAMS__["circuit"]) + return circuit def getF2(self): - complete_url = __TV_API__+"/api/sets/coll_4440e712d31d42fb95c9a2145ab4dac7" - r = self.account_manager.getSession().get(complete_url) - if r.ok: - return r.json() - - def getAnyURL(self, url): - complete_url = __TV_API__+url - r = self.account_manager.getSession().get(complete_url) - if r.ok: - return r.json() - - def getAnyOldURL(self, url): - complete_url = __OLD_TV_API__+url - r = self.account_manager.getSession().get(complete_url) - if r.ok: - return r.json() + f2 = self.callAPI("/api/sets/coll_4440e712d31d42fb95c9a2145ab4dac7") + return f2 def getSets(self): - complete_url = __OLD_TV_API__ + "/api/sets/?slug=home" - r = self.account_manager.getSession().get(complete_url) - if r.ok: - rj = r.json() + sets = self.callAPI("/api/sets/?slug=home", api_ver=1) content = {} - for item in rj['objects'][0]['items']: - itemj = self.account_manager.getSession().get(__OLD_TV_API__+item['content_url']).json() - if 'title' in list(itemj): - content[itemj['title']] = item['content_url'] - elif 'name' in list(itemj): - content[itemj['name']] = item['content_url'] + for item in sets['objects'][0]['items']: + item_details = self.callAPI(item['content_url'], api_ver=1) + if 'title' in list(item_details): + content[item_details['title']] = item['content_url'] + elif 'name' in list(item_details): + content[item_details['name']] = item['content_url'] else: - content[itemj['UNKNOWN SET: ' + 'uid']] = item['content_url'] + content[item_details['UNKNOWN SET: ' + 'uid']] = item['content_url'] return content - - def getSetContent(self, url): - complete_url = __OLD_TV_API__ + url - r = self.account_manager.getSession().get(complete_url) - if r.ok: - return r.json() - - def getEpisode(self, url): - complete_url = __OLD_TV_API__ + url - r = self.account_manager.getSession().get(complete_url) - if r.ok: - return r.json() - def setLanguage(self, language): self.account_manager.session.headers['Accept-Language'] = "{}, en".format(language.upper()) diff --git a/resources/lib/plugin.py b/resources/lib/plugin.py index 73d7282..13bf479 100644 --- a/resources/lib/plugin.py +++ b/resources/lib/plugin.py @@ -47,10 +47,10 @@ def get_mainpage(): #Get the current live event from sets - sometimes there is nothing live, easiest way to stop errors is to just try: except: try: - response = _api_manager.getAnyOldURL('/api/sets?slug=grand-prix-weekend-live') + response = _api_manager.callAPI('/api/sets?slug=grand-prix-weekend-live', api_ver=1) eventurl = response['objects'][0]['items'][0]['content_url'].replace("/api/","") #Get name for nice display - response = _api_manager.getAnyURL(eventurl) + response = _api_manager.callAPI(eventurl) eventname = "Current Event - "+response['name'] list_item = xbmcgui.ListItem(label=eventname) url = get_url(action='list_sessions', event_url=eventurl, event_name=eventname) @@ -86,8 +86,7 @@ def get_mainpage(): def sets(): - _api_manager.login(_ADDON.getSetting("username"), _ADDON.getSetting("password")) - + xbmcplugin.setPluginCategory(_handle, 'Sets') xbmcplugin.setContent(_handle, 'videos') @@ -117,7 +116,7 @@ def sets(): xbmcplugin.addDirectoryItem(_handle, url, list_item, True) elif "/api/episodes" in sets[key]: #It's an episode. - epi_data = _api_manager.getEpisode(sets[key]) + epi_data = _api_manager.callAPI(sets[key], api_ver=1) name = epi_data['title'] asset = epi_data['items'][0] list_item = xbmcgui.ListItem(label=name) @@ -133,15 +132,14 @@ def sets(): xbmcplugin.endOfDirectory(_handle) def setContents(content_url): - _api_manager.login(_ADDON.getSetting("username"), _ADDON.getSetting("password")) - set_content = _api_manager.getSetContent(content_url) + set_content = _api_manager.callAPI(content_url, api_ver=1) xbmcplugin.setPluginCategory(_handle, set_content['title']) xbmcplugin.setContent(_handle, 'videos') for item in set_content['items']: - epi_data = _api_manager.getEpisode(item['content_url']) + epi_data = _api_manager.callAPI(item['content_url'], api_ver=1) name = epi_data['title'] asset = epi_data['items'][0] @@ -160,8 +158,6 @@ def setContents(content_url): def list_seasons(): - _api_manager.login(_ADDON.getSetting("username"), _ADDON.getSetting("password")) - # Set plugin category. It is displayed in some skins as the name # of the current section. xbmcplugin.setPluginCategory(_handle, 'Season Overview') @@ -193,7 +189,6 @@ def list_seasons(): def list_circuits(): - _api_manager.login(_ADDON.getSetting("username"), _ADDON.getSetting("password")) # Set plugin category. It is displayed in some skins as the name # of the current section. @@ -211,7 +206,6 @@ def list_circuits(): continue list_item = xbmcgui.ListItem(label=circuit['name']) - xbmc.log(fix_string(circuit['name']), xbmc.LOGNOTICE) list_item.setInfo('video', {'title': circuit['name'], 'genre': "Motorsport", 'mediatype': 'video'}) @@ -229,8 +223,6 @@ def list_circuits(): def list_season_events(season_url, year): - _api_manager.login(_ADDON.getSetting("username"), _ADDON.getSetting("password")) - # Set plugin category. It is displayed in some skins as the name # of the current section. xbmcplugin.setPluginCategory(_handle, 'Season ' + str(year)) @@ -243,7 +235,6 @@ def list_season_events(season_url, year): round_counter = 1 for event in season['eventoccurrence_urls']: - xbmc.log(str(event), xbmc.LOGNOTICE) if 'start_date' in event and event['start_date'] is not None: try: @@ -288,8 +279,6 @@ def list_season_events(season_url, year): def list_circuit_events(circuit_url, circuit_name): - _api_manager.login(_ADDON.getSetting("username"), _ADDON.getSetting("password")) - # Set plugin category. It is displayed in some skins as the name # of the current section. xbmcplugin.setPluginCategory(_handle, circuit_name) @@ -298,10 +287,8 @@ def list_circuit_events(circuit_url, circuit_name): xbmcplugin.setContent(_handle, 'videos') # Get video categories circuit = _api_manager.getCircuit(circuit_url) - xbmc.log(str(circuit), xbmc.LOGNOTICE) for event in circuit['eventoccurrence_urls']: - xbmc.log(str(event), xbmc.LOGNOTICE) @@ -342,8 +329,6 @@ def list_circuit_events(circuit_url, circuit_name): xbmcplugin.endOfDirectory(_handle) def list_sessions(event_url, event_name): - _api_manager.login(_ADDON.getSetting("username"), _ADDON.getSetting("password")) - xbmc.log("{} - {}".format(event_name, event_url), xbmc.LOGINFO) # Set plugin category. It is displayed in some skins as the name @@ -392,7 +377,6 @@ def list_sessions(event_url, event_name): def list_content(session_url, session_name): - _api_manager.login(_ADDON.getSetting("username"), _ADDON.getSetting("password")) # Set plugin category. It is displayed in some skins as the name # of the current section. @@ -420,7 +404,7 @@ def list_content(session_url, session_name): thumb = image['url'] break # Create a list item with a text label and a thumbnail image. - channel = _api_manager.getAnyOldURL(channel) + channel = _api_manager.callAPI(channel, api_ver=1) name = channel['name'] if 'WIF' not in channel['name'] else session['session_name'] list_item = xbmcgui.ListItem(label=name) @@ -441,7 +425,7 @@ def list_content(session_url, session_name): xbmcplugin.addDirectoryItem(_handle, url, list_item, is_folder) for content in session['content_urls']: - content = _api_manager.getAnyOldURL(content) + content = _api_manager.callAPI(content, api_ver=1) thumb = '' for image in content['image_urls']: thumb = image @@ -516,7 +500,6 @@ def getCorrectedM3U8(stream_url): def playContent(content_url): - _api_manager.login(_ADDON.getSetting("username"), _ADDON.getSetting("password")) stream_url = _api_manager.getStream(content_url) @@ -577,6 +560,7 @@ def router(paramstring): def run(): + _api_manager.setLanguage(xbmc.getLanguage(format=xbmc.ISO_639_1)) if _ADDON.getSetting('apikey') == '' or _ADDON.getSetting('system_id') == '':