class API(object):
    def new_session(self):
        self.logged_in = False
        self._session = Session(HEADERS, timeout=30)
        self._set_authentication(userdata.get('access_token'))
        self._set_languages()

    @mem_cache.cached(60 * 60, key='config')
    def get_config(self):
        return self._session.get(CONFIG_URL).json()

    def _set_languages(self):
        self._app_language = 'en'
        self._playback_language = 'en'
        self._subtitle_language = 'en'
        self._kids_mode = False
        self._maturity_rating = 9999
        self._region = None

        if not self.logged_in:
            return

        token = userdata.get('access_token')
        if '..' in token:  #JWE Token
            return

        data = jwt_data(token)['context']

        #   self._maturity_rating = data['preferred_maturity_rating']['implied_maturity_rating']
        #   self._region = data['location']['country_code']

        for profile in data['profiles']:
            if profile['id'] == data['active_profile_id']:
                self._app_language = profile['language_preferences'][
                    'app_language']
                self._playback_language = profile['language_preferences'][
                    'playback_language']
                self._subtitle_language = profile['language_preferences'][
                    'subtitle_language']
                self._kids_mode = profile['kids_mode_enabled']
                return

    @mem_cache.cached(60 * 60, key='transaction_id')
    def _transaction_id(self):
        return str(uuid.uuid4())

    @property
    def session(self):
        return self._session

    def _set_authentication(self, access_token):
        if not access_token:
            return

        ## JWT requires Bearer
        if '..' not in access_token:
            access_token = 'Bearer {}'.format(access_token)

        self._session.headers.update({'Authorization': access_token})
        self._session.headers.update(
            {'x-bamsdk-transaction-id': self._transaction_id()})
        self.logged_in = True

    def _refresh_token(self, force=False):
        if not force and userdata.get('expires', 0) > time():
            return

        payload = {
            'refresh_token': userdata.get('refresh_token'),
            'grant_type': 'refresh_token',
            'platform': 'android',
        }

        self._oauth_token(payload)

    def _oauth_token(self, payload):
        headers = {
            'Authorization': 'Bearer {}'.format(API_KEY),
        }

        endpoint = self.get_config(
        )['services']['token']['client']['endpoints']['exchange']['href']
        token_data = self._session.post(endpoint,
                                        data=payload,
                                        headers=headers).json()

        self._check_errors(token_data)

        self._set_authentication(token_data['access_token'])

        userdata.set('access_token', token_data['access_token'])
        userdata.set('expires', int(time() + token_data['expires_in'] - 15))

        if 'refresh_token' in token_data:
            userdata.set('refresh_token', token_data['refresh_token'])

    def login(self, username, password):
        self.logout()

        try:
            self._do_login(username, password)
        except:
            self.logout()
            raise

    def _check_errors(self, data, error=_.API_ERROR):
        if not type(data) is dict:
            return

        if data.get('errors'):
            error_msg = ERROR_MAP.get(
                data['errors'][0].get('code')) or data['errors'][0].get(
                    'description') or data['errors'][0].get('code')
            raise APIError(_(error, msg=error_msg))

        elif data.get('error'):
            error_msg = ERROR_MAP.get(data.get('error_code')) or data.get(
                'error_description') or data.get('error_code')
            raise APIError(_(error, msg=error_msg))

        elif data.get('status') == 400:
            raise APIError(_(error, msg=data.get('message')))

    def _do_login(self, username, password):
        headers = {
            'Authorization': 'Bearer {}'.format(API_KEY),
        }

        payload = {
            'deviceFamily': 'android',
            'applicationRuntime': 'android',
            'deviceProfile': 'tv',
            'attributes': {},
        }

        endpoint = self.get_config()['services']['device']['client'][
            'endpoints']['createDeviceGrant']['href']
        device_data = self._session.post(endpoint,
                                         json=payload,
                                         headers=headers,
                                         timeout=20).json()

        self._check_errors(device_data)

        payload = {
            'subject_token': device_data['assertion'],
            'subject_token_type': 'urn:bamtech:params:oauth:token-type:device',
            'platform': 'android',
            'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange',
        }

        self._oauth_token(payload)

        payload = {
            'email': username,
            'password': password,
        }

        endpoint = self.get_config()['services']['bamIdentity']['client'][
            'endpoints']['identityLogin']['href']
        login_data = self._session.post(endpoint, json=payload).json()

        self._check_errors(login_data)

        endpoint = self.get_config()['services']['account']['client'][
            'endpoints']['createAccountGrant']['href']
        grant_data = self._session.post(endpoint,
                                        json={
                                            'id_token': login_data['id_token']
                                        }).json()

        payload = {
            'subject_token': grant_data['assertion'],
            'subject_token_type':
            'urn:bamtech:params:oauth:token-type:account',
            'platform': 'android',
            'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange',
        }

        self._oauth_token(payload)

    def _json_call(self, endpoint, variables=None):
        self._refresh_token()
        params = {'variables': json.dumps(variables)} if variables else None
        data = self._session.get(endpoint, params=params).json()
        self._check_errors(data)
        return data

    def profiles(self):
        self._refresh_token(force=True)
        endpoint = self.get_config()['services']['account']['client'][
            'endpoints']['getUserProfiles']['href']
        return self._json_call(endpoint)

    def active_profile(self):
        endpoint = self.get_config()['services']['account']['client'][
            'endpoints']['getActiveUserProfile']['href']
        return self._json_call(endpoint)

    def set_profile(self, profile, pin=None):
        self._refresh_token()

        endpoint = self.get_config()['services']['account']['client'][
            'endpoints']['setActiveUserProfile']['href'].format(
                profileId=profile['profileId'])

        payload = {}
        if pin:
            payload['entryPin'] = str(pin)

        grant_data = self._session.put(endpoint, json=payload).json()
        self._check_errors(grant_data)

        payload = {
            'subject_token': grant_data['assertion'],
            'subject_token_type':
            'urn:bamtech:params:oauth:token-type:account',
            'platform': 'android',
            'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange',
        }

        self._oauth_token(payload)

        userdata.set(
            'profile_language',
            profile['attributes']['languagePreferences']['appLanguage'])

    def search(self, query, page=1, page_size=PAGE_SIZE):
        variables = {
            'preferredLanguage': [self._app_language],
            'index': 'disney_global',
            'q': query,
            'page': page,
            'pageSize': page_size,
            'contentTransactionId': self._transaction_id(),
        }

        endpoint = self.get_config()['services']['content']['client'][
            'endpoints']['searchPersisted']['href'].format(
                queryId='core/disneysearch')
        return self._json_call(endpoint, variables)['data']['disneysearch']

    def avatar_by_id(self, ids):
        variables = {
            'preferredLanguage': [self._app_language],
            'avatarId': ids,
        }

        endpoint = self.get_config()['services']['content']['client'][
            'endpoints']['searchPersisted']['href'].format(
                queryId='core/AvatarByAvatarId')
        return self._json_call(endpoint, variables)['data']['AvatarByAvatarId']

    def video_bundle(self, family_id):
        variables = {
            'preferredLanguage': [self._app_language],
            'familyId': family_id,
            'contentTransactionId': self._transaction_id(),
        }

        endpoint = self.get_config()['services']['content']['client'][
            'endpoints']['dmcVideos']['href'].format(
                queryId='core/DmcVideoBundle')
        return self._json_call(endpoint, variables)['data']['DmcVideoBundle']

    def extras(self, family_id):
        variables = {
            'preferredLanguage': [self._app_language],
            'familyId': family_id,
            'page': 1,
            'pageSize': 999,
            'contentTransactionId': self._transaction_id(),
        }

        endpoint = self.get_config()['services']['content']['client'][
            'endpoints']['dmcVideos']['href'].format(queryId='core/DmcExtras')
        return self._json_call(endpoint, variables)['data']['DmcExtras']

    def series_bundle(self, series_id, page=1, page_size=PAGE_SIZE):
        variables = {
            'preferredLanguage': [self._app_language],
            'seriesId': series_id,
            'episodePage': page,
            'episodePageSize': page_size,
            'contentTransactionId': self._transaction_id(),
        }

        endpoint = self.get_config()['services']['content']['client'][
            'endpoints']['dmcVideos']['href'].format(
                queryId='core/DmcSeriesBundle')
        return self._json_call(endpoint, variables)['data']['DmcSeriesBundle']

    def episodes(self, season_ids, page=1, page_size=PAGE_SIZE_EPISODES):
        variables = {
            'preferredLanguage': [self._app_language],
            'seasonId': season_ids,
            'episodePage': page,
            'episodePageSize': page_size,
            'contentTransactionId': self._transaction_id(),
        }

        endpoint = self.get_config()['services']['content']['client'][
            'endpoints']['dmcVideos']['href'].format(
                queryId='core/DmcEpisodes')
        return self._json_call(endpoint, variables)['data']['DmcEpisodes']

    def collection_by_slug(self, slug, content_class):
        variables = {
            'preferredLanguage': [self._app_language],
            'contentClass': content_class,
            'slug': slug,
            'contentTransactionId': self._transaction_id(),
        }

        #endpoint = self.get_config()['services']['content']['client']['endpoints']['dmcVideos']['href'].format(queryId='disney/CollectionBySlug')
        endpoint = self.get_config()['services']['content']['client'][
            'endpoints']['dmcVideos']['href'].format(
                queryId='core/CompleteCollectionBySlug')
        return self._json_call(endpoint,
                               variables)['data']['CompleteCollectionBySlug']

    def set_by_id(self, set_id, set_type, page=1, page_size=PAGE_SIZE):
        variables = {
            'preferredLanguage': [self._app_language],
            'setId': set_id,
            'setType': set_type,
            'page': page,
            'pageSize': page_size,
            'contentTransactionId': self._transaction_id(),
        }

        #endpoint = self.get_config()['services']['content']['client']['endpoints']['dmcVideos']['href'].format(queryId='disney/SetBySetId')
        endpoint = self.get_config()['services']['content']['client'][
            'endpoints']['dmcVideos']['href'].format(queryId='core/SetBySetId')
        return self._json_call(endpoint, variables)['data']['SetBySetId']

    def add_watchlist(self, content_id):
        variables = {
            'preferredLanguage': [self._app_language],
            'contentIds': content_id,
        }

        endpoint = self.get_config()['services']['content']['client'][
            'endpoints']['dmcVideos']['href'].format(
                queryId='core/AddToWatchlist')
        return self._json_call(endpoint, variables)['data']['AddToWatchlist']

    def delete_watchlist(self, content_id):
        variables = {
            'preferredLanguage': [self._app_language],
            'contentIds': content_id,
        }

        endpoint = self.get_config()['services']['content']['client'][
            'endpoints']['dmcVideos']['href'].format(
                queryId='core/DeleteFromWatchlist')
        data = self._json_call(endpoint,
                               variables)['data']['DeleteFromWatchlist']
        xbmc.sleep(500)
        return data

    def up_next(self, content_id):
        variables = {
            'preferredLanguage': [self._app_language],
            'contentId': content_id,
            'contentTransactionId': self._transaction_id(),
        }

        endpoint = self.get_config()['services']['content']['client'][
            'endpoints']['dmcVideos']['href'].format(queryId='core/UpNext')
        return self._json_call(endpoint, variables)['data']['UpNext']

    def videos(self, content_id):
        variables = {
            'preferredLanguage': [self._app_language],
            'contentId': content_id,
            'contentTransactionId': self._transaction_id(),
        }

        endpoint = self.get_config()['services']['content']['client'][
            'endpoints']['dmcVideos']['href'].format(queryId='core/DmcVideos')
        return self._json_call(endpoint, variables)['data']['DmcVideos']

    def update_resume(self, media_id, fguid, playback_time):
        payload = [{
            "server": {
                "fguid": fguid,
                "mediaId": media_id,
            },
            "client": {
                "event": "urn:dss:telemetry-service:event:stream-sample",
                "timestamp": str(int(time() * 1000)),
                "play_head": playback_time,
                # "playback_session_id": str(uuid.uuid4()),
                # "interaction_id": str(uuid.uuid4()),
                # "bitrate": 4206,
            },
        }]

        self._refresh_token()
        endpoint = self.get_config(
        )['services']['telemetry']['client']['endpoints']['postEvent']['href']
        return self._session.post(endpoint, json=payload).status_code

    def playback_data(self, playback_url):
        self._refresh_token(force=True)

        config = self.get_config()
        scenario = config['services']['media']['extras'][
            'restrictedPlaybackScenario']

        if settings.getBool('wv_secure', False):
            scenario = config['services']['media']['extras'][
                'playbackScenarioDefault']

            if settings.getBool('h265', False):
                scenario += '-h265'

                if settings.getBool('dolby_vision', False):
                    scenario += '-dovi'
                elif settings.getBool('hdr10', False):
                    scenario += '-hdr10'

                if settings.getBool('dolby_atmos', False):
                    scenario += '-atmos'

        headers = {
            'accept': 'application/vnd.media-service+json; version=4',
            'authorization': userdata.get('access_token')
        }

        endpoint = playback_url.format(scenario=scenario)
        playback_data = self._session.get(endpoint, headers=headers).json()
        self._check_errors(playback_data)

        return playback_data

    def continue_watching(self):
        set_id = CONTINUE_WATCHING_SET_ID
        set_type = CONTINUE_WATCHING_SET_TYPE
        data = self.set_by_id(set_id, set_type, page_size=999)

        continue_watching = {}
        for row in data['items']:
            if row['meta']['bookmarkData']:
                play_from = row['meta']['bookmarkData']['playhead']
            else:
                play_from = 0

            continue_watching[row['contentId']] = play_from

        return continue_watching

    def logout(self):
        userdata.delete('access_token')
        userdata.delete('expires')
        userdata.delete('refresh_token')

        mem_cache.delete('transaction_id')
        mem_cache.delete('config')

        self.new_session()
Example #2
0
class API(object):
    def new_session(self):
        self.logged_in = False

        self._session = Session(HEADERS, base_url=API_BASE)
        self._set_authentication()

    def _set_authentication(self):
        token = userdata.get('token')
        if not token:
            return

        self._session.headers.update(
            {'Authorization': 'Bearer {}'.format(token)})
        self.logged_in = True

    @contextmanager
    def api_call(self):
        if self.logged_in:
            self.refresh_token()

        try:
            yield
        except Exception as e:
            log.exception(e)
            raise APIError(_.NO_DATA)

    def refresh_token(self):
        if not self.logged_in or time.time() < userdata.get('expires', 0):
            return

        data = self._session.put('/oam/v2/user/tokens').json()

        if 'errorMessage' in data:
            self.logout()
            raise APIError(_(_.TOKEN_ERROR, msg=data['errorMessage']))

        self._set_token(data['sessionToken'])

    def _set_token(self, token):
        data = jwt_data(token)
        expires = min(int(time.time() + 86400), data['exp'] - 10)

        userdata.set('expires', expires)
        userdata.set('token', token)

        self._set_authentication()

    def login(self, username, password):
        self.logout()

        deviceid = str(uuid.uuid3(uuid.UUID(UUID_NAMESPACE), str(username)))

        payload = {
            "username": username,
            "password": password,
            "deviceID": deviceid,
        }

        headers = {'Authorization': 'Bearer {}'.format(DEFAULT_TOKEN)}

        with self.api_call():
            data = self._session.post('/oam/v2/user/tokens',
                                      json=payload,
                                      headers=headers).json()

        if 'errorMessage' in data:
            raise APIError(_(_.LOGIN_ERROR, msg=data['errorMessage']))

        userdata.set('deviceid', deviceid)

        self._set_token(data['sessionToken'])

    def whats_on(self, query=''):
        now = arrow.utcnow()
        later = now.shift(days=21)

        params = {
            'count': 100,
            'offset': 0,
            'language': '*',
            'query': query,
            'sort': 'startTime',
            'sortOrder': 'asc',
            'startTime.lte': later.format('YYYY-MM-DDTHH:mm:ss.000') + 'Z',
            'endTime.gte': now.format('YYYY-MM-DDTHH:mm:ss.000') + 'Z',
            'types': 'live/competitions,live/teamCompetitions,live/events',
        }

        with self.api_call():
            return self._session.get('/ocm/v2/search',
                                     params=params).json()['results']

    def search(self, query):
        params = {
            'count': 100,
            'offset': 0,
            'language': '*',
            'query': query,
            'sort': 'liveEventDate',
            'sortOrder': 'desc',
            'searchMethods': 'prefix,fuzzy',
            'types': 'vod/competitions,vod/teamCompetitions,vod/events',
        }

        with self.api_call():
            return self._session.get('/ocm/v2/search',
                                     params=params).json()['results']

    def sparksport(self):
        with self.api_call():
            return self._session.get(
                'https://d2rhrqdzx7i00p.cloudfront.net/sparksport2').json()

    def page(self, page_id):
        with self.api_call():
            return self._session.get('/ocm/v4/pages/{}'.format(page_id)).json()

    @cached(expires=60 * 10)
    def section(self, section_id):
        with self.api_call():
            return self._session.get(
                '/ocm/v4/sections/{}'.format(section_id)).json()

    def live_channels(self):
        with self.api_call():
            return self._session.get(
                '/ocm/v2/epg/stations').json()['epg/stations']

    def entitiy(self, entity_id):
        with self.api_call():
            data = self._session.get(
                '/ocm/v2/entities/{}'.format(entity_id)).json()

        for key in data:
            try:
                if data[key][0]['id'] == entity_id:
                    return data[key][0]
            except (TypeError, KeyError):
                continue

        return None

    def play(self, entity_id):
        entity = self.entitiy(entity_id)
        if not entity or not entity.get('assetIDs'):
            raise APIError(_.NO_ASSET_ERROR)

        with self.api_call():
            assets = self._session.get('/ocm/v2/assets/{}'.format(
                entity['assetIDs'][0])).json()['assets']

        mpd_url = None
        for asset in assets:
            try:
                urls = asset['liveURLs'] or asset['vodURLs']
                mpd_url = urls['dash']['primary']
                backup = urls['dash'].get('backup')
                if 'dai.google.com' in mpd_url and backup and 'dai.google.com' not in backup:
                    mpd_url = backup
            except (TypeError, KeyError):
                continue
            else:
                break

        if not mpd_url:
            raise APIError(_.NO_MPD_ERROR)

        # Hack until Spark fix their bad second base-url
        # if '/startover/' in mpd_url:
        #     mpd_url = mpd_url.split('/')
        #     mpd_url = "/".join(mpd_url[:-3]) + '/master.mpd'
        #

        payload = {
            'assetID': entity_id,
            'playbackUrl': mpd_url,
            'deviceID': userdata.get('deviceid'),
        }

        data = self._session.post('/oem/v2/entitlement?tokentype=isp-atlas',
                                  json=payload).json()
        token = data.get('entitlementToken')

        if not token:
            raise APIError(_(_.NO_ENTITLEMENT, error=data.get('errorMessage')))

        params = {
            'progress': 0,
            'device': userdata.get('deviceid'),
        }

        self._session.put('/oxm/v1/streams/{}/stopped'.format(entity_id),
                          params=params)

        headers = {'X-ISP-TOKEN': token}

        from_start = True
        if entity.get('customAttributes',
                      {}).get('isLinearChannelInLiveEvent') == 'true':
            from_start = False

        return mpd_url, WV_LICENSE_URL, headers, from_start

    def logout(self):
        userdata.delete('token')
        userdata.delete('deviceid')
        userdata.delete('expires')
        self.new_session()