From 1140b9b07a52a0ffe2f306ece3e550b674b3ebe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Tue, 9 Feb 2021 20:54:40 +0100 Subject: [PATCH] Implement My List (#71) * Fetch my list from amazon cognito sync * Allow updating My List --- .../resource.language.en_gb/strings.po | 16 ++ .../resource.language.nl_nl/strings.po | 16 ++ resources/lib/addon.py | 21 ++ resources/lib/modules/catalog.py | 63 +++++- resources/lib/modules/menu.py | 33 ++++ resources/lib/modules/player.py | 2 +- resources/lib/viervijfzes/auth.py | 38 +++- resources/lib/viervijfzes/aws/__init__.py | 0 .../lib/viervijfzes/aws/cognito_identity.py | 71 +++++++ .../{auth_awsidp.py => aws/cognito_idp.py} | 13 +- resources/lib/viervijfzes/aws/cognito_sync.py | 187 ++++++++++++++++++ resources/lib/viervijfzes/content.py | 30 ++- resources/lib/viervijfzes/search.py | 34 ++-- tests/test_auth.py | 8 +- tests/test_mylist.py | 45 +++++ 15 files changed, 544 insertions(+), 33 deletions(-) create mode 100644 resources/lib/viervijfzes/aws/__init__.py create mode 100644 resources/lib/viervijfzes/aws/cognito_identity.py rename resources/lib/viervijfzes/{auth_awsidp.py => aws/cognito_idp.py} (97%) create mode 100644 resources/lib/viervijfzes/aws/cognito_sync.py create mode 100644 tests/test_mylist.py diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 64e56d4..1a4b288 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -34,6 +34,14 @@ msgctxt "#30010" msgid "Search trough the catalogue" msgstr "" +msgctxt "#30011" +msgid "My List" +msgstr "" + +msgctxt "#30012" +msgid "Browse My List" +msgstr "" + msgctxt "#30013" msgid "TV guide" msgstr "" @@ -74,6 +82,14 @@ msgstr "" ### CONTEXT MENU +msgctxt "#30100" +msgid "Add to My List" +msgstr "" + +msgctxt "#30101" +msgid "Remove from My List" +msgstr "" + msgctxt "#30102" msgid "Go to Program" msgstr "" diff --git a/resources/language/resource.language.nl_nl/strings.po b/resources/language/resource.language.nl_nl/strings.po index 8d23590..5613e72 100644 --- a/resources/language/resource.language.nl_nl/strings.po +++ b/resources/language/resource.language.nl_nl/strings.po @@ -35,6 +35,14 @@ msgctxt "#30010" msgid "Search trough the catalogue" msgstr "Doorzoek de catalogus" +msgctxt "#30011" +msgid "My List" +msgstr "Mijn lijst" + +msgctxt "#30012" +msgid "Browse My List" +msgstr "Bekijk mijn lijst" + msgctxt "#30013" msgid "TV guide" msgstr "Tv-gids" @@ -75,6 +83,14 @@ msgstr "Bekijk korte videoclips van [B]{program}[/B]" ### CONTEXT MENU +msgctxt "#30100" +msgid "Add to My List" +msgstr "Toevoegen aan mijn lijst" + +msgctxt "#30101" +msgid "Remove from My List" +msgstr "Verwijderen uit mijn lijst" + msgctxt "#30102" msgid "Go to Program" msgstr "Ga naar programma" diff --git a/resources/lib/addon.py b/resources/lib/addon.py index 7cdd181..2d2b638 100644 --- a/resources/lib/addon.py +++ b/resources/lib/addon.py @@ -98,6 +98,27 @@ def show_catalog_program_season(program, season): Catalog().show_program_season(program, season) +@routing.route('/mylist') +def show_mylist(): + """ Show my list """ + from resources.lib.modules.catalog import Catalog + Catalog().show_mylist() + + +@routing.route('/mylist/add/') +def mylist_add(uuid): + """ Add a program to My List """ + from resources.lib.modules.catalog import Catalog + Catalog().mylist_add(uuid) + + +@routing.route('/mylist/del/') +def mylist_del(uuid): + """ Remove a program from My List """ + from resources.lib.modules.catalog import Catalog + Catalog().mylist_del(uuid) + + @routing.route('/search') @routing.route('/search/') def show_search(query=None): diff --git a/resources/lib/modules/catalog.py b/resources/lib/modules/catalog.py index 8018088..f1c2da0 100644 --- a/resources/lib/modules/catalog.py +++ b/resources/lib/modules/catalog.py @@ -4,6 +4,9 @@ from __future__ import absolute_import, division, unicode_literals import logging +from datetime import datetime + +import dateutil.tz from resources.lib import kodiutils from resources.lib.kodiutils import TitleItem @@ -19,8 +22,8 @@ class Catalog: def __init__(self): """ Initialise object """ - auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) - self._api = ContentApi(auth, cache_path=kodiutils.get_cache_path()) + self._auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) + self._api = ContentApi(self._auth, cache_path=kodiutils.get_cache_path()) def show_catalog(self): """ Show all the programs of all channels """ @@ -174,3 +177,59 @@ class Catalog: # Sort like we get our results back. kodiutils.show_listing(listing, 30003, content='episodes') + + def show_mylist(self): + """ Show all the programs of all channels """ + try: + mylist, _ = self._auth.get_dataset('myList') + except Exception as ex: + kodiutils.notification(message=str(ex)) + raise + + items = [] + for item in mylist: + program = self._api.get_program_by_uuid(item.get('id')) + if program: + program.my_list = True + items.append(program) + + listing = [Menu.generate_titleitem(item) for item in items] + + # Sort items by title + # Used for A-Z listing or when movies and episodes are mixed. + kodiutils.show_listing(listing, 30011, content='tvshows', sort='title') + + def mylist_add(self, uuid): + """ Add a program to My List """ + if not uuid: + kodiutils.end_of_directory() + return + + mylist, sync_info = self._auth.get_dataset('myList') + + if uuid not in [item.get('id') for item in mylist]: + # Python 2.7 doesn't support .timestamp(), and windows doesn't do '%s', so we need to calculate it ourself + epoch = datetime(1970, 1, 1, tzinfo=dateutil.tz.gettz('UTC')) + now = datetime.now(tz=dateutil.tz.gettz('UTC')) + timestamp = str(int((now - epoch).total_seconds())) + '000' + + mylist.append({ + 'id': uuid, + 'timestamp': timestamp, + }) + + self._auth.put_dataset('myList', mylist, sync_info) + + kodiutils.end_of_directory() + + def mylist_del(self, uuid): + """ Remove a program from My List """ + if not uuid: + kodiutils.end_of_directory() + return + + mylist, sync_info = self._auth.get_dataset('myList') + new_mylist = [item for item in mylist if item.get('id') != uuid] + self._auth.put_dataset('myList', new_mylist, sync_info) + + kodiutils.end_of_directory() diff --git a/resources/lib/modules/menu.py b/resources/lib/modules/menu.py index 3c48454..8ae2fcf 100644 --- a/resources/lib/modules/menu.py +++ b/resources/lib/modules/menu.py @@ -41,6 +41,17 @@ class Menu: plot=kodiutils.localize(30008), ) ), + TitleItem( + title=kodiutils.localize(30011), # My List + path=kodiutils.url_for('show_mylist'), + art_dict=dict( + icon='DefaultPlaylist.png', + fanart=kodiutils.get_addon_info('fanart'), + ), + info_dict=dict( + plot=kodiutils.localize(30012), + ) + ), TitleItem( title=kodiutils.localize(30009), # Search path=kodiutils.url_for('show_search'), @@ -90,8 +101,30 @@ class Menu: # We have episodes, or we don't know it title = item.title + context_menu = [] + if item.uuid: + if item.my_list: + context_menu.append(( + kodiutils.localize(30101), # Remove from My List + 'Container.Update(%s)' % + kodiutils.url_for('mylist_del', uuid=item.uuid) + )) + else: + context_menu.append(( + kodiutils.localize(30100), # Add to My List + 'Container.Update(%s)' % + kodiutils.url_for('mylist_add', uuid=item.uuid) + )) + + context_menu.append(( + kodiutils.localize(30102), # Go to Program + 'Container.Update(%s)' % + kodiutils.url_for('show_catalog_program', program=item.path) + )) + return TitleItem(title=title, path=kodiutils.url_for('show_catalog_program', program=item.path), + context_menu=context_menu, art_dict=art_dict, info_dict=info_dict) diff --git a/resources/lib/modules/player.py b/resources/lib/modules/player.py index 851d837..2f79eeb 100644 --- a/resources/lib/modules/player.py +++ b/resources/lib/modules/player.py @@ -9,7 +9,7 @@ from resources.lib import kodiutils from resources.lib.modules.menu import Menu from resources.lib.viervijfzes import CHANNELS, ResolvedStream from resources.lib.viervijfzes.auth import AuthApi -from resources.lib.viervijfzes.auth_awsidp import AuthenticationException, InvalidLoginException +from resources.lib.viervijfzes.aws.cognito_idp import AuthenticationException, InvalidLoginException from resources.lib.viervijfzes.content import ContentApi, GeoblockedException, UnavailableException _LOGGER = logging.getLogger(__name__) diff --git a/resources/lib/viervijfzes/auth.py b/resources/lib/viervijfzes/auth.py index 79ce3e2..70a92ed 100644 --- a/resources/lib/viervijfzes/auth.py +++ b/resources/lib/viervijfzes/auth.py @@ -9,7 +9,9 @@ import os import time from resources.lib import kodiutils -from resources.lib.viervijfzes.auth_awsidp import AuthenticationException, AwsIdp, InvalidLoginException +from resources.lib.viervijfzes.aws.cognito_identity import CognitoIdentity +from resources.lib.viervijfzes.aws.cognito_idp import AuthenticationException, CognitoIdp, InvalidLoginException +from resources.lib.viervijfzes.aws.cognito_sync import CognitoSync _LOGGER = logging.getLogger(__name__) @@ -19,6 +21,7 @@ class AuthApi: COGNITO_REGION = 'eu-west-1' COGNITO_POOL_ID = 'eu-west-1_dViSsKM5Y' COGNITO_CLIENT_ID = '6s1h851s8uplco5h6mqh1jac8m' + COGNITO_IDENTITY_POOL_ID = 'eu-west-1:8b7eb22c-cf61-43d5-a624-04b494867234' TOKEN_FILE = 'auth-tokens.json' @@ -93,11 +96,36 @@ class AuthApi: @staticmethod def _authenticate(username, password): """ Authenticate with Amazon Cognito and fetch a refresh token and id token. """ - client = AwsIdp(AuthApi.COGNITO_POOL_ID, AuthApi.COGNITO_CLIENT_ID) - return client.authenticate(username, password) + idp_client = CognitoIdp(AuthApi.COGNITO_POOL_ID, AuthApi.COGNITO_CLIENT_ID) + return idp_client.authenticate(username, password) @staticmethod def _refresh(refresh_token): """ Use the refresh token to fetch a new id token. """ - client = AwsIdp(AuthApi.COGNITO_POOL_ID, AuthApi.COGNITO_CLIENT_ID) - return client.renew_token(refresh_token) + idp_client = CognitoIdp(AuthApi.COGNITO_POOL_ID, AuthApi.COGNITO_CLIENT_ID) + return idp_client.renew_token(refresh_token) + + def get_dataset(self, dataset): + """ Fetch the value from the specified dataset. """ + identity_client = CognitoIdentity(AuthApi.COGNITO_POOL_ID, AuthApi.COGNITO_IDENTITY_POOL_ID) + id_token = self.get_token() + identity_id = identity_client.get_id(id_token) + credentials = identity_client.get_credentials_for_identity(id_token, identity_id) + + sync_client = CognitoSync(AuthApi.COGNITO_IDENTITY_POOL_ID, identity_id, credentials) + data, session_token, sync_count = sync_client.list_records(dataset) + + sync_info = { + 'identity_id': identity_id, + 'credentials': credentials, + 'session_token': session_token, + 'sync_count': sync_count, + } + + return data, sync_info + + @staticmethod + def put_dataset(dataset, value, sync_info): + """ Store the value from the specified dataset. """ + sync_client = CognitoSync(AuthApi.COGNITO_IDENTITY_POOL_ID, sync_info.get('identity_id'), sync_info.get('credentials')) + sync_client.update_records(dataset, value, sync_info.get('session_token'), sync_info.get('sync_count')) diff --git a/resources/lib/viervijfzes/aws/__init__.py b/resources/lib/viervijfzes/aws/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/resources/lib/viervijfzes/aws/cognito_identity.py b/resources/lib/viervijfzes/aws/cognito_identity.py new file mode 100644 index 0000000..49fcb85 --- /dev/null +++ b/resources/lib/viervijfzes/aws/cognito_identity.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +""" Amazon Cognito Identity implementation without external dependencies """ + +from __future__ import absolute_import, division, unicode_literals + +import json +import logging + +import requests + +_LOGGER = logging.getLogger(__name__) + + +class CognitoIdentity: + """ Cognito Identity """ + + def __init__(self, pool_id, identity_pool_id): + """ + + See https://docs.aws.amazon.com/cognitoidentity/latest/APIReference/Welcome.html. + + :param str pool_id: + :param str identity_pool_id: + """ + self.pool_id = pool_id + if "_" not in self.pool_id: + raise ValueError("Invalid pool_id format. Should be _.") + + self.identity_pool_id = identity_pool_id + self.region = self.pool_id.split("_")[0] + self.url = "https://cognito-identity.%s.amazonaws.com/" % self.region + self._session = requests.session() + + def get_id(self, id_token): + """ Get the Identity ID based on the id_token. """ + provider = 'cognito-idp.%s.amazonaws.com/%s' % (self.region, self.pool_id) + data = { + "IdentityPoolId": self.identity_pool_id, + "Logins": { + provider: id_token, + } + } + response = self._session.post(self.url, json=data, headers={ + 'x-amz-target': 'AWSCognitoIdentityService.GetId', + 'content-type': 'application/x-amz-json-1.1', + }) + _LOGGER.debug(response.text) + + result = json.loads(response.text) + + return result.get('IdentityId') + + def get_credentials_for_identity(self, id_token, identity_id): + """ Get credentials based on the id_token and identity_id. """ + provider = 'cognito-idp.%s.amazonaws.com/%s' % (self.region, self.pool_id) + data = { + "IdentityId": identity_id, + "Logins": { + provider: id_token, + } + } + + response = self._session.post(self.url, json=data, headers={ + 'x-amz-target': 'AWSCognitoIdentityService.GetCredentialsForIdentity', + 'content-type': 'application/x-amz-json-1.1', + }) + _LOGGER.debug(response.text) + + result = json.loads(response.text) + + return result.get('Credentials') diff --git a/resources/lib/viervijfzes/auth_awsidp.py b/resources/lib/viervijfzes/aws/cognito_idp.py similarity index 97% rename from resources/lib/viervijfzes/auth_awsidp.py rename to resources/lib/viervijfzes/aws/cognito_idp.py index fc04f14..c9e87ce 100644 --- a/resources/lib/viervijfzes/auth_awsidp.py +++ b/resources/lib/viervijfzes/aws/cognito_idp.py @@ -27,11 +27,14 @@ class AuthenticationException(Exception): """ Something went wrong while logging in """ -class AwsIdp: - """ AWS Identity Provider """ +class CognitoIdp: + """ Cognito IDP """ def __init__(self, pool_id, client_id): """ + + See https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/Welcome.html. + :param str pool_id: The AWS user pool to connect to (format: _). E.g.: eu-west-1_aLkOfYN3T :param str client_id: The client application ID (the ID of the application connecting) @@ -291,7 +294,7 @@ class AwsIdp: @staticmethod def __hex_hash(hex_string): - return AwsIdp.__hash_sha256(bytearray.fromhex(hex_string)) + return CognitoIdp.__hash_sha256(bytearray.fromhex(hex_string)) @staticmethod def __hash_sha256(buf): @@ -311,7 +314,7 @@ class AwsIdp: # noinspection PyTypeChecker if not isinstance(long_int, six.string_types): - hash_str = AwsIdp.__long_to_hex(long_int) + hash_str = CognitoIdp.__long_to_hex(long_int) else: hash_str = long_int if len(hash_str) % 2 == 1: @@ -323,7 +326,7 @@ class AwsIdp: @staticmethod def __get_random(nbytes): random_hex = binascii.hexlify(os.urandom(nbytes)) - return AwsIdp.__hex_to_long(random_hex) + return CognitoIdp.__hex_to_long(random_hex) @staticmethod def __get_current_timestamp(): diff --git a/resources/lib/viervijfzes/aws/cognito_sync.py b/resources/lib/viervijfzes/aws/cognito_sync.py new file mode 100644 index 0000000..fadd6fd --- /dev/null +++ b/resources/lib/viervijfzes/aws/cognito_sync.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +""" Amazon Cognito Sync implementation without external dependencies """ + +from __future__ import absolute_import, division, unicode_literals + +import datetime +import hashlib +import hmac +import json +import logging + +import requests + +try: # Python 3 + from urllib.parse import quote, urlparse +except ImportError: # Python 2 + from urllib import quote + from urlparse import urlparse + +_LOGGER = logging.getLogger(__name__) + + +class CognitoSync: + """ Amazon Cognito Sync """ + + def __init__(self, identity_pool_id, identity_id, credentials): + """ + + See https://docs.aws.amazon.com/cognitosync/latest/APIReference/Welcome.html. + + :param str identity_pool_id: + :param str identity_id: + :param dict credentials: + """ + self.identity_pool_id = identity_pool_id + self.identity_id = identity_id + self.credentials = credentials + + self.region = self.identity_pool_id.split(":")[0] + self.url = "https://cognito-sync.%s.amazonaws.com" % self.region + self._session = requests.session() + + def _sign(self, request, service='cognito-sync'): + """ Sign the request. + + More info at https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html. + + :param requests.PreparedRequest request: A prepared request that should be signed. + :param str service: The service where this request is going to. + """ + + def sign(key, msg): + """ Sign this message. """ + return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest() + + def get_signature_key(key, date_stamp, region_name, service_name): + """ Generate a signature key. """ + k_date = sign(('AWS4' + key).encode('utf-8'), date_stamp) + k_region = sign(k_date, region_name) + k_service = sign(k_region, service_name) + k_signing = sign(k_service, 'aws4_request') + return k_signing + + # Parse the URL + url_parsed = urlparse(request.url) + + # Create a date for headers and the credential string + now = datetime.datetime.utcnow() + amzdate = now.strftime('%Y%m%dT%H%M%SZ') + datestamp = now.strftime('%Y%m%d') # Date w/o time, used in credential scope + + # Step 1. Create a canonical request + canonical_uri = quote(url_parsed.path) + canonical_querystring = url_parsed.query # TODO: sort when using multiple values + canonical_headers = ('host:' + url_parsed.netloc + '\n' + + 'x-amz-date:' + amzdate + '\n') + signed_headers = 'host;x-amz-date' + + if request.body: + payload_hash = hashlib.sha256(request.body).hexdigest() + else: + # SHA256 of empty string + payload_hash = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' + + canonical_request = (request.method + '\n' + + canonical_uri + '\n' + + canonical_querystring + '\n' + + canonical_headers + '\n' + + signed_headers + '\n' + + payload_hash) + + _LOGGER.warning(canonical_request) + + # Step 2. Create a string to sign + algorithm = 'AWS4-HMAC-SHA256' + credential_scope = '%s/%s/%s/%s' % (datestamp, self.region, service, 'aws4_request') + string_to_sign = (algorithm + '\n' + + amzdate + '\n' + + credential_scope + '\n' + + hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()) + signing_key = get_signature_key(self.credentials.get('SecretKey'), datestamp, self.region, service) + + # Step 3. Calculate the signature + signature = hmac.new(signing_key, string_to_sign.encode('utf-8'), hashlib.sha256).hexdigest() + + authorization_header = '%s Credential=%s/%s, SignedHeaders=%s, Signature=%s' % ( + algorithm, self.credentials.get('AccessKeyId'), credential_scope, signed_headers, signature) + + # Step 4. Add the signature to the request + request.headers.update({ + 'x-amz-date': amzdate, + 'Authorization': authorization_header + }) + + def list_records(self, dataset): + """ Return the values of this dataset. + + :param str dataset: The name of the dataset to request. + :return The requested dataset + :rtype: dict + """ + # Prepare the request + request = requests.Request( + method='GET', + params={ + 'maxResults': 1024, + }, + url=self.url + '/identitypools/{identity_pool_id}/identities/{identity_id}/datasets/{dataset}/records'.format( + identity_pool_id=self.identity_pool_id, + identity_id=self.identity_id, + dataset=dataset + ), + headers={ + 'x-amz-security-token': self.credentials.get('SessionToken'), + }).prepare() + + # Sign the request + self._sign(request) + + # Send the request + reply = self._session.send(request) + reply.raise_for_status() + result = json.loads(reply.text) + + # Return the records + record = next(record for record in result.get('Records', []) if record.get('Key') == dataset) + value = json.loads(record.get('Value')) + + return value, result.get('SyncSessionToken'), record.get('SyncCount') + + def update_records(self, dataset, value, session_token, sync_count): + """ Return the values of this dataset. + + :param str dataset: The name of the dataset to request. + :param any value: The value. + :param str session_token: The session token from the list_records call. + :param int sync_count: The last SyncCount value, so we refuse race conditions. + """ + # Prepare the request + request = requests.Request( + method='POST', + url=self.url + '/identitypools/{identity_pool_id}/identities/{identity_id}/datasets/{dataset}'.format( + identity_pool_id=self.identity_pool_id, + identity_id=self.identity_id, + dataset=dataset + ), + headers={ + 'x-amz-security-token': self.credentials.get('SessionToken'), + }, + json={ + "SyncSessionToken": session_token, + "RecordPatches": [ + { + "Key": dataset, + "Op": "replace", + "SyncCount": sync_count, + "Value": json.dumps(value), + } + ] + }).prepare() + + # Sign the request + self._sign(request) + + # Send the request + reply = self._session.send(request) + reply.raise_for_status() diff --git a/resources/lib/viervijfzes/content.py b/resources/lib/viervijfzes/content.py index 08f55d6..9d0c3c5 100644 --- a/resources/lib/viervijfzes/content.py +++ b/resources/lib/viervijfzes/content.py @@ -45,7 +45,7 @@ class Program: """ Defines a Program. """ def __init__(self, uuid=None, path=None, channel=None, title=None, description=None, aired=None, cover=None, background=None, seasons=None, episodes=None, - clips=None): + clips=None, my_list=False): """ :type uuid: str :type path: str @@ -58,6 +58,7 @@ class Program: :type seasons: list[Season] :type episodes: list[Episode] :type clips: list[Episode] + :type my_list: bool """ self.uuid = uuid self.path = path @@ -70,6 +71,7 @@ class Program: self.seasons = seasons self.episodes = episodes self.clips = clips + self.my_list = my_list def __repr__(self): return "%r" % self.__dict__ @@ -182,6 +184,7 @@ class ContentApi: def get_programs(self, channel=None, cache=CACHE_AUTO): """ Get a list of all programs of the specified channel. + :type channel: str :type cache: str :rtype list[Program] """ @@ -260,6 +263,31 @@ class ContentApi: return program + def get_program_by_uuid(self, uuid, cache=CACHE_AUTO): + """ Get a Program object with the specified uuid. + :type uuid: str + :type cache: str + :rtype Program + """ + if not uuid: + return None + + def update(): + """ Fetch the program metadata """ + # Fetch webpage + result = self._get_url(self.SITE_URL + '/api/program/%s' % uuid) + data = json.loads(result) + return data + + # Fetch listing from cache or update if needed + data = self._handle_cache(key=['program', uuid], cache_mode=cache, update=update) + if not data: + return None + + program = self._parse_program_data(data) + + return program + def get_episode(self, path, cache=CACHE_AUTO): """ Get a Episode object from the specified page. :type path: str diff --git a/resources/lib/viervijfzes/search.py b/resources/lib/viervijfzes/search.py index 48b62a3..4f1fd17 100644 --- a/resources/lib/viervijfzes/search.py +++ b/resources/lib/viervijfzes/search.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -""" AUTH API """ +""" Search API """ from __future__ import absolute_import, division, unicode_literals @@ -8,17 +8,19 @@ import logging import requests -from resources.lib.viervijfzes.content import Program +from resources.lib import kodiutils +from resources.lib.viervijfzes.content import Program, ContentApi, CACHE_ONLY _LOGGER = logging.getLogger(__name__) class SearchApi: """ GoPlay Search API """ - API_ENDPOINT = 'https://api.viervijfzes.be/search' + API_ENDPOINT = 'https://api.goplay.be/search' def __init__(self): """ Initialise object """ + self._api = ContentApi(None, cache_path=kodiutils.get_cache_path()) self._session = requests.session() def search(self, query): @@ -33,26 +35,28 @@ class SearchApi: self.API_ENDPOINT, json={ "query": query, - "sites": ["vier", "vijf", "zes"], "page": 0, - "mode": "byDate" + "mode": "programs" } ) - - if response.status_code != 200: - raise Exception('Could not search') + _LOGGER.debug(response.content) + response.raise_for_status() data = json.loads(response.text) results = [] for hit in data['hits']['hits']: if hit['_source']['bundle'] == 'program': - results.append(Program( - channel=hit['_source']['site'], - path=hit['_source']['url'].split('/')[-1], - title=hit['_source']['title'], - description=hit['_source']['intro'], - cover=hit['_source']['img'], - )) + path = hit['_source']['url'].split('/')[-1] + program = self._api.get_program(path, cache=CACHE_ONLY) + if program: + results.append(program) + else: + results.append(Program( + path=path, + title=hit['_source']['title'], + description=hit['_source']['intro'], + cover=hit['_source']['img'], + )) return results diff --git a/tests/test_auth.py b/tests/test_auth.py index 9978f53..0ffa043 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -26,12 +26,12 @@ class TestAuth(unittest.TestCase): auth.clear_tokens() # We should get a token by logging in - token = auth.get_token() - self.assertTrue(token) + id_token = auth.get_token() + self.assertTrue(id_token) # Test it a second time, it should go from memory now - token = auth.get_token() - self.assertTrue(token) + id_token = auth.get_token() + self.assertTrue(id_token) if __name__ == '__main__': diff --git a/tests/test_mylist.py b/tests/test_mylist.py new file mode 100644 index 0000000..d5f891e --- /dev/null +++ b/tests/test_mylist.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +""" Tests for My List """ + +# pylint: disable=missing-docstring,no-self-use + +from __future__ import absolute_import, division, print_function, unicode_literals + +import logging +import unittest + +from resources.lib import kodiutils +from resources.lib.viervijfzes.auth import AuthApi + +_LOGGER = logging.getLogger(__name__) + + +class TestMyList(unittest.TestCase): + def __init__(self, *args, **kwargs): + super(TestMyList, self).__init__(*args, **kwargs) + + @unittest.skipUnless(kodiutils.get_setting('username') and kodiutils.get_setting('password'), 'Skipping since we have no credentials.') + def test_mylist(self): + auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) + id_token = auth.get_token() + self.assertTrue(id_token) + + dataset, _ = auth.get_dataset('myList') + self.assertTrue(dataset) + + # Test disabled since it would cause locks due to all the CI tests changing this at the same time. + + # # Python 2.7 doesn't support .timestamp(), and windows doesn't do '%s', so we need to calculate it ourself + # epoch = datetime(1970, 1, 1, tzinfo=dateutil.tz.gettz('UTC')) + # now = datetime.now(tz=dateutil.tz.gettz('UTC')) + # timestamp = str(int((now - epoch).total_seconds())) + '000' + # new_dataset = [ + # {'id': '06e209f9-092e-421e-9499-58c62c292b98', 'timestamp': timestamp}, + # {'id': 'da584be3-dea6-49c7-bfbd-c480d8096937', 'timestamp': timestamp} + # ] + # + # auth.put_dataset('myList', new_dataset, sync_info) + + +if __name__ == '__main__': + unittest.main()