Exemple #1
0
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

        data = jwt_data(userdata.get('access_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

        self._session.headers.update(
            {'Authorization': 'Bearer {}'.format(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': 'browser',
        }

        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 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))

    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 profiles(self):
        self._refresh_token()

        endpoint = self.get_config()['services']['account']['client'][
            'endpoints']['getUserProfiles']['href']
        return self._session.get(endpoint).json()

    def add_profile(self, name, kids=False, avatar=None):
        payload = {
            'attributes': {
                'kidsModeEnabled': bool(kids),
                'languagePreferences': {
                    'appLanguage': self._app_language,
                    'playbackLanguage': self._playback_language,
                    'subtitleLanguage': self._subtitle_language,
                },
                'playbackSettings': {
                    'autoplay': True,
                },
            },
            'metadata': None,
            'profileName': name,
        }

        if avatar:
            payload['attributes']['avatar'] = {
                'id': avatar,
                'userSelected': False,
            }

        endpoint = self.get_config()['services']['account']['client'][
            'endpoints']['createUserProfile']['href']
        return self._session.post(endpoint, json=payload).json()

    def delete_profile(self, profile):
        endpoint = self.get_config()['services']['account']['client'][
            'endpoints']['deleteUserProfile']['href'].format(
                profileId=profile['profileId'])
        return self._session.delete(endpoint)

    def active_profile(self):
        self._refresh_token()

        endpoint = self.get_config()['services']['account']['client'][
            'endpoints']['getActiveUserProfile']['href']
        return self._session.get(endpoint).json()

    def update_profile(self, profile):
        self._refresh_token()

        endpoint = self.get_config()['services']['account']['client'][
            'endpoints']['updateUserProfile']['href'].format(
                profileId=profile['profileId'])
        if self._session.patch(endpoint, json=profile).ok:
            self._refresh_token(force=True)
            return True
        else:
            return False

    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):
        self._refresh_token()

        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')

        data = self._session.get(endpoint,
                                 params={
                                     'variables': json.dumps(variables)
                                 }).json()
        self._check_errors(data)

        return data['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._session.get(endpoint,
                                 params={
                                     'variables': json.dumps(variables)
                                 }).json()['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._session.get(endpoint,
                                 params={
                                     'variables': json.dumps(variables)
                                 }).json()['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._session.get(endpoint,
                                 params={
                                     'variables': json.dumps(variables)
                                 }).json()['data']['DmcExtras']

    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 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._session.get(endpoint,
                                 params={
                                     'variables': json.dumps(variables)
                                 }).json()['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._session.get(endpoint,
                                 params={
                                     'variables': json.dumps(variables)
                                 }).json()['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._session.get(endpoint,
                                 params={
                                     'variables': json.dumps(variables)
                                 }).json()['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._session.get(endpoint,
                                 params={
                                     'variables': json.dumps(variables)
                                 }).json()['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._session.get(endpoint,
                                 params={
                                     'variables': json.dumps(variables)
                                 }).json()['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._session.get(endpoint,
                                 params={
                                     'variables': json.dumps(variables)
                                 }).json()['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._session.get(endpoint,
                                 params={
                                     'variables': json.dumps(variables)
                                 }).json()['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._session.get(endpoint,
                                 params={
                                     'variables': json.dumps(variables)
                                 }).json()['data']['DmcVideos']

    def update_resume(self, media_id, fguid, playback_time):
        self._refresh_token()

        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,
            },
        }]

        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 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()
Exemple #2
0
class API(object):
    def new_session(self):
        self.logged_in = False
        self._auth_header = {}

        self._session = Session(HEADERS)
        self._set_authentication()

    @mem_cache.cached(60 * 10)
    def _config(self):
        return self._session.get(CONFIG_URL).json()

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

        self._auth_header = {'authorization': 'Bearer {}'.format(access_token)}
        self.logged_in = True

    def _oauth_token(self, data, _raise=True):
        token_data = self._session.post(
            'https://auth.streamotion.com.au/oauth/token',
            json=data,
            headers={
                'User-Agent': 'okhttp/3.10.0'
            },
            error_msg=_.TOKEN_ERROR).json()

        if 'error' in token_data:
            error = _.REFRESH_TOKEN_ERROR if data.get(
                'grant_type') == 'refresh_token' else _.LOGIN_ERROR
            if _raise:
                raise APIError(
                    _(error, msg=token_data.get('error_description')))
            else:
                return False, token_data

        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'])

        self._set_authentication()
        return True, token_data

    def refresh_token(self):
        self._refresh_token()

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

        log.debug('Refreshing token')

        payload = {
            'client_id':
            CLIENT_ID,
            'refresh_token':
            userdata.get('refresh_token'),
            'grant_type':
            'refresh_token',
            'scope':
            'openid offline_access drm:{} email'.format(
                'high' if settings.getBool('wv_secure', False) else 'low'),
        }

        self._oauth_token(payload)

    def device_code(self):
        payload = {
            'client_id':
            CLIENT_ID,
            'audience':
            'streamotion.com.au',
            'scope':
            'openid offline_access drm:{} email'.format(
                'high' if settings.getBool('wv_secure', False) else 'low'),
        }

        return self._session.post(
            'https://auth.streamotion.com.au/oauth/device/code',
            data=payload).json()

    def device_login(self, device_code):
        payload = {
            'client_id':
            CLIENT_ID,
            'device_code':
            device_code,
            'scope':
            'openid offline_access drm:{}'.format(
                'high' if settings.getBool('wv_secure', False) else 'low'),
            'grant_type':
            'urn:ietf:params:oauth:grant-type:device_code',
        }

        result, token_data = self._oauth_token(payload, _raise=False)
        if result:
            self._refresh_token(force=True)
            return True

        if token_data.get('error') != 'authorization_pending':
            raise APIError(
                _(_.LOGIN_ERROR, msg=token_data.get('error_description')))
        else:
            return False

    def login(self, username, password):
        payload = {
            'client_id':
            CLIENT_ID,
            'username':
            username,
            'password':
            password,
            'audience':
            'streamotion.com.au',
            'scope':
            'openid offline_access drm:{} email'.format(
                'high' if settings.getBool('wv_secure', False) else 'low'),
            'grant_type':
            'http://auth0.com/oauth/grant-type/password-realm',
            'realm':
            'prod-martian-database',
        }

        self._oauth_token(payload)
        self._refresh_token(force=True)

    def profiles(self):
        self._refresh_token()
        return self._session.get('{host}/user/profile'.format(
            host=self._config()['endPoints']['profileAPI']),
                                 headers=self._auth_header).json()

    def add_profile(self, name, avatar_id):
        self._refresh_token()

        payload = {
            'name': name,
            'avatar_id': avatar_id,
            'onboarding_status': 'welcomeScreen',
        }

        return self._session.post('{host}/user/profile'.format(
            host=self._config()['endPoints']['profileAPI']),
                                  json=payload,
                                  headers=self._auth_header).json()

    def delete_profile(self, profile):
        self._refresh_token()

        return self._session.delete('{host}/user/profile/{profile_id}'.format(
            host=self._config()['endPoints']['profileAPI'],
            profile_id=profile['id']),
                                    headers=self._auth_header)

    def profile_avatars(self):
        return self._session.get(
            '{host}/production/avatars/avatars.json'.format(
                host=self._config()['endPoints']['resourcesAPI'])).json()

    def sport_menu(self):
        return self._session.get(
            '{host}/production/sport-menu/lists/default.json'.format(
                host=self._config()['endPoints']['resourcesAPI'])).json()

    def use_cdn(self, live=False):
        return self._session.get('{host}/web/usecdn/unknown/{media}'.format(
            host=self._config()['endPoints']['cdnSelectionServiceAPI'],
            media='LIVE' if live else 'VOD'),
                                 headers=self._auth_header).json()

    #landing has heros and panels
    def landing(self, name, sport=None):
        params = {
            'evaluate': 3,
            'profile': userdata.get('profile_id'),
        }

        if sport:
            params['sport'] = sport

        return self._session.get(
            '{host}/content/types/landing/names/{name}'.format(
                host=self._config()['endPoints']['contentAPI'], name=name),
            params=params,
            headers=self._auth_header).json()

    #panel has shows and episodes
    def panel(self, id, sport=None):
        params = {
            'evaluate': 3,
            'profile': userdata.get('profile_id'),
        }

        if sport:
            params['sport'] = sport

        return self._session.get(
            '{host}/content/types/carousel/keys/{id}'.format(
                host=self._config()['endPoints']['contentAPI'], id=id),
            params=params,
            headers=self._auth_header).json()[0]

    #show has episodes and panels
    def show(self, show_id, season_id=None):
        params = {
            'evaluate': 3,
            'showCategory': show_id,
            'seasonCategory': season_id,
            'profile': userdata.get('profile_id'),
        }

        return self._session.get(
            '{host}/content/types/landing/names/show'.format(
                host=self._config()['endPoints']['contentAPI']),
            params=params,
            headers=self._auth_header).json()

    def search(self, query, page=1, size=250):
        params = {
            'q': query,
            'size': size,
            'page': page,
        }

        return self._session.get('{host}/v2/search'.format(
            host=self._config()['endPoints']['contentAPI']),
                                 params=params).json()

    def event(self, id):
        params = {
            'evaluate': 3,
            'event': id,
        }

        return self._session.get(
            '{host}/content/types/landing/names/event'.format(
                host=self._config()['endPoints']['contentAPI']),
            params=params).json()[0]['contents'][0]['data']['asset']

    def stream(self, asset_id):
        self._refresh_token()

        params = {
            'fields':
            'alternativeStreams,assetType,markers,metadata.isStreaming',
        }

        data = self._session.post('{host}/api/v1/asset/{asset_id}/play'.format(
            host=self._config()['endPoints']['vimondPlayAPI'],
            asset_id=asset_id),
                                  params=params,
                                  json={},
                                  headers=self._auth_header).json()
        if ('status' in data and data['status'] != 200) or 'errors' in data:
            msg = data.get('detail') or data.get('errors',
                                                 [{}])[0].get('detail')
            raise APIError(_(_.ASSET_ERROR, msg=msg))

        return data['data'][0]

    def logout(self):
        userdata.delete('access_token')
        userdata.delete('refresh_token')
        userdata.delete('expires')
        self.new_session()
Exemple #3
0
class API(object):
    def new_session(self):
        self.logged_in = False
        self._session = Session(HEADERS, base_url=API_URL)
        self._set_authentication()

    def _set_authentication(self):
        self.logged_in = userdata.get('token') != None

    def nav_items(self, key):
        data = self.page('sitemap')

        for row in data['navs']['browse']:
            if row['path'] == '/' + key:
                return row['items']

        return []

    def page(self, key):
        return self.url('/pages/v6/{}.json'.format(key))

    def url(self, url):
        self._check_token()

        params = {
            'feedTypes': 'posters,landscapes,hero',
            'jwToken': userdata.get('token'),
        }

        return self._session.get(url, params=params).json()

    def search(self, query, page=1, limit=50):
        self._check_token()

        params = {
            'q': query,
            'limit': limit,
            'offset': (page - 1) * limit,
            'jwToken': userdata.get('token'),
        }

        if userdata.get('profile_kids', False):
            url = '/search/v12/kids/search'
        else:
            url = '/search/v12/search'

        return self._session.get(url, params=params).json()

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

        payload = {
            'email': username,
            'password': password,
            'rnd': str(int(time.time())),
            'stanName': 'Stan-Android',
            'type': 'mobile',
            'os': 'Android',
            'stanVersion': STAN_VERSION,
            #   'clientId': '',
            # 'model': '',
            #  'sdk': '',
            # 'manufacturer': '',
        }

        payload['sign'] = self._get_sign(payload)

        self._login('/login/v1/sessions/mobile/account', payload)

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

        params = {
            'type': 'mobile',
            'os': 'Android',
            'stanVersion': STAN_VERSION,
        }

        payload = {
            'jwToken': userdata.get('token'),
        }

        self._login('/login/v1/sessions/mobile/app', payload, params)

    def _login(self, url, payload, params=None):
        data = self._session.post(url, data=payload, params=params).json()

        if 'errors' in data:
            try:
                msg = data['errors'][0]['code']
                if msg == 'Streamco.Login.VPNDetected':
                    msg = _.IP_ADDRESS_ERROR
            except:
                msg = ''

            raise APIError(_(_.LOGIN_ERROR, msg=msg))

        userdata.set('token', data['jwToken'])
        userdata.set('expires',
                     int(time.time() + (data['renew'] - data['now']) - 30))
        userdata.set('user_id', data['userId'])

        userdata.set('profile_id', data['profile']['id'])
        userdata.set('profile_name', data['profile']['name'])
        userdata.set('profile_icon', data['profile']['iconImage']['url'])
        userdata.set('profile_kids',
                     int(data['profile'].get('isKidsProfile', False)))

        self._set_authentication()

        try:
            log.debug('Token Data: {}'.format(
                json.dumps(jwt_data(userdata.get('token')))))
        except:
            pass

    def watchlist(self):
        self._check_token()

        params = {
            'jwToken': userdata.get('token'),
        }

        url = '/watchlist/v1/users/{user_id}/profiles/{profile_id}/watchlistitems'.format(
            user_id=userdata.get('user_id'),
            profile_id=userdata.get('profile_id'))
        return self._session.get(url, params=params).json()

    def history(self, program_ids=None):
        self._check_token()

        params = {
            'jwToken': userdata.get('token'),
            'limit': 100,
        }

        if program_ids:
            params['programIds'] = program_ids

        url = '/history/v1/users/{user_id}/profiles/{profile_id}/history'.format(
            user_id=userdata.get('user_id'),
            profile_id=userdata.get('profile_id'))
        return self._session.get(url, params=params).json()

    # def resume_series(self, series_id):
    #     params = {
    #         'jwToken': userdata.get('token'),
    #     }

    #     url = '/resume/v1/users/{user_id}/profiles/{profile_id}/resumeSeries/{series_id}'.format(user_id=userdata.get('user_id'), profile_id=userdata.get('profile_id'), series_id=series_id)
    #     return self._session.get(url, params=params).json()

    # def resume_program(self, program_id):
    #     params = {
    #         'jwToken': userdata.get('token'),
    #     }

    #     url = '/resume/v1/users/{user_id}/profiles/{profile_id}/resume/{program_id}'.format(user_id=userdata.get('user_id'), profile_id=userdata.get('profile_id'), program_id=program_id)
    #     return self._session.get(url, params=params).json()

    def set_profile(self, profile_id):
        self._check_token()

        params = {
            'type': 'mobile',
            'os': 'Android',
            'stanVersion': STAN_VERSION,
        }

        payload = {
            'jwToken': userdata.get('token'),
            'profileId': profile_id,
        }

        self._login('/login/v1/sessions/mobile/app', payload, params)

    def profiles(self):
        self._check_token()

        params = {
            'jwToken': userdata.get('token'),
        }

        return self._session.get(
            '/accounts/v1/users/{user_id}/profiles'.format(
                user_id=userdata.get('user_id')),
            params=params).json()

    def add_profile(self, name, icon_set, icon_index, kids=False):
        self._check_token()

        payload = {
            'jwToken': userdata.get('token'),
            'name': name,
            'isKidsProfile': kids,
            'iconSet': icon_set,
            'iconIndex': icon_index,
        }

        return self._session.post(
            '/accounts/v1/users/{user_id}/profiles'.format(
                user_id=userdata.get('user_id')),
            data=payload).json()

    def delete_profile(self, profile_id):
        self._check_token()

        params = {
            'jwToken': userdata.get('token'),
            'profileId': profile_id,
        }

        return self._session.delete(
            '/accounts/v1/users/{user_id}/profiles'.format(
                user_id=userdata.get('user_id')),
            params=params).ok

    def profile_icons(self):
        self._check_token()

        params = {
            'jwToken': userdata.get('token'),
        }

        return self._session.get('/accounts/v1/accounts/icons',
                                 params=params).json()

    def program(self, program_id):
        self._check_token()

        params = {
            'jwToken': userdata.get('token'),
        }

        if userdata.get('profile_kids', False):
            url = '/cat/v12/kids/programs/{program_id}.json'
        else:
            url = '/cat/v12/programs/{program_id}.json'

        return self._session.get(url.format(program_id=program_id),
                                 params=params).json()

    def play(self, program_id):
        self._check_token(force=True)

        program_data = self.program(program_id)
        if 'errors' in program_data:
            try:
                msg = program_data['errors'][0]['code']
                if msg == 'Streamco.Concurrency.OutOfRegion':
                    msg = _.IP_ADDRESS_ERROR
                elif msg == 'Streamco.Catalogue.NOT_SAFE_FOR_KIDS':
                    msg = _.KIDS_PLAY_DENIED
            except:
                msg = ''

            raise APIError(_(_.PLAYBACK_ERROR, msg=msg))

        jw_token = userdata.get('token')

        params = {
            'programId': program_id,
            'jwToken': jw_token,
            'format': 'dash',
            'capabilities.drm': 'widevine',
            'quality': 'high',
        }

        data = self._session.get('/concurrency/v1/streams',
                                 params=params).json()

        if 'errors' in data:
            try:
                msg = data['errors'][0]['code']
                if msg == 'Streamco.Concurrency.OutOfRegion':
                    msg = _.IP_ADDRESS_ERROR
            except:
                msg = ''

            raise APIError(_(_.PLAYBACK_ERROR, msg=msg))

        play_data = data['media']
        play_data['drm']['init_data'] = self._init_data(
            play_data['drm']['keyId'])
        play_data['videoUrl'] = API_URL.format(
            '/manifest/v1/dash/androidtv.mpd?url={url}&audioType=all&version=88'
            .format(url=quote_plus(play_data['videoUrl']), ))

        params = {
            'form': 'json',
            'schema': '1.0',
            'jwToken': jw_token,
            '_id': data['concurrency']['lockID'],
            '_sequenceToken': data['concurrency']['lockSequenceToken'],
            '_encryptedLock': 'STAN',
        }

        self._session.get('/concurrency/v1/unlock', params=params).json()

        return program_data, play_data

    def _init_data(self, key):
        key = key.replace('-', '')
        key_len = '{:x}'.format(len(bytearray.fromhex(key)))
        key = '12{}{}'.format(key_len, key)
        key = bytearray.fromhex(key)

        return cenc_init(key)

    def _get_sign(self, payload):
        module_version = 214

        f3757a = bytearray(
            (144, 100, 149, 1, 2, 8, 36, 208, 209, 51, 103, 131, 240, 66,
             module_version, 20, 195, 170, 44, 194, 17, 161, 118, 71, 105, 42,
             76, 116, 230, 87, 227, 40, 115, 5, 62, 199, 66, 7, 251, 125, 238,
             123, 71, 220, 179, 29, 165, 136, 16, module_version, 117, 10, 100,
             222, 41, 60, 103, 2, 121, 130, 217, 75, 220, 100, 59, 35, 193, 22,
             117, 27, 74, 50, 85, 40, 39, 31, 180, 81, 34, 155, 172, 202, 71,
             162, 202, 234, 91, 176, 199, 207, 131, 229, 125, 105, 9, 227, 188,
             234, 61, 33, 17, 113, 222, 173, 182, 120, 34, 80, 135, 219, 8, 97,
             176, 62, 137, 126, 222, 139, 136, 77, 243, 37, 11, 234, 82, 244,
             222, 44))

        f3758b = bytearray(
            (120, 95, 52, 175, 139, 155, 151, 35, 39, 184, 141, 27, 55, 215,
             102, 173, 2, 37, 141, 164, 236, 217, 173, 194, 94, 67, 195, 24,
             221, 66, 233, 11, 226, 91, 33, 249, 225, 54, 88, 54, 118, 101, 31,
             248, 11, 208, 206, 226, 68, 20, 143, 37, 104, 159, 184, 22, 53,
             179, 104, 152, 170, 29, 26, 6, 163, 45, 87, 193, 136, 226, 128,
             245, 231, 238, 154, 211, 71, 134, 232, 99, 35, 54, 170, 128, 1,
             218, 249, 70, 182, 145, 125, 211, 16, 43, 118, 177, 64, 128, 111,
             73, 234, 22, 21, 165, 67, 23, 15, 5, 11, 70, 48, 97, 134, 185, 11,
             28, 167, 140, 123, 81, 240, 247, 77, 187, 23, 243, 89, 54))

        msg = ''
        for key in sorted(payload.keys()):
            if msg: msg += '&'
            msg += key + '=' + quote_plus(payload[key], safe="_-!.~'()*")

        bArr = bytearray(len(f3757a))
        for i in range(len(bArr)):
            bArr[i] = f3757a[i] ^ f3758b[i]

        bArr2 = bytearray(int(len(bArr) / 2))
        for i in range(len(bArr2)):
            bArr2[i] = bArr[i] ^ bArr[len(bArr2) + i]

        signature = hmac.new(bArr2,
                             msg=msg.encode('utf8'),
                             digestmod=hashlib.sha256).digest()

        return base64.b64encode(signature).decode('utf8')

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

        userdata.delete('profile_id')
        userdata.delete('profile_icon')
        userdata.delete('profile_name')
        userdata.delete('profile_kids')

        self.new_session()
Exemple #4
0
class API(object):
    def new_session(self):
        self.logged_in = False
        self._session  = Session(HEADERS, base_url=BASE_URL, timeout=30)
        self._set_authentication(userdata.get('access_token'))

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

        self._session.headers.update({'Authorization': 'Bearer {}'.format(access_token)})
        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',
            'scope': 'browse video_playback device',
        }

        self._oauth_token(payload, {'Authorization': None})

    def _oauth_token(self, payload, headers=None):
        data = self._session.post('/tokens', data=payload, headers=headers).json()
        self._check_errors(data)

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

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

    def _device_serial(self):
        def _format_id(string):
            try:
                mac_address = uuid.getnode()
                if mac_address != uuid.getnode():
                    mac_address = ''
            except:
                mac_address = ''

            system, arch = get_system_arch()
            return str(string.format(mac_address=mac_address, system=system).strip())

        return str(uuid.uuid3(uuid.UUID(UUID_NAMESPACE), _format_id(settings.get('device_id'))))

    def device_code(self):
        self.logout()

        serial = self._device_serial()

        payload = {
            'client_id': CLIENT_ID,
            'client_secret': CLIENT_ID,
            'scope': 'browse video_playback_free account_registration',
            'grant_type': 'client_credentials',
            'deviceSerialNumber': serial,
            'clientDeviceData': {
            	'paymentProviderCode': 'google-play'
            }
        }

        data = self._session.post('/tokens', json=payload).json()
        self._check_errors(data)

        self._set_authentication(data['access_token'])

        payload = {
            'model': DEVICE_MODEL,
            'serialNumber': serial,
            'userIntent': 'login',
        }

        data = self._session.post('/devices/activationCode', json=payload).json()

        return serial, data['activationCode']

    def set_profile(self, profile_id):
        self._refresh_token()

        payload = {
            'grant_type': 'user_refresh_profile',
            'profile_id': profile_id,
            'refresh_token': userdata.get('refresh_token'),
        }

        self._oauth_token(payload)

    def device_login(self, serial, code):
        payload = {
            'model': DEVICE_MODEL,
            'code': code,
            'serialNumber': serial,
            'grant_type': 'user_activation_code',
            'scope': 'browse video_playback device elevated_account_management',
        }

        try:
            self._oauth_token(payload)
            return True
        except Exception as e:
            return False

    def _check_errors(self, data, error=_.API_ERROR):
        if 'code' in data:
            error_msg = data.get('message') or data.get('code')
            raise APIError(_(error, msg=error_msg))

    def profiles(self):
        self._refresh_token()
        payload = [{'id': 'urn:hbo:profiles:mine'},]
        return self._session.post('/content', json=payload).json()[0]['body']['profiles']

    def delete_profile(self, profile_id):
        self._refresh_token()
        data = self._session.delete('https://profiles.api.hbo.com/profiles/{}'.format(profile_id)).json()
        return data['success']

    def add_profile(self, name, kids, avatar):
        self._refresh_token()

        payload = {
            'avatarId': avatar,
            'name': name,
            'profileType': 'adult',
        }

        if kids:
            payload.update({
                'profileType': 'child',
                'exitPinRequired': False,
                'birth': {
                    'year': arrow.now().year - 5,
                    'month': arrow.now().month,
                },
                'parentalControls': {
                    'movie': 'G',
                    'tv': 'TV-G',
                },
            })

        data = self._session.post('https://profiles.api.hbo.com/profiles', json=payload).json()
        self._check_errors(data)

        for row in data['results']['profiles']:
            if row['name'] == name:
                return row

        raise APIError('Failed to create profile')

    def _age_category(self):
        month, year = userdata.get('profile', {}).get('birth', [0,0])

        i = arrow.now()
        n = i.year - year
        if i.month < month:
            n -= 1
        age = max(0, n)

        group = AGE_CATS[0][1]
        for cat in AGE_CATS:
            if age >= cat[0]:
                group = cat[1]

        return group

    def content(self, slug, tab=None):
        self._refresh_token()

        params = {
            'device-code': DEVICE_MODEL,
            'product-code': 'hboMax',
            'api-version': 'v9',
            'country-code': 'us',
            'profile-type': 'default',
            'signed-in': True,
        }

        if userdata.get('profile',{}).get('child', 0):
            params.update({
                'profile-type': 'child',
                'age-category': self._age_category(),
            })

        data = self._session.get('/express-content/{}'.format(slug), params=params).json()
        self._check_errors(data)

        _data = {}
        for row in data:
            _data[row['id']] = row['body']

        return self._process(_data, tab or slug)

    def _process(self, data, slug):
        main = data[slug]

        def process(element):
            element['items'] = []
            element['tabs'] = []
            element['edits'] = []
            element['seasons'] = []
            element['episodes'] = []
            element['target'] = None

            for key in element.get('references', {}):
                if key in ('items', 'tabs', 'edits', 'seasons', 'episodes'):
                    for id in element['references'][key]:
                        if id == '$dataBinding':
                            continue

                        item = {'id': id}
                        if id in data:
                            item.update(data[id])
                        process(item)
                        element[key].append(item)
                else:
                    element[key] = element['references'][key]

            element.pop('references', None)

        process(main)
        return main

    def search(self, query):
        self._refresh_token()

        payload = [{
            'id': 'urn:hbo:flexisearch:{}'.format(query),
        }]

        data = self._session.post('/content', json=payload).json()
        self._check_errors(data)

        keys = {}
        key = None
        for row in data:
            keys[row['id']] = row['body']
            if row['id'].startswith('urn:hbo:grid:search') and row['id'].endswith('-all'):
                key = row['id']

        if not key:
            return None

        return self._process(keys, key)

    def play(self, slug):
        self._refresh_token()

        content_data = self.content(slug)
        if not content_data['edits']:
            raise APIError(_.NO_VIDEO_FOUND)

        selected = content_data['edits'][0]
        for edit in content_data['edits']:
            for row in edit.get('textTracks', []):
                if row.get('language') == 'en-US':
                    selected = edit
                    break

        payload = [{
            'id': selected['video'],
            'headers' : {
                'x-hbo-preferred-blends': 'DASH_WDV,HSS_PR',
                'x-hbo-video-mlp': True,
            }
        }]

        data = self._session.post('/content', json=payload).json()[0]['body']
        self._check_errors(data)

        for row in data['manifests']:
            if row['type'] == 'urn:video:main':
                return row, content_data

        raise APIError(_.NO_VIDEO_FOUND)

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