def login(self, username, password): self.logout() s = Session() s.headers.update(HEADERS) if not password: raise APIError(_.LOGIN_ERROR) r = s.get(BASE_URL + 'superview/', timeout=20) soup = BeautifulSoup(r.text, 'html.parser') login_form = soup.find(id="membersignin") inputs = login_form.find_all('input') data = {} for elem in inputs: if elem.attrs.get('value'): data[elem.attrs['name']] = elem.attrs['value'] data.update({ 'signinusername': username, 'signinpassword': password, }) r = s.post(BASE_URL + 'superview/', data=data, allow_redirects=False, timeout=20) if r.status_code != 302: raise APIError(_.LOGIN_ERROR) if settings.getBool('save_password', False): userdata.set(PASSWORD_KEY, password) for cookie in r.cookies: if cookie.name.startswith('wordpress_logged_in'): userdata.set('_cookies', {cookie.name: cookie.value}) break
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): id_token = userdata.get('id_token') if not id_token: return self._session.headers.update({'Authorization': id_token}) self.logged_in = True def navigation(self): return self._session.get( '/metadata/navigations/nav_v1').json()['navigations'] def page(self, name): data = self._session.get('/metadata/pages/{}'.format(name)).json() return [x for x in data['panels'] if 'title' in x] def editorial(self, name): data = self._session.get( '/metadata/editorials/v2/{}/mobile'.format(name)).json() return data['assets'] def asset(self, id): return self._session.get( '/metadata/assets/v2/{}/mobile'.format(id)).json() def login(self, username, password): self.logout() payload = { "username": username, "password": password, "rememberMe": "false" } r = self._session.post('/userauth/login', json=payload) if not r.ok: if r.status_code == 403: raise APIError(_.GEO_BLOCKED) else: raise APIError( _(_.LOGIN_ERROR, msg=r.json()['error'].get('description'))) data = r.json() userdata.set('user_id', data['userId']) self._parse_token(data['result']) def _parse_token(self, data): userdata.set('id_token', data['IdToken']) userdata.set('expires', int(time.time() + data['ExpiresIn'] - 15)) if 'RefreshToken' in data: userdata.set('refresh_token', data['RefreshToken']) self._set_authentication() def _check_token(self): if userdata.get('expires') > time.time(): return headers = { 'X-Amz-Target': 'AWSCognitoIdentityProviderService.InitiateAuth', 'X-Amz-User-Agent': 'aws-amplify/0.1.x js', 'Content-Type': 'application/x-amz-json-1.1', } payload = { 'AuthFlow': 'REFRESH_TOKEN_AUTH', 'AuthParameters': { 'DEVICE_KEY': None, 'REFRESH_TOKEN': userdata.get('refresh_token'), }, 'ClientId': AWS_CLIENT_ID, } r = self._session.post(AWS_URL, json=payload, headers=headers) data = r.json() if 'message' in data: raise APIError(_(_.LOGIN_ERROR, msg=data['message'])) self._parse_token(data['AuthenticationResult']) def play(self, asset, from_start=False): self._check_token() params = { 'type': 'dash', 'drm': 'widevine', 'watchMode': 'startover' if from_start else 'live', } r = self._session.get( '/playback/generalPlayback/mobile/users/{user_id}/assets/{asset_id}' .format(user_id=userdata.get('user_id'), asset_id=asset), params=params) if not r.ok: if r.status_code == 403: raise APIError(_.GEO_BLOCKED) else: raise APIError(r.json()['error'].get('description')) try: data = r.json() stream = data['playback']['items']['item'] if type(stream) is list: stream = stream[0] except: stream = None if not stream: raise APIError(_.NO_STREAM) return stream def logout(self): userdata.delete('user_id') userdata.delete('id_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(base_url=API_URL, headers=HEADERS) self._set_authentication() def _set_authentication(self): access_token = userdata.get('access_token') if not access_token: return self._session.headers.update({'authorization': access_token}) self.logged_in = True def _refresh_token(self, force=False): refresh_token = userdata.get('refresh_token') if not refresh_token or (not force and userdata.get('expires', 0) > time()): return params = { 'key': GOOGLE_KEY, } payload = { 'grantType': 'refresh_token', 'refreshToken': refresh_token, } data = self._session.post(TOKEN_URL, params=params, json=payload).json() if 'error' in data: self.logout() raise APIError(data['error']['message']) userdata.set('access_token', data['access_token']) userdata.set('refresh_token', data['refresh_token']) userdata.set('expires', int(time()) + int(data['expires_in']) - 30) self._set_authentication() def channels(self): self._refresh_token() params = { 'noGuideData': True, } return self._session.get('/programGuide', params=params).json() def play(self, id): self._refresh_token() params = { 'channelId': id, } return self._session.get('/playbackAuthenticated', params=params).json() def epg(self, id=None): self._refresh_token() params = {} if id: params['channelId'] = id return self._session.get('/programGuide', params=params).json() def login(self, email, password, register=False): self.logout() params = { 'key': GOOGLE_KEY, } payload = { 'email': email, 'password': password, 'returnSecureToken': True, } data = self._session.post(REGISTER_URL if register else LOGIN_URL, params=params, json=payload).json() if 'error' in data: raise APIError(data['error']['message']) userdata.set('refresh_token', data['refreshToken']) self._refresh_token(force=True) 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) if userdata.get('singtel_tv_no'): self.logged_in = True def login(self, singtel_tv_no, identification_no): self.logout() device_id = hash_6(singtel_tv_no, length=16) payload = { 'deviceType': APP_DEVICE, 'deviceId': device_id, 'identityType': APP_ID_TYPE, 'identityId': identification_no, 'iptvNo': singtel_tv_no, 'appId': APP_ID, 'appKey': APP_KEY, 'mode': APP_MODE, 'ver': APP_VER, } data = self._session.post('/HomeLoginService.aspx', data={ 'JSONtext': json.dumps(payload) }).json()['item'][0] if data.get('StatusCode'): raise APIError(_.LOGIN_ERROR) userdata.set('device_id', device_id) userdata.set('singtel_tv_no', singtel_tv_no) userdata.set('identification_no', identification_no) return data @mem_cache.cached(60 * 5) def channels(self): data = self._session.gz_json(DATA_URL) channels = [ x for x in data['getAuthorizationResponse']['channelList'] if x['isLive'].upper() == 'Y' ] user_data = self.login(userdata.get('singtel_tv_no'), userdata.get('identification_no')) if user_data['OTTAccess'].upper() != 'Y': guest_ids = [ int(x['ChannelID']) for x in user_data['guestPreviewChannels'] ] channels = [x for x in channels if x['id'] in guest_ids] else: channels = [ x for x in channels if x['shortName'] in user_data['SubscribedCallLetters'] ] return channels def _stop_stream(self, channel_id, token): start = arrow.utcnow() end = start.shift(seconds=10) payload = { 'deviceType': APP_DEVICE, 'deviceId': userdata.get('device_id'), 'identityType': APP_ID_TYPE, 'identityId': userdata.get('identification_no'), 'iptvNo': userdata.get('singtel_tv_no'), 'channelID': channel_id, 'startTime': start.format('YYYY-MM-DD HH:mm:ss'), 'stopTime': end.format('YYYY-MM-DD HH:mm:ss'), 'bitRates': '1', 'token': token, 'appId': APP_ID, 'appKey': APP_KEY, } resp = self._session.post('/LogStopStream.aspx', data={'JSONtext': json.dumps(payload)}) return resp.ok def play(self, channel_id, call_letter): payload = { 'deviceType': APP_DEVICE, 'deviceId': userdata.get('device_id'), 'identityType': APP_ID_TYPE, 'identityId': userdata.get('identification_no'), 'iptvNo': userdata.get('singtel_tv_no'), 'callLetter': call_letter, 'channelID': channel_id, 'appId': APP_ID, 'appKey': APP_KEY, 'mode': APP_MODE, 'ver': APP_VER, } data = self._session.get('/WatchOTTStreaming.aspx', data={ 'JSONtext': json.dumps(payload) }).json()['item'][0] if data.get('StatusCode'): raise APIError(_(_.PLAYBACK_ERROR, error=data.get('StatusDesc'))) self._stop_stream(channel_id, data['UserToken']) return data def logout(self): userdata.delete('singtel_tv_no') userdata.delete('identification_no') userdata.delete('device_id') mem_cache.empty() self.new_session()
class API(object): def new_session(self): self.logged_in = False self._session = Session(HEADERS) self._set_authentication() def _set_authentication(self): token = userdata.get('jwt_token') if not token: return self._session.headers.update({'Authorization': 'Bearer {}'.format(token)}) self.logged_in = True def _device_info(self, username): return { 'uuid': hashlib.sha1(username.encode('utf8')).hexdigest(), 'platform': 'Android', 'name': 'KODI', 'os': 'KODI', 'model': 'KODI', #'version': '27' } def _query_request(self, query, variables=None, **kwargs): data = { 'query': ' '.join(query.split()), 'variables': variables or {}, } return self._session.post(API_URL, json=data, **kwargs).json() def _set_token(self, jwt_token): userdata.set('jwt_token', jwt_token) data = jwt_data(jwt_token) userdata.set('expires', data['exp'] - 2592000) self._set_authentication() def login(self, username, password): self.logout() variables = { 'input': { 'deviceInfo': self._device_info(username), }, 'username': username, 'password': password, } data = self._query_request(queries.LOGIN, variables) if data.get('errors'): raise APIError(data['errors'][0].get('message')) self._set_token(data['data']['login']['session']['token']) def content(self, screen_id): self._check_token() variables = { 'screenId': screen_id, } return self._query_request(queries.CONTENT, variables)['data']['screen'] def _check_token(self): if time.time() < userdata.get('expires', 0): return self.set_profile(userdata.get('profile_id')) def account(self): self._check_token() return self._query_request(queries.ACCOUNT)['data']['account'] def set_profile(self, profile_id): variables = { 'input': { 'selectedProfile': profile_id, }, 'pin': None, } data = self._query_request(queries.UPDATE_ACCOUNT, variables)['data']['account'] self._set_token(data['session']['token']) for row in data['profiles']: if row['id'] == data['selectedProfile']: userdata.set('profile_id', row['id']) userdata.set('profile_name', row['name']) userdata.set('profile_icon', row['avatar']['uri']) userdata.set('profile_kids', row['isKid']) return raise APIError(_.PROFILE_SET_ERROR) def search(self, query): self._check_token() variables = { 'input': { 'query': query, }, } data = self._query_request(queries.SEARCH, variables)['data']['search']['components'] results = [] for row in data: results.extend(row['tiles']) return results def playback_auth(self, contentID): self._check_token() variables = { 'contentItemId': contentID, } return self._query_request(queries.PLAYBACK_AUTH, variables) def get_brightcove_src(self, referenceID, jwt_token): brightcove_url = BRIGHTCOVE_URL.format(BRIGHTCOVE_ACCOUNT, referenceID, jwt_token) data = self._session.get(brightcove_url, headers={'BCOV-POLICY': BRIGHTCOVE_KEY}).json() return util.process_brightcove(data) def logout(self): userdata.delete('jwt_token') userdata.delete('expires') userdata.delete('profile_id') userdata.delete('profile_name') userdata.delete('profile_icon') userdata.delete('profile_kids') self.new_session()
class API(object): def new_session(self): self.logged_in = False self._session = Session(headers=HEADERS) self._set_authentication() self._default_params = DEFAULT_PARAMS self._default_params['signedUp'] = False #self.logged_in def _set_authentication(self): access_token = userdata.get('access_token') if not access_token: self._session.headers.update( {'authorization': 'Bearer {}'.format(DEFAULT_TOKEN)}) return self._session.headers.update({ 'authorization': 'Bearer {}'.format(userdata.get('access_token')) }) self.logged_in = True @mem_cache.cached(60 * 5) def _market_id(self): try: return self._session.get(MARKET_ID_URL, params={ 'apikey': 'web' }).json()['_id'] except: log.debug('Failed to get market id') return '-1' def _oauth_token(self, payload): data = self._session.post('https://auth2.swm.digital/connect/token', data=payload, headers={ 'x-swm-apikey': SWM_API_KEY }).json() if 'Errors' in data: self.logout() raise APIError(data['Errors'][0]['Detail']) userdata.set('access_token', data['access_token']) userdata.set('expires', int(time() + data['expires_in'] - 30)) if 'refresh_token' in data: userdata.set('refresh_token', data['refresh_token']) self._set_authentication() 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 = { 'platformId': 'android', 'regsource': '7plus', 'refreshToken': userdata.get('refresh_token'), } self._oauth_token(payload) @mem_cache.cached(60 * 5) def search(self, query): params = { 'searchTerm': query, 'size': 30, } params.update(self._default_params) return self._session.get( 'https://searchapi.swm.digital/3.0/api/Search', params=params).json() @mem_cache.cached(60 * 5) def content(self, slug): params = self._default_params params['market-id'] = self._market_id() return self._session.get( 'https://component-cdn.swm.digital/content/{slug}'.format( slug=slug), params=params).json() @mem_cache.cached(60 * 5) def component(self, slug, component_id): self._refresh_token() params = { 'component-id': component_id, } params.update(self._default_params) return self._session.get( 'https://component.swm.digital/component/{slug}'.format(slug=slug), params=params).json() @mem_cache.cached(60 * 5) def video_player(self, slug): params = self._default_params params['market-id'] = self._market_id() return self._session.get( 'https://component.swm.digital/player/live/{slug}'.format( slug=slug), params=params).json()['videoPlayer'] def login(self, username, password): self.logout() payload = { 'loginID': username, 'password': password, 'apiKey': GIGYA_API_KEY, 'format': 'json', } data = self._session.post( 'https://accounts.au1.gigya.com/accounts.login', data=payload).json() if 'errorMessage' in data: raise APIError(data['errorMessage']) cookie = { data['sessionInfo']['cookieName']: data['sessionInfo']['cookieValue'] } payload = { 'login_token': data['sessionInfo']['cookieValue'], 'authMode': 'cookie', 'apiKey': GIGYA_API_KEY, 'format': 'json', } data = self._session.post( 'https://accounts.au1.gigya.com/accounts.getJWT', params=payload, cookies=cookie).json() if 'errorMessage' in data: raise APIError(data['errorMessage']) payload = { 'platformId': 'android', 'regsource': '7plus', 'IdToken': data['id_token'], } self._oauth_token(payload) def play(self, account, reference, live): self._refresh_token() params = { 'appId': '7plus', 'deviceType': 'android', 'platformType': 'app', 'accountId': account, 'referenceId': reference, 'deliveryId': 'csai', # 'ppId': '', # 'deviceId': '71aa8979-75b0-4393-ad93-239d368ed79d', # 'pc': 3350, # 'advertid': 'null', # 'videoType': 'live', # 'tvid': 'null', # 'ozid': 'abda324c-31bd-473f-8633-0cdd8bdbf780', } if live: params['videoType'] = 'live' data = self._session.get('https://videoservice.swm.digital/playback', params=params).json() if 'media' not in data: raise APIError(data[0]['error_code']) return process_brightcove(data['media']) def logout(self): mem_cache.empty() 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) self._set_authentication() def _set_authentication(self): token = userdata.get('access_token') if not token: return self._session.headers.update( {'authorization': 'Bearer {}'.format(token)}) self._session.headers.update( {'x-user-profile': userdata.get('profile_id')}) self.logged_in = True def _query_request(self, query, variables=None, **kwargs): self._refresh_token() data = { 'query': ' '.join(query.split()), 'variables': variables or {}, } return self._session.post(GRAPH_URL, json=data, **kwargs).json() 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 profile email offline_access', } self._oauth_token(payload) def _oauth_token(self, payload): token_data = self._session.post('https://login.sky.co.nz/oauth/token', json=payload, error_msg=_.TOKEN_ERROR).json() if 'error' in token_data: error = _.REFRESH_TOKEN_ERROR if data.get( 'grant_type') == 'refresh_token' else _.LOGIN_ERROR raise APIError(_(error, msg=token_data.get('error_description'))) 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']) #Force 1st profile data = jwt_data(token_data['access_token']) profile_id = data['https://skygo.co.nz/profiles'][0]['id'] userdata.set('profile_id', profile_id) #### self._set_authentication() def channels(self): ids = [] channels = [] groups = self._query_request( queries.CHANNELS)['data']['linearChannelGroups'] for group in groups: for row in group.get('channels', []): if row['__typename'] == 'LinearChannel' and row[ 'id'] not in ids: ids.append(row['id']) channels.append(row) return sorted(channels, key=lambda x: x['number']) def play_channel(self, channel_id): variables = { 'deviceId': '', 'assetId': channel_id, 'channelId': channel_id, # 'playbackDevice': { # 'platform': 'Windows', # 'osVersion': '10', # 'drmType': 'WIDEVINE', # 'drmLevel': 'SW_SECURE_DECODE' # } } data = self._query_request(queries.START_LINEAR, variables)['data']['startLinearPlayback'] if data['__typename'] == 'SubscriptionNeeded': raise APIError('{} Subscription Required'.format( data['subscriptions'][0]['title'])) elif data['__typename'] == 'Geoblocked': raise APIError(_.GEO_ERROR) elif data['__typename'] != 'LinearPlaybackSources': raise APIError('Unkown error: {}'.format(data['__typename'])) try: self._query_request(queries.STOP_LINEAR, variables)['data']['stopLinearPlayback'] except: log.debug('Stop Linear Failed') return data['playbackSource']['streamUri'], data['playbackSource'][ 'drmLicense']['licenseUri'] def login(self, username, password): self.logout() params = { 'client_id': CLIENT_ID, 'audience': 'https://api.sky.co.nz', 'redirect_uri': 'https://www.skygo.co.nz', 'connection': 'Sky-Internal-Connection', 'scope': 'openid profile email offline_access', 'response_type': 'code', } resp = self._session.get('https://login.sky.co.nz/authorize', params=params, allow_redirects=False) parsed = urlparse(resp.headers['location']) payload = dict(parse_qsl(parsed.query)) payload.update({ 'username': username, 'password': password, 'tenant': 'skynz-prod', 'client_id': CLIENT_ID, 'client': None, }) resp = self._session.post( 'https://login.sky.co.nz/usernamepassword/login', json=payload) if not resp.ok: data = resp.json() raise APIError(_(_.LOGIN_ERROR, msg=data['message'])) soup = BeautifulSoup(resp.text, 'html.parser') payload = {} for e in soup.find_all('input'): if 'name' in e.attrs: payload[e.attrs['name']] = e.attrs.get('value') resp = self._session.post('https://login.sky.co.nz/login/callback', data=payload, allow_redirects=False) parsed = urlparse(resp.headers['location']) data = dict(parse_qsl(parsed.query)) payload = { 'code': data['code'], 'client_id': CLIENT_ID, 'grant_type': 'authorization_code', 'redirect_uri': 'https://www.skygo.co.nz' } self._oauth_token(payload) def logout(self): userdata.delete('access_token') userdata.delete('refresh_token') userdata.delete('expires') userdata.delete('profile_id') self.new_session()
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()
class API(object): def new_session(self): self.logged_in = False self._session = Session(HEADERS, base_url=BASE_URL) self.set_authentication() def set_authentication(self): token = userdata.get('auth_token') if not token: return self._session.headers.update({'x-auth-token': token}) self.logged_in = True def login(self, username, password): self.logout() data = {'user': { 'email': username, 'password': password, 'remember_me': True }} data = self._session.post('/services/users/auth/sign_in', json=data).json() if 'error' in data: raise APIError(data['error']) auth_token = data['auth_token'] user_id = data['account']['user_id'] userdata.set('auth_token', auth_token) userdata.set('user_id', user_id) self.set_authentication() def my_library(self): meta = {} items = self._session.get('/services/content/v3/user_library/{}/index'.format(userdata.get('user_id')), params={'sort_by': 'relevance'}).json() _meta = self._session.get('/services/meta/v2/film/{}/show_multiple'.format(','.join(str(x['info']['film_id']) for x in items))).json() for item in _meta: meta[item['film_id']] = item for item in items: item['meta'] = meta.get(item['info']['film_id'], {}) return items def get_stream(self, film_id): play_data = self._session.get('/services/content/v4/media_content/play/film/{}'.format(film_id), params={'encoding_type':'dash', 'drm':'widevine'}).json() if 'error' in play_data: raise APIError(play_data['error']) mpd_url = play_data['streams'][0]['url'] key_url = BASE_URL.format('/services/license/widevine/cenc?context={}'.format(play_data['streams'][0]['drm_key_encoded'].strip())) item = plugin.Item( path = play_data['streams'][0]['url'], inputstream = inputstream.Widevine(license_key=key_url), headers = self._session.headers, ) return item def logout(self): userdata.delete('auth_token') userdata.delete('user_id') self.new_session()
class API(object): def new_session(self): self.logged_in = False self._session = Session(base_url=BASE_URL, headers=HEADERS) self._set_auth(userdata.get('token')) def _set_auth(self, token): if not token: return self._session.cookies.update({ '_session': token, '_device': userdata.get('device_id') }) self.logged_in = True def login(self, username, password): self.logout(site_logout=False) device_id = hash_6(username.lower().strip(), length=20) device_id = 'Windows:Chrome:' + device_id[:5] + '_' + device_id[ 5:13] + '_' + device_id[13:] self._session.cookies.update({'_device': device_id}) resp = self._session.get('/login') soup = BeautifulSoup(resp.text, 'html.parser') found = None for form in soup.find_all('form'): data = {} for e in form.find_all('input'): data[e.attrs['name']] = e.attrs.get('value') if 'email' in data and 'password' in data: found = data break if not found: raise APIError(_.LOGIN_FORM_ERROR) found.update({ 'email': username, 'password': password, }) resp = self._session.post('/login', data=data, allow_redirects=False) if resp.status_code != 302 or not resp.cookies.get('_session'): raise APIError(_.LOGIN_FAILED) token = resp.cookies.get('_session') userdata.set('token', token) userdata.set('device_id', device_id) self._set_auth(token) def play(self, slug): resp = self._session.get('/videos/{slug}'.format(slug=slug), allow_redirects=False) if resp.status_code == 302 or 'The device limit for your account has been reached' in resp.text: raise APIError(_.DEVICE_LIMIT) page = resp.text.replace(' ', '').strip() play_url = re.search('embed_url:"(.*?)"', page).group(1) resp = self._session.get(play_url) page = resp.text.replace(' ', '').strip() event_id = re.search('eventId:(.*?),', page) if event_id: config_url = LIVESTREAM_URL.format(event_id=event_id.group(1)) else: config_url = re.search('"config_url":"(.*?)"', page).group(1) config_url = config_url.encode().decode('unicode_escape') data = self._session.get(config_url, headers={ 'Referer': 'https://embed.vhx.tv/' }).json() if data.get('secure_m3u8_url'): return data['secure_m3u8_url'], inputstream.HLS() default_cdn = data['request']['files']['dash']['default_cdn'] mpd_url = data['request']['files']['dash']['cdns'][default_cdn][ 'url'] #.replace('.json?base64_init=1', '.mpd') mpd_url = mpd_url.replace('.json', '.mpd') if data['request'].get('drm'): license_url = self._session.get( data['request']['drm']['cdms']['widevine']['license_url']).text ia = inputstream.Widevine(license_key=license_url) else: ia = inputstream.MPD() return mpd_url, ia def _vhx_token(self): token = mem_cache.get('vhx_token') if token: return token page = self._session.get('/').text.replace(' ', '').strip() token = re.search('TOKEN="(.*?)"', page).group(1) data = jwt_data(token) expires_in = int(data['exp'] - time.time()) - 30 mem_cache.set('vhx_token', token, expires_in) return token def browse(self): params = { 'product': 'https://api.vhx.tv/products/70302', 'per_page': 100, } return self._session.get('https://api.vhx.tv/browse', params=params, headers={ 'Authorization': 'Bearer {}'.format(self._vhx_token()) }).json() def search(self, query, page=1): params = { 'query': query, 'product': 'https://api.vhx.tv/products/70302', 'page': 1, 'per_page': 100, } return self._session.get('https://api.vhx.tv/collections', params=params, headers={ 'Authorization': 'Bearer {}'.format(self._vhx_token()) }).json() def collection(self, id): params = { 'per_page': 100, } return self._session.get( 'https://api.vhx.tv/collections/{}/items'.format(id), params=params, headers={ 'Authorization': 'Bearer {}'.format(self._vhx_token()) }).json() def logout(self, site_logout=True): if site_logout: self._session.get('/logout') userdata.delete('token') userdata.delete('device_id') mem_cache.delete('vhx_token') self.new_session()
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.cookies.update({TOKEN_COOKIE_KEY: token}) self.logged_in = True def require_token(self): if TOKEN_COOKIE_KEY in self._session.cookies: return app_version = self._session.get(APP_VERSION_URL).text.strip() payload = { "ClientVersion": app_version, "DeviceBrand": "Nvidia", "DeviceFirmwareVersion": "27", "DeviceId": "4jhrpoUfUr1Dxmbf", "DeviceManufacturer": "Nvidia", "DeviceModel": "Shield", "DeviceName": "AndroidTv", "IsRoot": "false", "MacAddress": "00:00:00:00:00:00", } token = self._session.post('/api/configuration/appconfig', json=payload).json()['Data']['AccessToken'] self._session.cookies.update({TOKEN_COOKIE_KEY: token}) def device_code(self): self.require_token() return self._session.post( '/api/account/generateauthcode').json()['Data'] def device_login(self, code): self.require_token() payload = { 'AuthCode': code, } data = self._session.post('/api/account/loginwithauthcode', json=payload).json()['Data'] if not data: return token = data['User']['AccessToken'] userdata.set('token', token) self._set_authentication() return True def login(self, username, password): self.require_token() payload = { 'Action': '/View/Account/SubmitLogin', 'jsonModel': json.dumps({ 'Username': username, 'Password': password, 'IsOnboarding': False, 'IsVoucher': False, }), 'captcha': '', } resp = self._session.post('/View/Account/SubmitLogin', json=payload) token = resp.cookies.get(TOKEN_COOKIE_KEY) if not token: raise APIError(_.LOGIN_ERROR) userdata.set('token', token) self._set_authentication() def live_channels(self): items = [] for i in range(10): payload = { 'Page': i, 'PageSize': PAGESIZE, } data = self._session.post('/api/broadcast/channels', json=payload).json()['Data'] items.extend(data['Items']) if len(data['Items']) < PAGESIZE: break return items def epg(self, days=3): start = arrow.utcnow() end = start.shift(days=days) payload = { 'StartTime': start.format('YYYY-MM-DDTHH:mm:00.000') + 'Z', 'EndTime': end.format('YYYY-MM-DDTHH:mm:00.000') + 'Z', 'OnlyLiveEvents': False, # 'ChannelId': 'beinsports1', } return self._session.post('/api/broadcast/tvguide', json=payload).json()['Data']['Items'] def catch_up(self, _type=0, catalog_id='CATCHUP'): items = [] for i in range(10): payload = { 'Page': i, 'PageSize': PAGESIZE, 'Type': _type, 'CatalogId': catalog_id, } data = self._session.post('/api/content/catchups', json=payload).json()['Data'] rows = data.get('Items', []) if not rows: break items.extend(rows) if len(rows) < PAGESIZE: break return items def play(self, channel_id=None, vod_id=None): payload = { 'ChannelId': channel_id, 'VodId': vod_id, } data = self._session.post('/api/play/play', json=payload).json()['Data'] if not data: raise APIError(_.NO_STREAM) return data def logout(self): userdata.delete('token') 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=HEADERS, base_url=BASE_URL) self.logged_in = userdata.get('key', None) != None #LEGACY userdata.delete('token') userdata.delete('user_id') def _set_auth(self, token): self._session.headers.update({'Authorization': 'Bearer {}'.format(token)}) def login(self, username, password): self.logout() payload = { 'email': username, 'password': password, } data = self._session.post('https://api.watchnebula.com/api/v1/auth/login/', json=payload).json() if 'key' not in data or not data['key']: msg = data['non_field_errors'][0] raise APIError(msg) userdata.set('key', data['key']) def _refresh_token(self, force=False): token = userdata.get('auth_token') if token and not force and userdata.get('expires', 0) > time(): self._set_auth(token) return log.debug('Refreshing token' if token else 'Fetching token') key = userdata.get('key') data = self._session.post('https://api.watchnebula.com/api/v1/authorization/', params={'from': 'Android'}, json={}, headers={'Authorization': 'Token {}'.format(key)}).json() token = data['token'] jwt = jwt_data(token) userdata.set('auth_token', token) userdata.set('expires', jwt['exp'] - 10) self._set_auth(token) @mem_cache.cached(expires=60*5) def categories(self): self._refresh_token() return self._session.get('/video/categories/').json()['results'] @mem_cache.cached(expires=60*5) def podcast_categories(self): self._refresh_token() return self._session.get('/podcast/categories/').json()['results'] @mem_cache.cached(expires=60*5) def podcast_creators(self, category='', page=1, page_size=100): self._refresh_token() params = { 'page': page, 'page_size': page_size, } if category: params['category'] = category return self._session.get('/podcast/channels/', params=params).json() @mem_cache.cached(expires=60*5) def podcasts(self, slug, page=1, page_size=100): self._refresh_token() params = { 'page': page, 'page_size': page_size, } return self._session.get('/podcast/channels/{slug}/'.format(slug=slug), params=params).json() @mem_cache.cached(expires=60*5) def search_videos(self, query, page=1, page_size=100): self._refresh_token() params = { 'page': page, 'page_size': page_size, 'text': query, } return self._session.get('/search/video/', params=params).json() @mem_cache.cached(expires=60*5) def search_creators(self, query): self._refresh_token() params = { 'text': query, } return self._session.get('/search/channel/video/', params=params).json() @mem_cache.cached(expires=60*5) def search_podcasts(self, query): self._refresh_token() params = { 'text': query, } return self._session.get('/search/channel/podcast/', params=params).json() @mem_cache.cached(expires=60*5) def featured(self): self._refresh_token() return self._session.get('/featured/').json() @mem_cache.cached(expires=60*5) def videos(self, category='', page=1, page_size=100): self._refresh_token() params = { 'page': page, 'page_size': page_size, } if category: params['category'] = category return self._session.get('/video/', params=params).json() def my_videos(self, page=1, page_size=100): self._refresh_token() params = { 'page': page, 'page_size': page_size, } return self._session.get('/library/video/', params=params).json() def my_creators(self, page=1, page_size=100): self._refresh_token() params = { 'page': page, 'page_size': page_size, } return self._session.get('/library/video/channels/', params=params).json() @mem_cache.cached(expires=60*5) def creator(self, slug, page=1, page_size=100): self._refresh_token() params = { 'page': page, 'page_size': page_size, } return self._session.get('/video/channels/{slug}/'.format(slug=slug), params=params).json() @mem_cache.cached(expires=60*5) def creators(self, category='', page=1, page_size=100, random=False): self._refresh_token() params = { 'page': page, 'page_size': page_size, } if category: params['category'] = category if random: params['random'] = 'true' return self._session.get('/video/channels/', params=params).json() def follow_creator(self, slug): self._refresh_token() payload = { 'channel_slug': slug, } if not self._session.post('/engagement/video/follow/', json=payload).ok: raise APIError('Failed to follow creator') def unfollow_creator(self, slug): self._refresh_token() payload = { 'channel_slug': slug, } if not self._session.post('/engagement/video/unfollow/', json=payload).ok: raise APIError('Failed to unfollow creator') def play(self, slug): self._refresh_token() data = self._session.get('/video/{slug}/stream/'.format(slug=slug)).json() resp = self._session.head(data['iframe']) url = resp.headers.get('Location').replace('.html', '') data = Session(headers=HEADERS).get(url).json() if 'response' not in data: raise APIError('{}'.format(data.get('message', 'unknown error getting playdata'))) play_url = data['response']['body']['outputs'][0]['url'] subtitles = data['response']['body'].get('subtitles', []) return play_url, subtitles def play_podcast(self, channel, episode): self._refresh_token() return self._session.get('/podcast/channels/{channel}/episodes/{episode}/'.format(channel=channel, episode=episode)).json() def logout(self): userdata.delete('key') userdata.delete('auth_token') userdata.delete('expires') mem_cache.empty() self.new_session()
class API(object): def new_session(self): self._session = Session(HEADERS, base_url=API_URL) self.logged_in = userdata.get('token') != None def login(self, username, password): data = { 'username': username, 'password': password, 'cookielink': 'true', 'format': 'json', } r = self._session.post('/secure/authenticate', data=data) data = r.json() token = r.cookies.get('nllinktoken') if not token: raise APIError(data.get('code')) userdata.set('token', token) def get_play_url(self, game, game_type): params = { 'id': game.id, 'gs': game.state, 'gt': game_type, 'type': 'game', 'format': 'json', } if game.state == Game.PROCESSING: params['st'] = game.start * 1000 params['dur'] = game.duration * 1000 cookies = { 'nllinktoken': userdata.get('token'), 'RugbyLoggedIn': userdata.get('username') } resp = self._session.get('/service/publishpoint', params=params, cookies=cookies) if not resp.ok: data = self._session.get('/game/{}'.format(game.slug), params={ 'format': 'json', 'purchases': True }, cookies=cookies).json() if data.get('noAccess'): raise APIError(_.NO_ACCESS) elif 'blackout' in data: raise APIError(_.GEO_ERROR, heading=_.GEO_HEADING) else: raise APIError(_.PLAY_ERROR) return resp.json()['path'] def _parse_game(self, item): def get_timestamp(key): if key in item: return arrow.get(item[key]).timestamp else: return 0 info = {'home': item['homeTeam'], 'away': item['awayTeam']} game = Game(id=int(item['id']), state=int(item['gameState']), start=get_timestamp('dateTimeGMT'), end=get_timestamp('endDateTimeGMT'), slug=str(item['seoName']), info=info) return game def update_games(self): to_create = [] data = self._session.get('/scoreboard', params={ 'format': 'json' }).json() if 'games' not in data: if data.get('code') == 'failedgeo': raise APIError(_.GEO_ERROR, heading=_.GEO_HEADING) else: raise APIError(_.GAMES_ERROR) for row in data['games']: game = self._parse_game(row) to_create.append(game) Game.truncate() Game.bulk_create(to_create, batch_size=100) def fetch_game(self, slug): data = self._session.get('/game/{}'.format(slug), params={ 'format': 'json' }).json() return self._parse_game(data) def channels(self): return self._session.get('/channels', params={'format': 'json'}).json() def search(self, cat_id, query, page=1): params = { 'param': '*{}*'.format(query), 'fq': 'catId2:{}'.format(cat_id), 'pn': page, 'format': 'json', } return self._session.get('/search', params=params).json() def logout(self): userdata.delete('token') self.new_session()
class API(object): def new_session(self): self.logged_in = False self._session = Session(HEADERS) self.set_authentication() def set_authentication(self): ob_session = userdata.get('ob_session') if not ob_session: return self._session.headers.update({ 'X-OB-Channel': 'I', 'X-OB-SESSION': ob_session }) self._session.cookies.clear() self._session.cookies.update({ 'OB-SESSION': ob_session, 'OB-PERSIST': '1' }) self.logged_in = True def login(self, username, password): self.logout() data = {"username": username, "password": password} r = self._session.post( 'https://auth.tab.co.nz/identity-service/api/v1/assertion/by-credentials', json=data) if r.status_code == 403: raise APIError(_.GEO_ERROR) elif r.status_code != 201: raise APIError(_.LOGIN_ERROR) userdata.set('ob_session', self._session.cookies['OB-SESSION']) if settings.getBool('save_password', False): userdata.set('pswd', password) else: userdata.set('ob_tgt', self._session.cookies['OB-TGT']) self.set_authentication() return r.json()['data']['ticket'] def _set_ob_token(self): password = userdata.get('pswd') if password: ticket = self.login(userdata.get('username'), password) else: resp = self._session.post( 'https://auth.tab.co.nz/identity-service/api/v1/assertion/by-token', cookies={'OB-TGT': userdata.get('ob_tgt')}) if resp.status_code == 403: raise APIError(_.GEO_ERROR) elif resp.status_code != 201: raise APIError(_.AUTH_ERROR) else: ticket = resp.json()['data']['ticket'] resp = self._session.get( 'https://api.tab.co.nz/account-service/api/v1/account/header', headers={'Authentication': ticket}) if 'OB-TOKEN' not in self._session.cookies: raise APIError(_.AUTH_ERROR) userdata.set('ob_session', self._session.cookies['OB-SESSION']) def access(self, type, id): self._set_ob_token() url = 'https://api.tab.co.nz/sports-service/api/v1/streams/access/{}/{}'.format( type, id) r = self._session.post(url) if r.status_code == 403: raise APIError(_.GEO_ERROR) data = r.json() if data['errors']: raise APIError(data['errors'][0]['text']) return data['data'][0]['streams'][0]['accessInfo']['contentUrl'] def live_events(self): r = self._session.get( 'https://content.tab.co.nz/content-service/api/v1/q/event-list?liveNow=true&hasLiveStream=true' ) if r.status_code == 403: raise APIError(_.GEO_ERROR) return r.json()['data']['events'] def logout(self): userdata.delete('ob_session') userdata.delete('ob_tgt') self.new_session()
class API(object): def new_session(self): self.logged_in = False self._session = Session(headers=HEADERS) self._set_authentication() def _set_authentication(self): access_token = userdata.get('access_token') if not access_token: return self._session.headers.update({'authorization': access_token}) self.logged_in = True 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') data = self._session.get(API_URL+'identity/refresh/{}'.format(userdata.get('refresh_token'))).json() self._process_token(data) def _process_token(self, data): if 'error' in data: raise APIError(data['error']) userdata.set('access_token', data['authorizationToken']) if 'refreshToken' in data: userdata.set('refresh_token', data['refreshToken']) token_data = jwt_data(data['authorizationToken']) userdata.set('expires', int(time()+(token_data['exp']-token_data['iat']-30))) self._set_authentication() def login(self, username, password): self.logout() deviceid = str(uuid.uuid3(uuid.UUID(UUID_NAMESPACE), str(username.lower().strip()))) params = { 'site': 'live-rugby', 'deviceId': 'browser-{}'.format(deviceid), } payload = { 'email': username, 'password': password, } data = self._session.post(API_URL+'identity/signin', json=payload, params=params).json() self._process_token(data) @mem_cache.cached(60*5) def page(self, slug): params = { 'site': 'live-rugby', 'path': slug, 'includeContent': 'true', #'moduleOffset': 0, #'moduleLimit': 8, 'languageCode': 'default', 'countryCode': 'NZ', } return self._session.get(CACHED_API_URL+'content/pages', params=params).json() @mem_cache.cached(60*5) def search(self, query): params = { 'site': 'live-rugby', 'searchTerm': query, 'types': 'VIDEO,SERIES,EVENT',#,ARTICLE,PERSON,BUNDLE', 'languageCode': 'default', } return self._session.get(CACHED_API_URL+'search/v1', params=params).json() def play(self, id): self._refresh_token() params = { 'id': id, 'deviceType': 'web_browser', 'contentConsumption': 'web', } data = self._session.get(API_URL+'entitlement/video/status', params=params).json() if not data['success']: raise APIError(data['errorMessage']) return data['video']['streamingInfo']['videoAssets']['hls'] 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, 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 url(self, name, path=''): config = self._client_config() if name == 'tokens': try: if config['endpoints']['getTokens']['domain'] == 'userGateway': return 'https://gateway{userSubdomain}.{domain}.hbo.com'.format( **config['routeKeys'] ) + config['endpoints']['getTokens']['path'] except KeyError: pass return 'https://oauth{globalUserSubdomain}.{domain}.hbo.com/auth/tokens'.format( **config['routeKeys']) elif name == 'gateway': return 'https://gateway{userSubdomain}.{domain}.hbo.com'.format( **config['routeKeys']) + path elif name == 'comet': return 'https://comet{contentSubdomain}.{domain}.hbo.com'.format( **config['routeKeys']) + path elif name == 'artist': return 'https://artist.{cdnDomain}.hbo.com'.format( **config['routeKeys']) + path else: return None def _oauth_token(self, payload, headers=None): data = self._session.post(self.url('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 _guest_login(self): 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(GUEST_AUTH, json=payload, headers={ 'Authorization': None }).json() if 'code' in data and data['code'] == 'invalid_credentials': raise APIError(_.BLOCKED_IP) self._check_errors(data) self._set_authentication(data['access_token']) return serial @mem_cache.cached(60 * 30) def _client_config(self): serial = self._guest_login() payload = { 'contract': 'hadron:1.1.2.0', 'preferredLanguages': ['en-us'], } data = self._session.post(CONFIG_URL, json=payload).json() self._set_authentication(userdata.get('access_token')) self._check_errors(data) if data['features']['currentRegionOutOfFootprint']['enabled']: raise APIError(_.GEO_LOCKED) return data def device_code(self): self.logout() serial = self._guest_login() payload = { 'model': DEVICE_MODEL, 'serialNumber': serial, 'userIntent': 'login', } data = self._session.post(self.url('comet', '/devices/activationCode'), json=payload).json() self._check_errors(data) 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) except NotPairedError: return False else: return True def _check_errors(self, data, error=_.API_ERROR): if not data: raise APIError(_.BLOCKED_IP) if 'code' in data: if data['code'] == 'not_paired': raise NotPairedError() else: 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(self.url('comet', '/content'), json=payload).json()[0]['body']['profiles'] 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() headwaiter = '' for key in sorted(self._client_config()['payloadValues']): headwaiter += '{}:{},'.format( key, self._client_config()['payloadValues'][key]) headers = { 'x-hbo-headwaiter': headwaiter.rstrip(','), } params = { 'device-code': DEVICE_MODEL, 'product-code': 'hboMax', 'api-version': 'v9.0', 'brand': 'HBO MAX', 'country-code': 'US', 'profile-type': 'default', # 'territory': 'MEXICO', 'signed-in': True, } if userdata.get('profile', {}).get('child', 0): params.update({ 'profile-type': 'child', 'age-category': self._age_category(), }) data = self._session.get(self.url('comet', '/express-content/{}'.format(slug)), params=params, headers=headers).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] if len(data) == 1 and 'message' in main: raise APIError(_(_.API_ERROR, msg=main['message'])) 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(self.url('comet', '/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(self.url('comet', '/content'), json=payload).json()[0]['body'] for row in data.get('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') userdata.delete('config') mem_cache.empty() self.new_session()
class API(object): def new_session(self, config): self.logged_in = False self._config = config self._session = Session(base_url=self._config.api_url, headers=HEADERS) self._set_authentication() def _set_authentication(self): auth_cookies = userdata.get('auth_cookies') if not auth_cookies: return self._session.cookies.update(auth_cookies) self.logged_in = True def _device_id(self): device_id = userdata.get('device_id') if device_id: return device_id device_id = settings.get('device_id') try: mac_address = uuid.getnode() if mac_address != uuid.getnode(): mac_address = '' except: mac_address = '' system, arch = get_system_arch() device_id = device_id.format(username=userdata.get('username'), mac_address=mac_address, system=system).strip() if not device_id: device_id = uuid.uuid4() log.debug('Raw device id: {}'.format(device_id)) device_id = hash_6(device_id, length=16) log.debug('Hashed device id: {}'.format(device_id)) userdata.set('device_id', device_id) return device_id def device_code(self): payload = {'deviceId': self._device_id()} return self._session.post('/v2.0/androidtv/ott/auth/code.json', params=self._params(), data=payload).json() 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') self._set_profile(userdata.get('profile_id')) def login(self, username, password): payload = { 'j_password': password, 'j_username': username, 'deviceId': self._device_id(), } resp = self._session.post('/v2.0/androidtv/auth/login.json', params=self._params(), data=payload) data = resp.json() if not data['success']: raise APIError(data.get('message')) self._save_auth(resp.cookies) self._set_profile_data(self.user()['activeProfile']) def device_login(self, code, device_token): payload = { 'activationCode': code, 'deviceToken': device_token, 'deviceId': self._device_id(), } resp = self._session.post('/v2.0/androidtv/ott/auth/status.json', params=self._params(), data=payload) data = resp.json() if data.get('regenerateCode'): return -1 elif not data['success']: return False self._save_auth(resp.cookies) self._set_profile_data(self.user()['activeProfile']) return True def _save_auth(self, cookies): expires = None for cookie in cookies: if expires is None or cookie.expires < expires: expires = cookie.expires userdata.set('expires', min(expires, int(time() + 86400))) userdata.set('auth_cookies', cookies.get_dict()) self._set_authentication() def set_profile(self, profile_id): self._set_profile(profile_id) mem_cache.empty() def _set_profile(self, profile_id): resp = self._session.post( '/v2.0/androidtv/user/account/profile/switch/{}.json'.format( profile_id), params=self._params()) data = resp.json() if not data['success']: raise APIError('Failed to set profile: {}'.format(profile_id)) self._set_profile_data(data['profile']) self._save_auth(resp.cookies) def _set_profile_data(self, profile): userdata.set('profile_id', profile['id']) userdata.set('profile_name', profile['name']) userdata.set('profile_img', profile['profilePicPath']) def _params(self, params=None): _params = {'locale': 'en-us', 'at': self._config.tv_token} #_params = {'locale': 'en-us', 'at': self._at_token(secret), 'LOCATEMEIN': 'us'} if params: _params.update(params) return _params # def _at_token(self, secret): # payload = '{}|{}'.format(int(time())*1000, self._config.tv_secret) # try: # #python3 # key = bytes.fromhex(self._config.aes_key) # except AttributeError: # #python2 # key = str(bytearray.fromhex(self._config.aes_key)) # iv = os.urandom(16) # encrypter = pyaes.Encrypter(pyaes.AESModeOfOperationCBC(key, iv)) # ciphertext = encrypter.feed(payload) # ciphertext += encrypter.feed() # ciphertext = b'\x00\x10' + iv + ciphertext # return b64encode(ciphertext).decode('utf8') # def app_config(self): # selected_region = None # for region in REGIONS: # params = {'locale': 'en-us', 'at': region['tv_token']} # resp = self._session.get('{}/apps-api/v2.0/androidtv/app/status.json'.format(region['base_url']), params=params) # if resp.ok: # data = resp.json() # if data['appVersion']['availableInRegion']: # selected_region = [region, data] # break # if not selected_region: # raise Exception('Unable to find a region for your location') @mem_cache.cached(60 * 10) def carousel(self, url, params=None): params = params or {} params.update({ '_clientRegion': self._config.country_code, 'start': 0, }) for key in params: if type(params[key]) is list: params[key] = ','.join(params[key]) return self._session.get( '/v3.0/androidphone{}'.format(url), params=self._params(params)).json()['carousel'] @mem_cache.cached(60 * 10) def featured(self): params = { 'minProximity': 1, 'minCarouselItems': 5, 'maxCarouselItems': 20, 'rows': 15, } return self._session.get('/v3.0/androidphone/home/configurator.json', params=self._params(params)).json()['config'] @mem_cache.cached(60 * 10) def trending_movies(self): return self._session.get('/v3.0/androidphone/movies/trending.json', params=self._params()).json() @mem_cache.cached(60 * 10) def movies(self, genre=None, num_results=12, page=1): params = { 'includeTrailerInfo': False, 'packageCode': 'CBS_ALL_ACCESS_AD_FREE_PACKAGE', 'platformType': 'androidphone', 'start': (page - 1) * num_results, 'rows': num_results, 'includeContentInfo': True, } if genre: params['genre'] = genre return self._session.get('/v3.0/androidphone/movies.json', params=self._params(params)).json() @mem_cache.cached(60 * 10) def movie_genres(self): return self._session.get('/v3.0/androidphone/movies/genre.json', params=self._params()).json()['genres'] @mem_cache.cached(60 * 10) def show_groups(self): params = {'includeAllShowGroups': 'true'} return self._session.get( '/v2.0/androidphone/shows/groups.json', params=self._params(params)).json()['showGroups'] @mem_cache.cached(60 * 10) def show_group(self, group_id): params = {'includeAllShowGroups': 'true'} return self._session.get( '/v2.0/androidphone/shows/group/{}.json'.format(group_id), params=self._params(params)).json()['group'] @mem_cache.cached(60 * 10) def show(self, show_id): return self._session.get( '/v3.0/androidphone/shows/{}.json'.format(show_id), params=self._params()).json() @mem_cache.cached(60 * 10) def episodes(self, show_id, season): params = { 'platformType': 'apps', 'rows': 1, 'begin': 0, } section_id = self._session.get( '/v2.0/androidphone/shows/{}/videos/config/{}.json'.format( show_id, self._config.episodes_section), params=self._params( params)).json()['section_display_seasons'][0]['sectionId'] params = { 'rows': 999, 'params': 'seasonNum={}'.format(season), 'begin': 0, 'seasonNum': season, } return self._session.get( '/v2.0/androidphone/videos/section/{}.json'.format(section_id), params=self._params(params)).json()['sectionItems']['itemList'] @mem_cache.cached(60 * 10) def seasons(self, show_id): return self._session.get( '/v3.0/androidphone/shows/{}/video/season/availability.json'. format(show_id), params=self._params()).json()['video_available_season']['itemList'] @mem_cache.cached(60 * 10) def search(self, query): params = { 'term': query, 'termCount': 50, 'showCanVids': 'true', } return self._session.get( '/v3.0/androidphone/contentsearch/search.json', params=self._params(params)).json()['terms'] def user(self): self._refresh_token() return self._session.get('/v3.0/androidtv/login/status.json', params=self._params()).json() def play(self, video_id): self._refresh_token() def get_data(device): video_data = self._session.get( '/v2.0/{}/video/cid/{}.json'.format(device, video_id), params=self._params()).json()['itemList'][0] if 'pid' not in video_data: raise APIError('Check your subscription is valid') params = { #'formats': 'mpeg-dash', 'Tracking': 'true', 'format': 'SMIL', #'sig': '0060cbe3920bcb86969e8c733a9cdcdb203d6e57beae30781c706f63', } resp = self._session.get(LINK_PLATFORM_URL.format( account=video_data['cmsAccountId'], pid=video_data['pid']), params=params) root = ET.fromstring(resp.text) strip_namespaces(root) if root.find("./body/seq/ref/param[@name='exception']") != None: error_msg = root.find("./body/seq/ref").attrib.get('abstract') raise APIError(_(error_msg)) ref = root.find(".//video") return ref.attrib['src'], video_data device = 'androidtv' url, video_data = get_data(device) if 'cenc_fmp4_dash' in url and not settings.getBool( 'wv_secure', False): try: split = url.split('/') year = int(split[4]) month = int(split[5]) except: year = 2021 month = 6 if year >= 2021 and month >= 6: device = 'androidphone' url, video_data = get_data(device) params = {'contentId': video_id} data = self._session.get( '/v3.0/{}/irdeto-control/session-token.json'.format(device), params=self._params(params)).json() return url, data['url'], data['ls_session'], video_data def _ip(self): return self._session.get(self._config.ip_url, params=self._params()).json()['ip'] def live_channels(self): if not self._config.has_live_tv: return [] self._refresh_token() dma = self.dma() params = { 'start': 0, 'rows': 30, '_clientRegion': self._config.country_code, 'dma': dma['dma'] if dma else None, 'showListing': 'true', } data = self._session.get('/v3.0/androidphone/live/channels.json', params=self._params(params)).json() channels = [] for row in data['channels']: if row['dma'] and dma: row['dma'] = dma['tokenDetails'] channels.append(row) return sorted(channels, key=lambda x: x['displayOrder']) def epg(self, channel, page=1, rows=25): params = { 'start': (page - 1) * rows, 'rows': rows, '_clientRegion': self._config.country_code, 'showListing': 'true', } return self._session.get( '/v3.0/androidphone/live/channels/{slug}/listings.json'.format( slug=channel), params=self._params(params)).json()['listing'] def dma(self): self._refresh_token() ip = settings.get('region_ip') if not ip or ip == '0.0.0.0': ip = self._ip() params = { 'ipaddress': ip, 'dtp': 8, #controls quality 'syncBackVersion': '3.0', 'mvpdId': 'AllAccess', 'is60FPS': 'true', 'did': self._device_id(), } data = self._session.get('/v3.0/androidphone/dma.json', params=self._params(params)).json() if not data['success']: log.warning( 'Failed to get local CBS channel for IP address ({}). Server message: {}' .format(ip, data.get('message'))) return None return data['dmas'][0] def logout(self): userdata.delete('profile_img') userdata.delete('profile_name') userdata.delete('profile_id') userdata.delete('auth_cookies') userdata.delete('device_id') mem_cache.empty() self.new_session(self._config)
class API(object): def new_session(self): self.logged_in = False self._session = Session(HEADERS) self._set_authentication() def _set_authentication(self): token = userdata.get('access_token') if not token: return self._session.headers.update( {'authorization': 'Bearer {}'.format(token)}) self._session.headers.update( {'x-user-profile': userdata.get('profile_id')}) self.logged_in = True def _query_request(self, query, variables=None, **kwargs): self._refresh_token() data = { 'query': ' '.join(query.split()), 'variables': variables or {}, } data = self._session.post(GRAPH_URL, json=data, **kwargs).json() if 'errors' in data: raise APIError(data['errors'][0]['message']) return data 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 profile email offline_access', } self._oauth_token(payload) def _oauth_token(self, payload): token_data = self._session.post('https://login.sky.co.nz/oauth/token', json=payload, error_msg=_.TOKEN_ERROR).json() if 'error' in token_data: self.logout() error = _.REFRESH_TOKEN_ERROR if token_data.get( 'grant_type') == 'refresh_token' else _.LOGIN_ERROR raise APIError(_(error, msg=token_data.get('error_description'))) 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']) #Force 1st profile data = jwt_data(token_data['access_token']) profile_id = data['https://skygo.co.nz/profiles'][0]['id'] userdata.set('profile_id', profile_id) #### self._set_authentication() @mem_cache.cached(60 * 5) def home(self): return self._query_request(queries.HOME)['data']['section']['home'] @mem_cache.cached(60 * 5) def group(self, id): return self._query_request(queries.GROUP, variables={'railId': id})['data']['group'] @mem_cache.cached(60 * 5) def channels(self): ids = [] channels = [] variables = { 'from': arrow.utcnow().format('YYYY-MM-DDTHH:mm:ss.000') + 'Z', 'to': arrow.utcnow().shift(hours=6).format('YYYY-MM-DDTHH:mm:ss.000') + 'Z', } groups = self._query_request( queries.CHANNELS, variables=variables)['data']['linearChannelGroups'] for group in groups: for row in group.get('channels', []): if row['__typename'] == 'LinearChannel' and row[ 'id'] not in ids: ids.append(row['id']) channels.append(row) return sorted(channels, key=lambda x: x['number']) @mem_cache.cached(60 * 5) def search(self, query): return self._query_request(queries.SEARCH, variables={'term': query })['data']['search']['results'] @mem_cache.cached(60 * 5) def show(self, id): return self._query_request(queries.SHOW, variables={'brandId': id})['data']['show'] @mem_cache.cached(60 * 5) def collection(self, collection_id, filters="", limit=36, after=None, sort="ALPHABETICAL", tv_upcoming=False): context = 'VOD,CATCHUP' if tv_upcoming: context += ',LINEAR' filters = 'namedFilters: "{}"'.format(filters) if filters else '' limit = 'first: {}'.format(limit) if limit else '' sort = 'sort: {}'.format(sort) if sort else '' after = 'after: "{}"'.format(after) if after else '' query = queries.COLLECTION % (after, limit, context, filters, sort) return self._query_request(query, variables={'collectionId': collection_id })['data']['collection'] @mem_cache.cached(60 * 10) def vod_categories(self): return self._query_request( queries.VOD_CATEGORIES, variables={'excludeViewingContexts': [ 'DOWNLOAD', ]})['data']['section']['home']['categories'] def play(self, asset_id, is_linear=False): variables = { 'deviceId': '', 'assetId': asset_id, 'channelId': asset_id, # 'playbackDevice': { # 'platform': 'Windows', # 'osVersion': '10', # 'drmType': 'WIDEVINE', # 'drmLevel': 'SW_SECURE_DECODE' # } } if is_linear: data = self._query_request( queries.LINEAR_START, variables)['data']['startLinearPlayback'] else: data = self._query_request(queries.VOD_START, variables)['data']['startVodPlayback'] if data['__typename'] == 'SubscriptionNeeded': raise APIError( _(_.SUBSCRIPTION_REQUIRED, subscription=data['subscriptions'][0]['title'])) elif data['__typename'] == 'Geoblocked': raise APIError(_.GEO_ERROR) elif data['__typename'] == 'ConcurrentStreamsExceeded': raise APIError(_.CONCURRENT_STREAMS) elif data['__typename'] not in ('LinearPlaybackSources', 'VodPlaybackSources'): raise APIError('Unkown error: {}'.format(data['__typename'])) try: if is_linear: self._query_request(queries.LINEAR_STOP, variables)['data']['stopLinearPlayback'] else: self._query_request(queries.VOD_STOP, variables)['data']['stopVodPlayback'] except: log.debug('Stop Linear / VOD Failed') return data['playbackSource']['streamUri'], data['playbackSource'][ 'drmLicense']['licenseUri'] def login(self, username, password): self.logout() params = { 'client_id': CLIENT_ID, 'audience': 'https://api.sky.co.nz', 'redirect_uri': 'https://www.skygo.co.nz', 'connection': 'Sky-Internal-Connection', 'scope': 'openid profile email offline_access', 'response_type': 'code', } resp = self._session.get('https://login.sky.co.nz/authorize', params=params, allow_redirects=False) parsed = urlparse(resp.headers['location']) payload = dict(parse_qsl(parsed.query)) payload.update({ 'username': username, 'password': password, 'tenant': 'skynz-prod', 'client_id': CLIENT_ID, 'client': None, }) resp = self._session.post( 'https://login.sky.co.nz/usernamepassword/login', json=payload) if not resp.ok: data = resp.json() raise APIError(_(_.LOGIN_ERROR, msg=data['message'])) soup = BeautifulSoup(resp.text, 'html.parser') payload = {} for e in soup.find_all('input'): if 'name' in e.attrs: payload[e.attrs['name']] = e.attrs.get('value') resp = self._session.post('https://login.sky.co.nz/login/callback', data=payload) parsed = urlparse(resp.url) data = dict(parse_qsl(parsed.query)) payload = { 'code': data['code'], 'client_id': CLIENT_ID, 'grant_type': 'authorization_code', 'redirect_uri': 'https://www.skygo.co.nz' } self._oauth_token(payload) def logout(self): userdata.delete('access_token') userdata.delete('refresh_token') userdata.delete('expires') userdata.delete('profile_id') 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): access_token = userdata.get('token') if not access_token: return self._session.headers.update({'X-Auth-Token': access_token}) self.logged_in = True def login(self, username, password): self.logout() payload = { 'email': username, 'password': password, 'platform': 'google', } data = self._session.post('/v1/login/', data=payload).json() if 'error' in data: try: msg = data['error']['message']['base'][0] except: msg = '' raise APIError(_(_.LOGIN_ERROR, msg=msg)) userdata.set('token', data['message']['auth_token']) self._set_authentication() @mem_cache.cached(CACHE_TIME) def categories(self): return self._session.get('/v1/categories').json()['data'] def series(self, id): return self._session.get('/v2/series/{}'.format(id)).json()['data'] @mem_cache.cached(CACHE_TIME) def featured(self): return self._session.get('/v2/featured').json() def sections(self, id, page=1): params = { 'cache': False, 'collections': True, 'media_limit': 36, 'page': page, } return self._session.get( '/v1/sections/{}/mobile'.format(id)).json()['data']['groups'] def collection(self, id, flattened=False): params = { 'flattened': flattened, } return self._session.get('/v2/collections/{}'.format(id), params=params).json()['data'] @mem_cache.cached(CACHE_TIME) def collections(self, flattened=False, excludeMedia=True, page=1): params = { 'flattened': flattened, 'excludeMedia': excludeMedia, 'limit': 20, 'page': page, } return self._session.get('/v2/collections', params=params).json() def filter_media(self, filterby, term=None, collections=True, page=1): params = { 'filterBy': filterby, 'collections': collections, 'limit': 20, 'page': page, } if term: params['term'] = term return self._session.get('/v1/media', params=params).json() def set_user_media(self, id, **kwargs): params = { 'media_id': id, } params.update(kwargs) data = self._session.post('/v1/user_media', params=params, json={}).json() return data['status'] == 'success' def set_user_collection(self, id, **kwargs): params = { 'collection_id': id, } params.update(kwargs) data = self._session.post('/v1/user_collection', params=params, json={}).json() return data['status'] == 'success' def media(self, id): params = { # 'showEncodings': 'Android', #limits to 1080p 'encodingsNew': 'true', 'encodingsFormat': 'mpd', } return self._session.get('/v1/media/{}'.format(id), params=params).json()['data'] def logout(self): userdata.delete('token') mem_cache.empty() self.new_session()
class API(object): def new_session(self): self.logged_in = False self._auth_headers = {} self._config = {} self._session = Session(headers=HEADERS) self._session.after_request = self._check_response self._set_authentication() @mem_cache.cached(60*60, key='config') def get_config(self): try: return self._session.get(APP_DATA_URL).json()['settings'] except Exception as e: raise APIError('Failed to download app settings') @mem_cache.cached(60*60) def _app_version(self): return self._session.get(APP_VERSION_URL).text.strip() def _set_authentication(self): device_token = userdata.get('device_token') auth_token = userdata.get('auth_token') if not device_token or not auth_token: return self._auth_headers = { 'X-AN-WebService-CustomerAuthToken': auth_token, 'X-AN-WebService-DeviceAuthToken': device_token, } self.logged_in = True def _create_session(self, force=False): self._config = self.get_config() platform = self._config['alpha_networks_dash'][REGION] self._session._base_url = platform['platform_url']+'{}' self._session.headers.update({ 'X-AN-WebService-IdentityKey': platform['hss_key'], # hss_key, hls_key, chromecast_key }) if not self.logged_in or (not force and time.time() < userdata.get('token_expires')): return login_type = settings.getEnum('login_type', choices=LOGIN_TYPE, default=LOGIN_MULTI_IP) if login_type == LOGIN_MULTI_IP: # Single device, changing IP address (same as app) data = self._session.post('proxy/loginDevice', headers=self._auth_headers).json() elif login_type == LOGIN_MULTI_DEVICE: # Multiple device, static IP address data = self._session.post('proxy/casAvailableDevice', headers=self._auth_headers).json() elif login_type == LOGIN_PASSWORD: # Supports multiple devices and multiple IP address as long (as others also using password) data = { 'password': userdata.get('password'), 'deviceId': userdata.get('device_id'), 'email': userdata.get('username'), } data = self._session.post('proxy/login', data=data).json() if data['error']: error = _(_.TOKEN_ERROR, msg=data['error']['message']) if data['error']['code'] == -1: self.logout() gui.refresh() if login_type == LOGIN_MULTI_IP: error = _.LOGIN_MULTI_IP_ERROR elif login_type == LOGIN_MULTI_DEVICE: error = _.LOGIN_MULTI_DEVICE_ERROR raise APIError(error) if 'deviceAuthToken' in data['result']: userdata.set('device_token', data['result']['deviceAuthToken']) self._set_auth(data['result']['newAuthToken']) def _set_auth(self, auth_token): token_data = jwt_data(auth_token) userdata.set('auth_token', auth_token) userdata.set('token_expires', token_data['exp'] - 30) self._set_authentication() def _select_device(self, token): data = self._session.post('proxy/casAvailableDevice', headers={'X-AN-WebService-CustomerAuthToken': token}).json() devices = data['result'].get('device', []) while True: if devices: options = [] values = [] for row in devices: options.append(_(_.DEVICE_LABEL, name=row['name'], last_login=arrow.get(row['lastLoginDate']).to('local').format('D MMMM YYYY'))) values.append(row) options.append(_.NEW_DEVICE) values.append('new') options.append(_.REMOVE_DEVICE) values.append('remove') index = gui.select(_.SELECT_DEVICE, options=options) if index == -1: return selected = values[index] else: selected = 'new' if selected == 'new': device_name = gui.input(_.DEVICE_NAME).strip() if not device_name or not gui.yes_no(_(_.NEW_CONFIRM, device_name=device_name)): if devices: continue else: return return { 'uniqueDeviceId': hash_6('{}{}'.format(int(time.time()), device_name), length=16), 'name': device_name, 'type': 'Android', } elif selected == 'remove': options = [] values = [] for row in devices: options.append(row['name']) values.append(row) to_remove = None while not to_remove: index = gui.select(_.SELECT_REMOVE_DEVICE, options=options) if index == -1: break if gui.yes_no(_(_.REMOVE_CONFIRM, device_name=values[index]['name'])): to_remove = values[index] if not to_remove: continue data = { 'casDeviceId': to_remove['uniqueDeviceId'], } data = self._session.post('proxy/casRemoveDevice', data=data, headers={'X-AN-WebService-CustomerAuthToken': token}).json() if data['error']: gui.error(data['error']['message']) continue return self._select_device(data['result']['newAuthToken']) else: return selected def _check_response(self, resp): if resp.ok: return if resp.status_code == 451: raise APIError(_.GEO_BLOCKED) else: raise APIError(_(_.HTTP_ERROR, code=resp.status_code)) def login(self, username, password): self.logout() self._create_session() data = { 'password': password, 'email': username, } data = self._session.post('proxy/login', data=data).json() if data['error']: raise APIError(_(_.LOGIN_ERROR, msg=data['error']['message'])) auth_token = data['result']['newAuthToken'] while True: selected = self._select_device(auth_token) if not selected: return data = { 'password': password, 'deviceId': selected['uniqueDeviceId'], 'email': username, } data = self._session.post('proxy/login', data=data).json() if data['error']: gui.error(data['error']['message']) else: break auth_token = data['result']['newAuthToken'] device_token = data['result']['deviceAuthToken'] userdata.set('device_token', device_token) data = { 'name': selected['name'], 'casDeviceId': selected['uniqueDeviceId'], 'type': selected['type'], } data = self._session.post('proxy/casAuth', data=data, headers={'X-AN-WebService-CustomerAuthToken': auth_token, 'X-AN-WebService-DeviceAuthToken': device_token}).json() if data['error']: raise APIError(data['error']['message']) self._set_auth(data['result']['newAuthToken']) mem_cache.delete('channels') if settings.getEnum('login_type', choices=LOGIN_TYPE, default=LOGIN_MULTI_IP) == LOGIN_PASSWORD: userdata.set('password', password) userdata.set('device_id', selected['uniqueDeviceId']) @mem_cache.cached(60*10, key='channels') def channels(self): self._create_session() _channels = {} for row in self._config['channels_mapping'][REGION]: if 'sso_hls_channel_id' in row: _channels[row['sso_hls_channel_id']] = row channels = [] for row in self._session.post('proxy/listChannels').json()['result']['channels']: row.update(_channels.get(row['idChannel'], {})) channels.append(row) return sorted(channels, key=lambda x: x.get('sorting', x['localizeNumber'])) def license_request(self, channel_id, challenge): self._create_session() app_version = self._app_version() params = { 'wskey': self._session.headers['X-AN-WebService-IdentityKey'], 'playerName': PLAYER_NAME, 'playerVersion': app_version, 'checksum': self._checksum(channel_id, app_version), 'idChannel': channel_id, } resp = self._session.post('arkena/askLicenseWV', params=params, data=challenge, headers=self._auth_headers) if not resp.ok or '<LICENSE>' not in resp.text: raise APIError('Failed to get Widevine license') return resp.text.split('</LICENSE>')[0].split('<LICENSE>')[1] def play(self, channel_id): self._create_session(force=True) app_version = self._app_version() payload = { 'idChannel': channel_id, 'playerName': PLAYER_NAME, 'playerVersion': app_version, 'checksum': self._checksum(channel_id, app_version), 'languageId': 'eng', 'authToken': userdata.get('auth_token'), } data = self._session.post('proxy/channelStream', data=payload, headers=self._auth_headers).json() if data.get('error'): raise APIError(data['error']['message']) if 'newAuthToken' in data['result']: self._set_auth(data['result']['newAuthToken']) return data['result']['url'] def epg(self, ids, start, end): self._create_session() data = { '$and': [ {'id_channel': {'$in': ids}}, {'startutc': {'$ge': start.timestamp}}, {'startutc': {'$lt': end.timestamp}}, ] } params = { 'languageId': 'eng', 'filter': json.dumps(data, separators=(',', ':')), } data = self._session.get('cms/epg/filtered', params=params).json() if data['error']: raise APIError(data['error']['message']) return data['result']['epg'] def _checksum(self, channel_id, app_version): checksum = userdata.get('auth_token', '') + str(channel_id) + app_version checksum = hmac.new(SECRET_KEY.encode('utf8'), msg=checksum.encode('utf8'), digestmod=hashlib.sha256).digest() return base64.b64encode(checksum).decode('utf8') def logout(self): userdata.delete('password') userdata.delete('device_id') userdata.delete('device_token') userdata.delete('auth_token') userdata.delete('token_expires') mem_cache.delete('config') mem_cache.delete('channels') self.new_session()
class API(object): def new_session(self): self._session = Session(HEADERS, base_url=API_URL) self.logged_in = userdata.get('nllinktoken') != None def login(self, username, password): self.logout() data = { 'username': username, 'password': password, 'cookielink': 'true', 'format': 'json', } r = self._session.post('/secure/authenticate', data=data) data = r.json() nllinktoken = r.cookies.get_dict().pop('nllinktoken', None) code = data.get('code') or _.UNKNOWN_ERROR if code != 'loginsuccess' or not nllinktoken: if code == 'failedgeo': raise APIError(_.GEO_ERROR) else: raise APIError(_(_.LOGIN_ERROR, code=code)) userdata.set('nllinktoken', nllinktoken) self.new_session() def deviceid(self): return hashlib.sha1( userdata.get('username').encode('utf8')).hexdigest()[:8] def play(self, media_id, media_type, start=None, duration=None): payload = { 'id': media_id, 'nt': 1, 'type': media_type, 'format': 'json', 'drmtoken': True, 'deviceid': self.deviceid(), } if start: payload['st'] = '{}000'.format(start) if duration: payload['dur'] = '{}000'.format(duration) login_cookies = { 'nllinktoken': userdata.get('nllinktoken'), 'UserName': userdata.get('username') } data = self._session.post('/service/publishpoint', data=payload, cookies=login_cookies).json() if 'path' not in data: code = data.get('code') or _.UNKNOWN_ERROR if code == 'failedgeo': raise APIError(_.GEO_ERROR) else: raise APIError(_(_.PLAYBACK_ERROR, code=code)) return data def schedule(self, date): schedule = [] data = self._session.get( EPG_URL.format(date=date.format('YYYY/MM/DD'))).json() for channel in data: for event in channel['items']: start = arrow.get(event['su']) stop = start.shift(seconds=event['ds']) duration = int(event['ds']) title = event.get('e') desc = event.get('ed') schedule.append({ 'channel': channel['channelId'], 'start': start, 'stop': stop, 'duration': duration, 'title': title, 'desc': desc }) return sorted(schedule, key=lambda channel: channel['start']) def logout(self): # self._session.post('service/logout', data={'format': 'json'}) userdata.delete('nllinktoken') self.new_session() def highlights(self, page=1, pagesize=100): params = { 'type': '0', 'format': 'json', 'ps': pagesize, 'pn': page, } data = self._session.get('/service/search', params=params).json() try: code = data.get('code') except: code = None if code: if code == 'failedgeo': raise APIError(_.GEO_ERROR) else: raise APIError( _(_.PLAYBACK_ERROR, code=code or _.UNKNOWN_ERROR)) return data def channels(self): params = { 'format': 'json', } data = self._session.get('/channels', params=params).json() try: code = data.get('code') except: code = None if code: if code == 'failedgeo': raise APIError(_.GEO_ERROR) else: raise APIError( _(_.PLAYBACK_ERROR, code=code or _.UNKNOWN_ERROR)) return data
class API(object): def __init__(self): self.new_session() def new_session(self): self._logged_in = False self._session = Session(HEADERS, base_url=API_URL, timeout=TIMEOUT) self._set_access_token(userdata.get('access_token')) def _set_access_token(self, token): if token: self._session.headers.update({'Authorization': token}) self._logged_in = True profile_id = userdata.get('profile') if profile_id: self._session.headers.update({'x-profile-id': profile_id}) @property def logged_in(self): return self._logged_in def _parse_tokens(self, access_token, id_token): jwt = jwt_data(access_token) userdata.set('access_token', access_token) userdata.set('id_token', id_token) userdata.set('token_expires', int(time.time()) + (jwt['exp'] - jwt['iat'] - 30)) userdata.set('country', jwt['country']) userdata.set('package', jwt['package']) self._set_access_token(access_token) def _refresh_token(self, force=False): if not self._logged_in: raise APIError(_.PLUGIN_LOGIN_REQUIRED) expires = userdata.get('token_expires', 0) if not force and expires > time.time(): return payload = { 'idToken': userdata.get('id_token'), 'accessToken': userdata.get('access_token'), } data = self._session.post(REFRESH_TOKEN_URL, json=payload).json() if not data.get('accessToken') or not data.get('idToken'): raise APIError(_(_.REFRESH_TOKEN_ERROR, msg=data.get('reason'))) self._parse_tokens(data['accessToken'], data['idToken']) def profiles(self): return self._request_json('v6/profiles')['items'] def _get_auth(self): data = self._request_json( 'user-manager/v5/vod-authorisation;productId={product};platformId={platform};deviceType={device_type};' .format(product=PRODUCT_ID, platform=PLATFORM_ID, device_type=DEVICE_TYPE), type='post') return data def _request_json(self, url, type='get', timeout=30, attempts=3, refresh_token=True, **kwargs): if refresh_token: self._refresh_token(force=refresh_token == 'force') data = {} for i in range(attempts): if i > 0: log.debug("Try {}/{}".format(i + 1, attempts)) r = self._session.request(type, url, timeout=timeout, attempts=1, **kwargs) try: data = r.json() except: continue if 'errorCode' in data: break if r.ok: return data elif not str(r.status_code).startswith('5'): break if 'errorMessage' in data: raise APIError(_(_.API_ERROR, msg=data['errorMessage'])) elif 'reason' in data: raise APIError(_(_.API_ERROR, msg=data['reason'])) else: raise APIError( _(_.REQUEST_ERROR, url=url.split(';')[0], code=r.status_code)) def content(self, tags, sort, category='', page=0, pagesize=24): category = 'filter={};'.format(category) if category else '' data = self._request_json( 'now-content/v7/catalogueByPackageAndCountry;productId={product};platformId={platform};tags={tags};subscriptionPackage={package};country={country};{category}sort={sort};page={page};pageSize={pagesize}' .format( product=PRODUCT_ID, platform=PLATFORM_ID, tags=tags, country=userdata.get('country', DEFAULT_COUNTRY), package=userdata.get('package', DEFAULT_PACKAGE), sort=sort, category=category, page=page, pagesize=pagesize, )) return data def search(self, query): data = self._request_json( 'now-content/v6/search;platformId={platform};searchTerm={query}'. format(platform=PLATFORM_ID, query=query)) return data['items'][0]['editorialItems'] def channels(self, events=2): data = self._request_json( 'v7/epg-service/channels/events;genre={genre};platformId={platform};country={country};packageId={package};count={events};utcOffset=+00:00' .format(genre='ALL', platform=PLATFORM_ID, country=userdata.get('country', DEFAULT_COUNTRY), package=userdata.get('package', DEFAULT_PACKAGE), events=events)) return data['items'] def epg(self, tag, start_date, end_date=None, attempts=3): end_date = end_date or start_date.shift(hours=24) for i in range(attempts): try: data = self._request_json( 'epg/v7/getEpgSchedulesByTag;channelTags={tag};startDate={start};endDate={end}' .format(tag=tag, start=start_date.format('YYYY-MM-DDT00:00:00ZZ'), end=end_date.format('YYYY-MM-DDT00:00:00ZZ')), attempts=1) except: continue if len(data.get('items', 0)) > 0: break return data['items'] def series(self, id): data = self._request_json( 'now-content/v6/getCatalogue;productId={product};platformId={platform};programId={id}' .format(product=PRODUCT_ID, platform=PLATFORM_ID, id=id)) return data['items'][0]['program'] def get_video(self, id): data = self._request_json( 'now-content/v6/getCatalogue;productId={product};platformId={platform};videoId={id}' .format(product=PRODUCT_ID, platform=PLATFORM_ID, id=id)) return data['items'][0]['video'] def get_channel(self, id): for channel in self.channels(): if channel['id'] == id: return channel raise APIError(_(_.CHANNEL_NOT_FOUND, id=id)) def play_channel(self, id): channel = self.get_channel(id) stream_url = None for stream in channel['streams']: if stream['streamType'] in ('MobileAlt', 'WebAlt'): stream_url = stream['playerUrl'] break if not stream_url: raise APIError(_.STREAM_ERROR) parsed = urlparse(stream_url) content_id = parse_qs(parsed.query)['contentId'][0] return self.play_asset(stream_url, content_id) def play_video(self, id): video = self.get_video(id) if not video.get('videoAssets'): raise APIError(_.STREAM_ERROR) stream_url = video['videoAssets'][0]['url'] content_id = video['videoAssets'][0]['manItemId'] return self.play_asset(stream_url, content_id) def play_asset(self, stream_url, content_id): auth = self._get_auth() session = auth.get('irdetoSession') if '.isml' in stream_url: stream_url = stream_url.replace('.isml', '.isml/.mpd') elif '.ism' in stream_url: stream_url = stream_url.replace('.ism', '.ism/.mpd') if not session: raise APIError(data.get('errorMessage')) session_id = session['sessionId'] ticket = session['ticket'] license_url = LICENSE_URL.format(content_id, session_id, ticket) return stream_url, license_url, self._session.headers def _device_id(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')))) @contextmanager def device_login(self): device_id = self._device_id() payload = { 'deviceId': device_id, } data = self._request_json(DEVICE_REGISTER, type='post', json=payload, refresh_token=False) code = data['userCode'] log.debug('Device ID: {} | Device Code: {}'.format(device_id, code)) login = DeviceLogin(device_id, code) try: yield login finally: login.stop() if login.result: token_data = login.token_data() self._parse_tokens(token_data['accessToken'], token_data['idToken']) def logout(self): userdata.delete('access_token') userdata.delete('id_token') userdata.delete('token_expires') userdata.delete('country') userdata.delete('package') userdata.delete('profile') self.new_session()
class API(object): def new_session(self): self._session = Session(HEADERS) self.logged_in = userdata.get('userid') != None def login(self, username, password): self.logout() payload = { 'action': 'login', 'email': username, 'password': password, 'authToken': hashlib.md5('{}::{}::{}'.format( MD5_KEY, username, password).encode('utf8')).hexdigest(), } data = self._session.post(LOGIN_URL, data=payload).json() if data['result']['status'] != 'success': raise APIError(_(_.LOGIN_ERROR, msg=data['result'].get('message'))) userdata.set('userid', data['result']['userId']) def live_matches(self): return self._session.get(LIVE_URL).json() @cached(60 * 10) def played_series(self): return self._session.get(ARCHIVE_URL).json() @cached(60 * 10) def match(self, match_id): return self._session.get( ARCHIVES_MATCH_URL.format(match_id=match_id)).json() def upcoming_matches(self): return self._session.get(UPCOMING_URL).json() def get_series(self, series_id): data = self.played_series() for row in data['vod']: if row['sid'] == series_id: return row return None def play_live(self, match_id, priority): payload = { 'mid': match_id, 'type': 'live', 'devType': DEV_TYPE, 'pr': priority, 'wuid': userdata.get('userid'), } return self.play(payload) def play_replay(self, match_id, content_id): payload = { 'mid': match_id, 'type': 'replay', 'devType': DEV_TYPE, 'title': content_id, 'wuid': userdata.get('userid'), } return self.play(payload) def play_highlight(self, match_id, content_id): payload = { 'mid': match_id, 'type': 'highlight', 'devType': DEV_TYPE, 'id': content_id, 'wuid': userdata.get('userid'), } return self.play(payload) def play(self, payload): data = self._session.post(PLAYBACK_URL, data=payload).json() if 'error' in data: raise APIError(_(_.PLAYBACK_ERROR, msg=data['error'].get('title'))) elif 'subscribe' in data: raise APIError( _(_.SUBSCRIBE_ERROR, msg=data['subscribe'].get('description'))) elif not data.get('Videos'): raise APIError(_.NO_VIDEOS) return data['Videos'][0]['Url'] def logout(self): userdata.delete('userid') self.new_session()
class API(object): def new_session(self): self.logged_in = False ## Legacy ## userdata.delete('pswd') userdata.delete('access_token') ############ self._session = Session(HEADERS, base_url=API_URL) self._set_authentication() def _set_authentication(self): token = userdata.get('sky_token') if not token: return self._session.cookies.update({'sky-access': token}) self.logged_in = True def series(self, id): return self._session.get(CONTENT_URL + id).json() def content(self, section='', text='', genre='', channels='', start=0): params = { 'genre': genre, 'rating': '', 'text': text, 'sortBy': 'TITLE', 'title': '', 'lastChance': 'false', 'type': '', 'channel': channels, 'section': section, 'size': 200, 'start': start, } return self._session.get(CONTENT_URL, params=params).json() def channels(self): return self._session.get(CHANNELS_URL).json()['entries'] def login(self, username, password): session = self._session.get('/login/initSession').json() data = { 'authType': 'signIn', 'rememberMe': True, 'sessionJwt': session['sessionJwt'], 'username': username, 'password': password, } resp = self._session.post('/login/signin', json=data) self._process_login(resp) def _process_login(self, resp): data = resp.json() if not resp.ok or 'sky-access' not in resp.cookies: raise APIError(_(_.LOGIN_ERROR, message=data.get('message'))) token = resp.cookies['sky-access'] userdata.set('sky_token', token) userdata.set('device_id', data['deviceId']) if 'profileId' in data: userdata.set('profile_id', data['profileId']) jwt = jwt_data(token) userdata.set('token_expires', jwt['exp']) self._set_authentication() self._get_play_token() self._subscriptions() def _renew_token(self): if time.time() < userdata.get('token_expires'): return data = { 'authType': 'renew', 'deviceID': userdata.get('device_id'), 'profileId': userdata.get('profile_id'), 'rememberMe': True, } resp = self._session.post('/login/renew', json=data) self._process_login(resp) def _subscriptions(self): data = self._session.get('/entitlements/v2/onlineSubscriptions', params={ 'profileId': userdata.get('profile_id') }).json() userdata.set('subscriptions', data['onlineSubscriptions']) def _get_play_token(self): params = { 'profileId': userdata.get('profile_id'), 'deviceId': userdata.get('device_id'), 'partnerId': 'skygo', 'description': 'undefined undefined undefined', } resp = self._session.get('/mpx/v1/token', params=params) data = resp.json() if not resp.ok or 'token' not in data: raise APIError(_(_.TOKEN_ERROR, message=data.get('message'))) userdata.set('play_token', data['token']) def _concurrency_unlock(self, root): concurrency_url = root.find( "./head/meta[@name='concurrencyServiceUrl']").attrib['content'] lock_id = root.find("./head/meta[@name='lockId']").attrib['content'] lock_token = root.find( "./head/meta[@name='lockSequenceToken']").attrib['content'] lock = root.find("./head/meta[@name='lock']").attrib['content'] params = { 'schema': '1.0', 'form': 'JSON', '_clientId': 'playerplayerHTML', '_id': lock_id, '_sequenceToken': lock_token, '_encryptedLock': lock, 'httpError': False, } return self._session.get( '{}/web/Concurrency/unlock'.format(concurrency_url), params=params).json() def play_media(self, id): self._renew_token() params = { 'form': 'json', 'types': None, 'fields': 'id,content', 'byId': id, } data = self._session.get(PLAY_URL, params=params).json() if not data['entries']: raise APIError(_.VIDEO_UNAVAILABLE) videos = data['entries'][0]['media$content'] chosen = videos[0] for video in videos: if video['plfile$format'].upper() == 'MPEG-DASH': chosen = video break if chosen['plfile$format'].upper() == 'F4M': raise APIError(_.ADOBE_ERROR) params = { 'auth': userdata.get('play_token'), 'formats': 'mpeg-dash', 'tracking': True, 'format': 'SMIL' } resp = self._session.get(chosen['plfile$url'], params=params) root = ET.fromstring(resp.text) strip_namespaces(root) if root.find("./body/seq/ref/param[@name='exception']") != None: error_msg = root.find("./body/seq/ref").attrib.get('abstract') raise APIError(_(_.PLAY_ERROR, message=error_msg)) try: data = self._concurrency_unlock(root) except Exception as e: log.debug( 'Failed to get concurrency lock. Attempting to continue without it...' ) log.exception(e) ref = root.find(".//switch/ref") url = ref.attrib['src'] tracking = {} for item in ref.find( "./param[@name='trackingData']").attrib['value'].split('|'): key, value = item.split('=') tracking[key] = value license = WIDEVINE_URL.format(token=userdata.get('play_token'), pid=tracking['pid'], challenge='B{SSM}') return url, license def logout(self): userdata.delete('device_id') userdata.delete('profile_id') userdata.delete('play_token') userdata.delete('token_expires') userdata.delete('sky_token') userdata.delete('subscriptions') self.new_session()
class API(object): def new_session(self): self._logged_in = False self._language = COMM_LANG self._session = Session(HEADERS, base_url=API_URL) self._set_access_token(userdata.get('access_token')) def _set_access_token(self, token): if token: self._session.headers.update({'Authorization': 'Bearer {}'.format(token)}) self._logged_in = True @property def logged_in(self): return self._logged_in def login(self, username, password): self.logout() data = { 'response_type': 'token', 'lang': self._language, } resp = self._session.get(LOGIN_URL, params=data) soup = BeautifulSoup(resp.text, 'html.parser') for form in soup.find_all('form'): data = {} for e in form.find_all('input'): data[e.attrs['name']] = e.attrs.get('value') if 'signin[email]' in data: break data.update({ 'signin[email]': username, 'signin[password]': password, }) resp = self._session.post(LOGIN_URL, data=data, allow_redirects=False) access_token = resp.cookies.get('showmax_oauth') if not access_token: raise APIError('Failed to login') self._set_access_token(access_token) data = self._session.get('user/current', params={'lang': self._language}).json() if 'error_code' in data: raise APIError('Failed to login') device_id = hashlib.sha1(username.lower().strip().encode('utf8')).hexdigest().upper() userdata.set('device_id', device_id) userdata.set('access_token', access_token) userdata.set('user_id', data['user_id']) def logout(self): userdata.delete('device_id') userdata.delete('access_token') userdata.delete('user_id') self.new_session() def _catalogue(self, _params): def process_page(start): params = { 'field[]': ['id'], 'lang': self._language, 'showmax_rating': '18-plus', 'sort': 'alphabet', 'start': start, 'subscription_status': 'full', } params.update(_params) data = self._session.get('catalogue/assets', params=params).json() items = data['items'] count = int(data.get('count', 0)) remaining = int(data.get('remaining', 0)) if count > 0 and remaining > 0: items.extend(process_page(start + count)) return items return process_page(start=0) def series(self): return self._catalogue({ 'field[]': ['id', 'images', 'title', 'items', 'total', 'description', 'videos', 'type'], 'type': 'tv_series', }) def movies(self): return self._catalogue({ 'field[]': ['id', 'images', 'title', 'items', 'total', 'description', 'videos', 'type'], 'type': 'movie', }) def kids(self): return self._catalogue({ 'field[]': ['id', 'images', 'title', 'items', 'total', 'description', 'videos', 'type'], 'showmax_rating': '5-6', 'types[]': ['tv_series', 'movie'], }) def seasons(self, series_id): params = { 'field[]': ['id', 'images', 'title', 'items', 'total', 'description', 'number', 'seasons', 'type'], 'lang': self._language, 'showmax_rating': '18-plus', 'subscription_status': 'full', } return self._session.get('catalogue/tv_series/{}'.format(series_id), params=params).json() def episodes(self, season_id): params = { 'field[]': ['id', 'images', 'title', 'items', 'total', 'description', 'number', 'tv_series', 'episodes', 'videos', 'type'], 'lang': self._language, 'showmax_rating': '18-plus', 'subscription_status': 'full', } return self._session.get('catalogue/season/{}'.format(season_id), params=params).json() def search(self, query): return self._catalogue({ 'field[]': ['id', 'images', 'title', 'items', 'total', 'type', 'description', 'type', 'videos'], 'types[]': ['tv_series', 'movie'], 'showmax_rating': '18-plus', 'subscription_status': 'full', 'q': query, }) def asset(self, asset_id): params = { 'field[]': ['videos',], 'exclude[]': 'episodes', 'lang': self._language, 'showmax_rating': '18-plus', 'subscription_status': 'full', } return self._session.get('catalogue/asset/{}'.format(asset_id), params=params).json()['videos'] def play(self, video_id): codecs = '' if settings.getBool('vp9', False): codecs += 'vp9+' if settings.getBool('h265', False): codecs += 'hevc+' if settings.getBool('h264', True): codecs += 'h264+' codecs = codecs.rstrip('+') params = { 'capabilities[]': ['codecs={}'.format(codecs), 'multiaudio'], 'encoding': 'mpd_widevine_modular', 'subscription_status': 'full', 'mode': 'paid', 'showmax_rating': '18-plus', 'lang': self._language, # 'content_country': 'ZA', } ## Temp below use old api until Proxy quality player fixed (showmax blocks too quick calls) data = self._session.get('playback/play/{}'.format(video_id), params=params).json() if 'url' not in data: raise APIError(data.get('message')) url = data['url'] task_id = data['packaging_task_id'] session_id = data['session_id'] data = { 'user_id': userdata.get('user_id'), 'video_id': video_id, 'hw_code': userdata.get('device_id'), 'packaging_task_id': task_id, 'session_id': session_id, } params = { 'showmax_rating': '18-plus', 'mode': 'paid', 'subscription_status': 'full', 'lang': self._language, } data = self._session.post('playback/verify', params=params, data=data).json() if 'license_request' not in data: raise APIError(data.get('message')) license_request = data['license_request'] license_url = API_URL.format('drm/widevine_modular?license_request={}'.format(license_request)) return url, license_url
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()
class API(object): def new_session(self): self._session = Session(HEADERS, base_url=API_URL) self.logged_in = userdata.get('token') != None def _refresh_token(self): payload = { 'username': userdata.get('username'), 'loginToken': userdata.get('token'), 'deviceId': userdata.get('deviceid'), 'accountType': 'foxtel', 'format': 'json', 'appID': 'GO2', 'plt': PLT_DEVICE, } password = userdata.get('pswd') if password: log.debug('Using Password Login') payload['password'] = self._hex_password(password, userdata.get('deviceid')) del payload['loginToken'] else: log.debug('Using Token Login') data = self._session.post( '/auth.class.api.php/logon/{site_id}'.format(site_id=VOD_SITEID), data=payload).json() response = data['LogonResponse'] error = response.get('Error') success = response.get('Success') if error: self.logout() raise APIError(_(_.TOKEN_ERROR, msg=error.get('Message'))) userdata.set('token', success['LoginToken']) userdata.set('deviceid', success['DeviceId']) userdata.set('entitlements', success.get('Entitlements', '')) self.logged_in = True def login(self, username, password, kickdevice=None): self.logout() raw_id = self._format_id(settings.get('device_id')).lower() device_id = hashlib.sha1(raw_id.encode('utf8')).hexdigest() log.debug('Raw device id: {}'.format(raw_id)) log.debug('Hashed device id: {}'.format(device_id)) hex_password = self._hex_password(password, device_id) payload = { 'username': username, 'password': hex_password, 'deviceId': device_id, 'accountType': 'foxtel', 'format': 'json', 'appID': 'GO2', 'plt': PLT_DEVICE, } if kickdevice: payload['deviceToKick'] = kickdevice log.debug('Kicking device: {}'.format(kickdevice)) data = self._session.post( '/auth.class.api.php/logon/{site_id}'.format(site_id=VOD_SITEID), data=payload).json() response = data['LogonResponse'] devices = response.get('CurrentDevices', []) error = response.get('Error') success = response.get('Success') if error: if not devices or kickdevice: raise APIError(_(_.LOGIN_ERROR, msg=error.get('Message'))) options = [d['Nickname'] for d in devices] index = gui.select(_.DEREGISTER_CHOOSE, options) if index < 0: raise APIError(_(_.LOGIN_ERROR, msg=error.get('Message'))) kickdevice = devices[index]['DeviceID'] return self.login(username, password, kickdevice=kickdevice) userdata.set('token', success['LoginToken']) userdata.set('deviceid', success['DeviceId']) userdata.set('entitlements', success.get('Entitlements', '')) if settings.getBool('save_password', False): userdata.set('pswd', password) log.debug('Password Saved') self.logged_in = True def _format_id(self, string): try: mac_address = uuid.getnode() if mac_address != uuid.getnode(): mac_address = '' except: mac_address = '' system, arch = get_system_arch() return string.format(username=userdata.get('username'), mac_address=mac_address, system=system).strip() def _hex_password(self, password, device_id): nickname = self._format_id(settings.get('device_name')) log.debug('Device nickname: {}'.format(nickname)) payload = { 'deviceId': device_id, 'nickName': nickname, 'format': 'json', 'appID': 'GO2', 'accountType': 'foxtel', 'plt': PLT_DEVICE, } secret = self._session.post( '/auth.class.api.php/prelogin/{site_id}'.format( site_id=VOD_SITEID), data=payload).json()['secret'] log.debug('Pass Secret: {}{}'.format(secret[:5], 'x' * len(secret[5:]))) try: #python3 iv = bytes.fromhex(AES_IV) except AttributeError: #python2 iv = str(bytearray.fromhex(AES_IV)) encrypter = pyaes.Encrypter( pyaes.AESModeOfOperationCBC(secret.encode('utf8'), iv)) ciphertext = encrypter.feed(password) ciphertext += encrypter.feed() try: #python3 hex_password = ciphertext.hex() except AttributeError: #python2 hex_password = ciphertext.encode('hex') log.debug('Hex password: {}{}'.format(hex_password[:5], 'x' * len(hex_password[5:]))) return hex_password def assets(self, asset_type, _filter=None, showall=False): params = { 'showall': showall, 'plt': PLT_DEVICE, 'entitlementToken': self._entitlement_token(), 'sort': 'latest', 'format': 'json', 'appID': 'GO2', 'serviceID': 'PLAY', } if _filter: params['filters'] = _filter return self._session.get( '/categoryTree.class.api.php/GOgetAssets/{site_id}/{asset_type}'. format(site_id=VOD_SITEID, asset_type=asset_type), params=params, timeout=20).json() def live_channels(self, _filter=None): params = { 'plt': PLT_DEVICE, 'entitlementToken': self._entitlement_token(), 'format': 'json', 'appID': 'GO2', 'serviceID': 'PLAY', } if _filter: params['filter'] = _filter return self._session.get( '/categoryTree.class.api.php/GOgetLiveChannels/{site_id}'.format( site_id=LIVE_SITEID), params=params).json() def show(self, show_id): params = { 'showId': show_id, 'plt': PLT_DEVICE, 'format': 'json', 'dateFormat': 'ISO8601', 'appID': 'GO2', 'serviceID': 'PLAY', } return self._session.get( '/asset.class.api.php/GOgetAssetData/{site_id}/0'.format( site_id=VOD_SITEID), params=params).json() def asset(self, media_type, id): params = { 'plt': PLT_DEVICE, 'format': 'json', 'dateFormat': 'ISO8601', 'appID': 'GO2', 'serviceID': 'PLAY', } if media_type == TYPE_VOD: site_id = VOD_SITEID else: site_id = LIVE_SITEID return self._session.get( '/asset.class.api.php/GOgetAssetData/{site_id}/{id}'.format( site_id=site_id, id=id), params=params).json() def bundle(self, mode=''): params = { 'plt': PLT_DEVICE, 'entitlementToken': self._entitlement_token(), 'apiVersion': 2, 'filter': '', 'mode': mode, 'format': 'json', 'appID': 'GO2', 'serviceID': 'PLAY', } return self._session.get(BUNDLE_URL, params=params).json() def _sync_token(self, site_id, catalog_name): self._refresh_token() params = { 'serviceID': 'PLAY', } payload = { 'loginToken': userdata.get('token'), 'deviceId': userdata.get('deviceid'), 'format': 'json', } vod_token = None live_token = None data = self._session.post( '/userCatalog.class.api.php/getSyncTokens/{site_id}'.format( site_id=VOD_SITEID), params=params, data=payload).json() for token in data.get('tokens', []): if token['siteId'] == site_id and token[ 'catalogName'] == catalog_name: return token['token'] return None def user_catalog(self, catalog_name, site_id=VOD_SITEID): token = self._sync_token(site_id, catalog_name) if not token: return params = { 'syncToken': token, 'platform': PLT_DEVICE, 'limit': 100, 'format': 'json', 'appID': 'GO2', 'serviceID': 'PLAY', } return self._session.get( '/userCatalog.class.api.php/getCarousel/{site_id}/{catalog_name}'. format(site_id=site_id, catalog_name=catalog_name), params=params).json() def epg(self, channel_codes, starttime=None, endtime=None): now = arrow.utcnow() starttime = starttime or now.shift(hours=-2) endtime = endtime or starttime.shift(days=1) params = { 'filter_starttime': starttime.timestamp, 'filter_endtime': endtime.timestamp, 'filter_channels': ','.join(channel_codes), 'filter_fields': 'EventTitle,ShortSynopsis,StartTimeUTC,EndTimeUTC,RawStartTimeUTC,RawEndTimeUTC,ProgramTitle,EpisodeTitle,Genre,HighDefinition,ClosedCaption,EpisodeNumber,SeriesNumber,ParentalRating,MergedShortSynopsis', 'format': 'json', } return self._session.get(EPG_URL, params=params).json() def search(self, query, _type='VOD'): params = { 'prod': 'FOXTELGO', 'idm': '04', 'BLOCKED': 'YES', 'fx': '"{}"'.format(query), 'sfx': 'type:{}'.format(_type), #VOD OR LINEAR 'limit': 100, 'offset': 0, 'dpg': 'R18+', 'ao': 'N', 'dopt': '[F0:11]', 'hwid': '_', 'REGION': '_', 'utcOffset': '+1200', 'swver': '3.3.7', 'aid': '_', 'fxid': '_', 'rid': 'SEARCH5', } return self._session.get(SEARCH_URL, params=params).json() def play(self, media_type, id): self._refresh_token() payload = { 'deviceId': userdata.get('deviceid'), 'loginToken': userdata.get('token'), } if media_type == TYPE_VOD: endpoint = 'GOgetVODConfig' site_id = VOD_SITEID else: endpoint = 'GOgetLiveConfig' site_id = LIVE_SITEID params = { 'rate': 'WIREDHIGH', 'plt': 'ipstb', 'appID': 'PLAY2', 'deviceCaps': hashlib.md5('TR3V0RwAZH3r3L00kingA7SumStuFF{}'.format('L1').encode( 'utf8')).hexdigest().lower(), 'format': 'json', } data = self._session.post(PLAY_URL.format(endpoint=endpoint, site_id=site_id, id=id), params=params, data=payload).json() error = data.get('errorMessage') if error: raise APIError(_(_.PLAYBACK_ERROR, msg=error)) streams = sorted(data['media'].get('streams', []), key=lambda s: STREAM_PRIORITY.get( s['profile'].upper(), STREAM_PRIORITY['DEFAULT']), reverse=True) if not streams: raise APIError(_.NO_STREAM_ERROR) playback_url = streams[0]['url'] playback_url = playback_url.replace( 'cm=yes&', '') #without this = bad widevine key ## Get L3 License URL params['plt'] = 'andr_phone' params['appID'] = 'PLAY2' data = self._session.post(LICENSE_URL.format(endpoint=endpoint, site_id=site_id, id=id), params=params, data=payload).json() license_url = data['fullLicenceUrl'] ####### params = { 'sessionId': data['general']['sessionID'], 'deviceId': userdata.get('deviceid'), 'loginToken': userdata.get('token'), 'sessionStatus': 'FINISHED', 'appID': 'GO2', 'serviceID': 'GO', 'format': 'json', } url = '/playback.class.api.php/GOupdateSession/{}/{}'.format( data['general']['siteID'], data['general']['assetID']) self._session.get(url, params=params).json() return playback_url, license_url def asset_for_program(self, show_id, program_id): show = self.show(show_id) if show.get('programId') == program_id: return show if 'childAssets' not in show: return None for child in show['childAssets']['items']: if child.get('programId') == program_id: return child if 'childAssets' not in child: return None for subchild in child['childAssets']['items']: if subchild.get('programId') == program_id: return subchild return None def _entitlement_token(self): entitlements = userdata.get('entitlements') if not entitlements: return None return hashlib.md5(entitlements.encode('utf8')).hexdigest() def logout(self): userdata.delete('token') userdata.delete('deviceid') userdata.delete('pswd') userdata.delete('entitlements') self.new_session()
class API(object): def new_session(self): self.logged_in = False host = settings.get('business_host') if settings.getBool( 'business_account', False) else DEFAULT_HOST if host != userdata.get('host', DEFAULT_HOST): userdata.delete('access_token') userdata.set('host', host) self._session = Session(HEADERS, base_url=API_URL.format(host)) self._set_authentication() def _set_authentication(self): token = userdata.get('access_token') if not token: return self._session.headers.update( {'Authorization': 'Bearer {}'.format(token)}) self.logged_in = True def my_courses(self, page=1, query=None): params = { 'page': page, 'page_size': PAGE_SIZE, 'ordering': 'title', 'fields[course]': 'id,title,image_480x270,image_750x422,headline,num_published_lectures,content_info,completion_ratio', } if query: params['search'] = query return self._session.get('users/me/subscribed-courses', params=params).json() def chapters(self, course_id, page=1): params = { 'page': page, 'page_size': PAGE_SIZE, 'fields[course]': 'image_480x270', 'fields[chapter]': 'description,object_index,title,course', 'fields[lecture]': 'id', 'fields[practice]': 'id', 'fields[quiz]': 'id', } data = self._session.get( 'courses/{}/cached-subscriber-curriculum-items'.format(course_id), params=params).json() rows = [r for r in data['results'] if r['_class'] == 'chapter'] return rows, data['next'] def lectures(self, course_id, chapter_id, page=1): params = { 'page': page, 'page_size': PAGE_SIZE, 'fields[course]': 'image_480x270,title', 'fields[chapter]': 'id', 'fields[lecture]': 'title,object_index,description,is_published,course,id,asset', 'fields[asset]': 'asset_type,length,status', 'fields[practice]': 'id', 'fields[quiz]': 'id', } data = self._session.get( 'courses/{}/cached-subscriber-curriculum-items'.format(course_id), params=params).json() lectures = [] found = False for row in data['results']: if not found and row['_class'] == 'chapter' and row['id'] == int( chapter_id): found = True elif found and row['_class'] == 'lecture' and row[ 'is_published'] and row['asset']['asset_type'] in ( 'Video', 'Audio'): lectures.append(row) elif found and row['_class'] == 'chapter': break return lectures, data['next'] def get_stream_data(self, asset_id): params = { 'fields[asset]': '@all', } data = self._session.get('assets/{0}'.format(asset_id), params=params).json() if 'detail' in data: raise APIError(data['detail']) return data def login(self, username, password): data = { 'email': username, 'password': password, 'upow': self._get_upow(username, 'login') } params = { 'fields[user]': 'title,image_100x100,name,access_token', } r = self._session.post('auth/udemy-auth/login/', params=params, data=data) try: data = r.json() except: raise APIError(_(_.LOGIN_ERROR, msg=r.status_code)) access_token = data.get('access_token') if not access_token: raise APIError(_(_.LOGIN_ERROR, msg=data.get('detail', ''))) userdata.set('access_token', access_token) self._set_authentication() def logout(self): userdata.delete('access_token') self.new_session() def _get_upow(self, message, secret): date = datetime.datetime.today().strftime('%Y%m%d') def get_token(email, date, secret): message = email + date i = 0 for x in range(0, 20): i3 = i * 50 while True: i2 = i + 1 if i3 >= i2 * 50: break i4 = i3 * 1000 i3 += 1 token = hash_calc(i4, i3 * 1000, message, secret) if token: return token i = i2 return None def m26785a(i): f19175e = "" while i >= 0: f19175e += chr(((i % 26) + 65)) i = int(i / 26) - 1 return f19175e[::-1] def hash_calc(i, i2, message, password): a = m26785a(i) _bytes = bytearray(message + a, 'utf8') password = password.encode() while i < i2: _i = i if (_i % 26 == 0): _bytes = bytearray(message + m26785a(_i), 'utf8') else: _bytes[len(_bytes) - 1] = (_bytes[len(_bytes) - 1] + 1) doFinal = hmac.new(password, _bytes, digestmod=hashlib.sha256).hexdigest() if doFinal[0:2] == '00' and doFinal[2:4] == '00': return m26785a(i) i += 1 return None return date + get_token(message, date, secret)
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()