diff --git a/.gitignore b/.gitignore index 7b182f83..c7c4c3b8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,9 @@ Thumbs.db *~ .cache - .coverage .tox/ + +# Testing tests/home/userdata/addon_data +.env diff --git a/resources/lib/vtmgo/vtmgo.py b/resources/lib/vtmgo/vtmgo.py index f492b20b..9faf8cf3 100644 --- a/resources/lib/vtmgo/vtmgo.py +++ b/resources/lib/vtmgo/vtmgo.py @@ -8,7 +8,7 @@ import requests -from resources.lib.vtmgo.vtmgoauth import VtmGoAuth +from resources.lib.vtmgo.vtmgoauth import VtmGoAuth, InvalidLoginException _LOGGER = logging.getLogger('vtmgo') @@ -117,8 +117,8 @@ def __repr__(self): class Movie: """ Defines a Movie """ - def __init__(self, movie_id=None, name=None, description=None, year=None, cover=None, image=None, duration=None, remaining=None, geoblocked=None, - channel=None, legal=None, aired=None, my_list=None): + def __init__(self, movie_id=None, name=None, description=None, year=None, cover=None, image=None, duration=None, + remaining=None, geoblocked=None, channel=None, legal=None, aired=None, my_list=None): """ :type movie_id: str :type name: str @@ -155,8 +155,8 @@ def __repr__(self): class Program: """ Defines a Program """ - def __init__(self, program_id=None, name=None, description=None, cover=None, image=None, seasons=None, geoblocked=None, channel=None, legal=None, - my_list=None): + def __init__(self, program_id=None, name=None, description=None, cover=None, image=None, seasons=None, + geoblocked=None, channel=None, legal=None, my_list=None): """ :type program_id: str :type name: str @@ -210,8 +210,9 @@ def __repr__(self): class Episode: """ Defines an Episode """ - def __init__(self, episode_id=None, program_id=None, program_name=None, number=None, season=None, name=None, description=None, cover=None, duration=None, - remaining=None, geoblocked=None, channel=None, legal=None, aired=None, progress=None, watched=False, next_episode=None): + def __init__(self, episode_id=None, program_id=None, program_name=None, number=None, season=None, name=None, + description=None, cover=None, duration=None, remaining=None, geoblocked=None, channel=None, legal=None, + aired=None, progress=None, watched=False, next_episode=None): """ :type episode_id: str :type program_id: str @@ -259,25 +260,39 @@ def __repr__(self): class VtmGo: """ VTM GO API """ + API_ENDPOINT = 'https://api.vtmgo.be' + CONTENT_TYPE_MOVIE = 'MOVIE' CONTENT_TYPE_PROGRAM = 'PROGRAM' CONTENT_TYPE_EPISODE = 'EPISODE' - _HEADERS = { - 'x-app-version': '8', - 'x-persgroep-mobile-app': 'true', - 'x-persgroep-os': 'android', - 'x-persgroep-os-version': '23', - } - def __init__(self, kodi): """ Initialise object :type kodi: resources.lib.kodiwrapper.KodiWrapper """ self._kodi = kodi - self._proxies = kodi.get_proxies() self._auth = VtmGoAuth(kodi) + self._session = requests.session() + self._session.proxies = kodi.get_proxies() + self._authenticate() + + def _authenticate(self): + """ Apply authentication headers in the session """ + self._session.headers = { + 'x-app-version': '8', + 'x-persgroep-mobile-app': 'true', + 'x-persgroep-os': 'android', + 'x-persgroep-os-version': '23', + } + token = self._auth.get_token() + if token: + self._session.headers['x-dpp-jwt'] = token + + profile = self._auth.get_profile() + if profile: + self._session.headers['x-dpp-profile'] = profile + def _mode(self): """ Return the mode that should be used for API calls """ return 'vtmgo-kids' if self.get_product() == 'VTM_GO_KIDS' else 'vtmgo' @@ -611,7 +626,8 @@ def get_program(self, program_id, cache=CACHE_AUTO): seasons[item_season.get('index')] = Season( number=item_season.get('index'), episodes=episodes, - cover=item_season.get('episodes', [{}])[0].get('bigPhotoUrl') if episodes else program.get('bigPhotoUrl'), + cover=item_season.get('episodes', [{}])[0].get('bigPhotoUrl') + if episodes else program.get('bigPhotoUrl'), geoblocked=program.get('geoBlocked'), channel=channel, legal=program.get('legalIcons'), @@ -732,7 +748,7 @@ def get_product(self): profile = self._kodi.get_setting('profile') try: return profile.split(':')[1] - except IndexError: + except (IndexError, AttributeError): return None @staticmethod @@ -751,123 +767,85 @@ def _parse_channel(url): def _get_url(self, url, params=None): """ Makes a GET request for the specified URL. :type url: str + :type params: dict :rtype str """ - headers = self._HEADERS - token = self._auth.get_token() - if token: - headers['x-dpp-jwt'] = token - - profile = self._auth.get_profile() - if profile: - headers['x-dpp-profile'] = profile - - _LOGGER.debug('Sending GET %s...', url) - - response = requests.session().get('https://lfvp-api.dpgmedia.net' + url, params=params, headers=headers, proxies=self._proxies) - - # Set encoding to UTF-8 if no charset is indicated in http headers (https://github.com/psf/requests/issues/1604) - if not response.encoding: - response.encoding = 'utf-8' - - _LOGGER.debug('Got response (status=%s): %s', response.status_code, response.text) - - if response.status_code == 404: - raise UnavailableException() - - if response.status_code == 426: - raise ApiUpdateRequired() - - if response.status_code not in [200, 204]: - raise Exception('Error %s.' % response.status_code) - - return response.text - - def _put_url(self, url): + try: + return self._request('GET', url, params=params) + except InvalidLoginException: + self._auth.clear_token() + self._authenticate() + # Retry the same request + return self._request('GET', url, params=params) + + def _put_url(self, url, params=None): """ Makes a PUT request for the specified URL. :type url: str + :type params: dict :rtype str """ - headers = self._HEADERS - token = self._auth.get_token() - if token: - headers['x-dpp-jwt'] = token - - profile = self._auth.get_profile() - if profile: - headers['x-dpp-profile'] = profile - - _LOGGER.debug('Sending PUT %s...', url) - - response = requests.session().put('https://api.vtmgo.be' + url, headers=headers, proxies=self._proxies) - - _LOGGER.debug('Got response (status=%s): %s', response.status_code, response.text) - - if response.status_code == 404: - raise UnavailableException() - - if response.status_code == 426: - raise ApiUpdateRequired() - - if response.status_code not in [200, 204]: - raise Exception('Error %s.' % response.status_code) - - return response.text - - def _post_url(self, url): + try: + return self._request('PUT', url, params=params) + except InvalidLoginException: + self._auth.clear_token() + self._authenticate() + # Retry the same request + return self._request('PUT', url, params=params) + + def _post_url(self, url, params=None, data=None): """ Makes a POST request for the specified URL. :type url: str + :type params: dict + :type data: dict :rtype str """ - headers = self._HEADERS - token = self._auth.get_token() - if token: - headers['x-dpp-jwt'] = token - - profile = self._auth.get_profile() - if profile: - headers['x-dpp-profile'] = profile - - _LOGGER.debug('Sending POST %s...', url) - - response = requests.session().post('https://api.vtmgo.be' + url, headers=headers, proxies=self._proxies) - - _LOGGER.debug('Got response (status=%s): %s', response.status_code, response.text) - - if response.status_code == 404: - raise UnavailableException() - - if response.status_code == 426: - raise ApiUpdateRequired() - - if response.status_code not in [200, 204]: - raise Exception('Error %s.' % response.status_code) - - return response.text - - def _delete_url(self, url): + try: + return self._request('POST', url, params=params, data=data) + except InvalidLoginException: + self._auth.clear_token() + self._authenticate() + # Retry the same request + return self._request('POST', url, params=params) + + def _delete_url(self, url, params=None): """ Makes a DELETE request for the specified URL. :type url: str + :type params: dict :rtype str """ - headers = self._HEADERS - token = self._auth.get_token() - if token: - headers['x-dpp-jwt'] = token - - profile = self._auth.get_profile() - if profile: - headers['x-dpp-profile'] = profile - - _LOGGER.debug('Sending DELETE %s...', url) + try: + return self._request('DELETE', url, params=params) + except InvalidLoginException: + self._auth.clear_token() + self._authenticate() + # Retry the same request + return self._request('DELETE', url, params=params) + + def _request(self, method, url, params=None, data=None): + """ Makes a request for the specified URL. + :type url: str + :type params: dict + :type data: dict + :rtype str + """ + _LOGGER.debug('Sending %s %s...', method, url) + response = self._session.request(method, + self.API_ENDPOINT + url, + params=params, + json=data) - response = requests.session().delete('https://api.vtmgo.be' + url, headers=headers, proxies=self._proxies) + # Set encoding to UTF-8 if no charset is indicated in http headers (https://github.com/psf/requests/issues/1604) + if not response.encoding: + response.encoding = 'utf-8' _LOGGER.debug('Got response (status=%s): %s', response.status_code, response.text) if response.status_code == 404: raise UnavailableException() + if response.status_code == 401: + raise InvalidLoginException() + if response.status_code == 426: raise ApiUpdateRequired() diff --git a/resources/lib/vtmgo/vtmgoauth.py b/resources/lib/vtmgo/vtmgoauth.py index bc2957de..26d3bbe8 100644 --- a/resources/lib/vtmgo/vtmgoauth.py +++ b/resources/lib/vtmgo/vtmgoauth.py @@ -53,16 +53,17 @@ def has_credentials_changed(self): def clear_token(self): """ Remove the cached JWT. """ _LOGGER.debug('Clearing token cache') - self._token = None path = os.path.join(self._kodi.get_userdata_path(), 'token.json') if self._kodi.check_if_path_exists(path): self._kodi.delete_file(path) - self._kodi.set_setting('profile', None) + self._token = None def get_token(self): """ Return a JWT that can be used to authenticate the user. :rtype str """ + userdata_path = self._kodi.get_userdata_path() + # Don't return a token when we have no password or username. if not self._kodi.get_setting('username') or not self._kodi.get_setting('password'): _LOGGER.debug('Skipping since we have no username or password') @@ -74,7 +75,7 @@ def get_token(self): return self._token # Try to load from cache - path = os.path.join(self._kodi.get_userdata_path(), 'token.json') + path = os.path.join(userdata_path, 'token.json') if self._kodi.check_if_path_exists(path): _LOGGER.debug('Returning token from cache') @@ -88,6 +89,10 @@ def get_token(self): self._token = self._login() _LOGGER.debug('Returning token from VTM GO') + # Make sure the path exists + if not self._kodi.check_if_path_exists(userdata_path): + self._kodi.mkdirs(userdata_path) + with self._kodi.open_file(path, 'w') as fdesc: fdesc.write(from_unicode(self._token)) @@ -98,7 +103,7 @@ def get_profile(self): profile = self._kodi.get_setting('profile') try: return profile.split(':')[0] - except IndexError: + except (IndexError, AttributeError): return None def _login(self): diff --git a/resources/lib/vtmgo/vtmgostream.py b/resources/lib/vtmgo/vtmgostream.py index 98d8a3aa..be07e085 100644 --- a/resources/lib/vtmgo/vtmgostream.py +++ b/resources/lib/vtmgo/vtmgostream.py @@ -266,7 +266,7 @@ def _delay_subtitles(self, subtitles, json_manifest): import re if not self._kodi.check_if_path_exists(temp_dir): - self._kodi.mkdir(temp_dir) + self._kodi.mkdirs(temp_dir) ad_breaks = list() delayed_subtitles = list()