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