From 701a5398dca19908cfb21db466d18332a8a12457 Mon Sep 17 00:00:00 2001 From: mkorganashvili Date: Sat, 17 Dec 2022 12:33:42 +0400 Subject: [PATCH 1/3] Update auth.py --- resources/lib/auth.py | 53 +++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/resources/lib/auth.py b/resources/lib/auth.py index ea016fb..6c01741 100644 --- a/resources/lib/auth.py +++ b/resources/lib/auth.py @@ -6,12 +6,13 @@ import json import urlquick +import time +import urllib.parse from resources.lib.vars import * from codequick.storage import PersistentDict from codequick import Script from base64 import b64decode -from time import time from random import choice @@ -100,7 +101,7 @@ def get_cookies(): logindata['refreshToken'] = refreshToken logindata.flush() Script.log('get_cookies: stored cookies to cache', lvl=Script.DEBUG) - return CIAM_TOKEN + return CIAM_TOKEN, refreshToken @@ -110,14 +111,19 @@ def get_token(): tokeninfo = PersistentDict(".accountinfo.token", ttl=7198) try: access_token = tokeninfo['access_token'] + expiry_time = None #tokeninfo['expiry_time'] Script.log('get_token: Got access token from cache', lvl=Script.DEBUG) except: access_token = None + expiry_time = None Script.log('get_token: Unable to get access token from cache', lvl=Script.DEBUG) - if access_token: + if expiry_time: try: - exp = json.loads(b64decode(access_token.split('.')[1]))['exp'] - now = time() + # b64 = access_token.split('.')[1] + # token_json = b64decode(b64 + '=' * (-len(b64) % 4)).decode('utf-8') + # exp = json.loads(token_json)['exp'] + exp = time.mktime(time.strptime(expiry_time[:26], '%Y-%m-%dT%H:%M:%S.%f')) + now = time.time() # renew if under 5 minutes to expire if (exp > now) and (exp - now < 300): try: @@ -138,38 +144,47 @@ def get_token(): Script.log('get_token: Cache token expired', lvl=Script.DEBUG) auth_data = None access_token = None + expiry_time = None else: return access_token - except: + except Exception as exp: + Script.log(exp, lvl=Script.ERROR) access_token = None - if not access_token: - CIAM_TOKEN = get_cookies() + if not access_token or not expiry_time: + CIAM_TOKEN, refreshToken = get_cookies() if not CIAM_TOKEN: return False - headers['CIAM_TOKEN'] = CIAM_TOKEN + #headers['CIAM_TOKEN'] = CIAM_TOKEN Script.log('get_token: trying to get new token', lvl=Script.DEBUG) - auth_data = urlquick.post( + auth_data = urlquick.get( AUTH_URL, headers=headers, - data=auth_payload, + cookies={ + 'nbaidentity': '{"jwt":"%s","refreshToken":"%s"}' % (CIAM_TOKEN, refreshToken) + }, max_age=0 ).json() - login_status = auth_data['code'] + + login_status = auth_data['status'] Script.log('get_cookies: Login status %s' % login_status, lvl=Script.DEBUG) - access_token = auth_data['data']['accessToken'] + if not 'success' in login_status: + return False + + access_token = auth_data['data']['AccessToken'] tokeninfo['access_token'] = access_token + tokeninfo['expiry_time'] = auth_data['data']['ExpiryTime'] tokeninfo.flush() Script.log('get_token: stored access token to cache', lvl=Script.DEBUG) - login_headers.update({'authorization': 'Bearer %s' % access_token}) + login_headers.update({'authorization': 'Bearer %s' % CIAM_TOKEN}) params = {'associations': 'false'} Script.log('get_token: getting subscrition infos', lvl=Script.DEBUG) - subscrition_data = urlquick.post( + subscrition_data = urlquick.get( SUBSCRIPTION_URL, headers=login_headers, max_age=86400 ).json() - if 'subs' in subscrition_data: + if 'subscriptions' in subscrition_data['data']: Script.log('get_token: found subscrition infos', lvl=Script.DEBUG) """ subscrition_type = subscrition_data['subs'][0]['productSubType'] @@ -225,8 +240,8 @@ def get_headers(free=False): if access_token: headers = { 'User-Agent': USER_AGENT, - 'Content-Type': 'application/x-www-form-urlencoded', - 'authorization': 'Bearer %s' % access_token + 'Content-Type': 'application/json', + 'Authorization': 'OAUTH2 access_token="%s"' % access_token } else: headers = None @@ -248,7 +263,7 @@ def get_profile_info(): FAVORITE_TEAMS = None FAVORITE_PLAYERS = None if not FAVORITE_TEAMS or not FAVORITE_PLAYERS: - access_token = get_cookies() + access_token, refreshToken = get_cookies() if not access_token: return {'FAVORITE_TEAMS': None, 'FAVORITE_PLAYERS': None} headers = { From 24f6461a13cb98f09acec32deedea3c513fb7169 Mon Sep 17 00:00:00 2001 From: mkorganashvili Date: Sat, 17 Dec 2022 12:34:39 +0400 Subject: [PATCH 2/3] Update vars.py --- resources/lib/vars.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/resources/lib/vars.py b/resources/lib/vars.py index f18346b..dee66cf 100644 --- a/resources/lib/vars.py +++ b/resources/lib/vars.py @@ -39,15 +39,19 @@ PROFILE_URL = IDENTIFY + 'profile' LOGIN_URL = IDENTIFY + 'auth' TOKEN_URL = IDENTIFY + 'oauth/token' +AUTH_URL = IDENTIFY + 'sts' +SUBSCRIPTION_URL = IDENTIFY + 'profile?entitlements=true' WATCH = 'https://watch.nba.com/' -AUTH_URL = WATCH + 'secure/authenticate' GAME_DATA_ENDPOINT = WATCH + 'game/%s?format=json' CONFIG_ENDPOINT = WATCH + 'service/config?format=json&cameras=true' FREE_TOKEN_URL = WATCH + 'secure/accesstoken' +PLAY_OPTIONS_URL = 'https://ottapp-appgw-client.nba.com/S1/subscriber/v1/events/%s/play-options?IsexternalId=true' +PLAY_ROLL_URL = 'https://ottapp-appgw-amp.nba.com/v1/client/roll?ownerUid=azuki&mediaId=%s&sessionId=%s' +WIDEVINE_LICENSE_URL = 'https://ottapp-appgw-amp.nba.com/v1/client/get-widevine-license?ownerUid=azuki&mediaId=%s&sessionId=%s' + NBAAPI = 'https://nbaapi.neulion.com/api_nba/v1/' -SUBSCRIPTION_URL = NBAAPI + 'account/subscriptions' RENEW_TOKEN_URL = NBAAPI + 'accesstoken' PUBLISH_ENDPOINT = NBAAPI + 'publishpoint' GAME_URL = NBAAPI + 'game' @@ -92,4 +96,3 @@ 'hls': {'extensions': ['m3u8', 'm3u'], 'mimetype': 'application/vnd.apple.mpegurl'}, } DRM = 'com.widevine.alpha' -LICENSE_URL = 'https://shield-twoproxy.imggaming.com/proxy' From 72ffae32adc0dfde3d08a0a9564c0bb5f63e488f Mon Sep 17 00:00:00 2001 From: mkorganashvili Date: Sat, 17 Dec 2022 12:35:17 +0400 Subject: [PATCH 3/3] Update games.py --- resources/lib/games.py | 167 ++++++++++++++++------------------------- 1 file changed, 63 insertions(+), 104 deletions(-) diff --git a/resources/lib/games.py b/resources/lib/games.py index c3e4eeb..731bdbc 100644 --- a/resources/lib/games.py +++ b/resources/lib/games.py @@ -11,23 +11,26 @@ import xbmcgui import os import re +import uuid from resources.lib.vars import * from resources.lib.tools import * from resources.lib.auth import get_headers from resources.lib.auth import get_profile_info from resources.lib.auth import get_device_ids +from resources.lib.auth import get_token from codequick import Route from codequick import Listitem from codequick import Resolver from codequick.utils import bold from inputstreamhelper import Helper +from base64 import b64encode -def process_games(game, teams_info): +def process_games(game, teams_info, cache_max_age): gameID = game['id'] gameCode = game['seoName'] game_time = game['st'] @@ -149,7 +152,8 @@ def process_games(game, teams_info): start_time=game_timestamp, end_time=game_end_timestamp, game_state=game_state, - feeds=feeds + feeds=feeds, + cache_max_age=cache_max_age ) return liz @@ -300,7 +304,7 @@ def BROWSE_GAMES(plugin, DATE=None, games=None, cache_max_age=0): if 'game' in game: if not game['game']: continue - liz = process_games(game, teams_info) + liz = process_games(game, teams_info, cache_max_age) yield liz if not liz: @@ -350,7 +354,7 @@ def BROWSE_MONTHS(plugin, year=None, team=None, cal=False): else: this_year = False month = 12 - headers = get_headers(True) + headers = None #get_headers(True) if not headers: yield False return @@ -437,7 +441,7 @@ def BROWSE_MONTH(plugin, year, month, team, **kwargs): for game in games: if game != []: game = game[0] - liz = process_games(game,teams_info) + liz = process_games(game,teams_info, 0) yield liz @@ -458,122 +462,77 @@ def BROWSE_YEARS(plugin, year, team=False): @Route.register(content_type="videos") -def BROWSE_GAME(plugin, gameID, start_time, end_time, game_state, feeds): - for feed in feeds: - feed['gameID'] = gameID - feed['start_time'] = start_time - feed['end_time'] = end_time - feed['game_state'] = game_state +def BROWSE_GAME(plugin, gameID, start_time, end_time, game_state, feeds, cache_max_age): + headers = get_headers() + play_options = urlquick.get( + PLAY_OPTIONS_URL % gameID, + headers=headers, + max_age=cache_max_age + ).json() + for play_option in play_options['Vods']: yield Listitem.from_dict( + PLAY_GAME, + bold(play_option['DisplayName'][0]['Value']), + params = { + 'gameID': gameID, + 'videoProfileId': play_option['PlayActions'][0]['VideoProfile']['Id'], + 'applicationToken': '0' + } + ) + if len(play_options['Schedules']) > 0: + for play_option in play_options['Schedules'][0]['Productions']: + yield Listitem.from_dict( PLAY_GAME, - bold(feed['name']), - params = feed + bold(play_option['DisplayName'][0]['Value']), + params = { + 'gameID': gameID, + 'videoProfileId': play_option['ExternalId'], + 'applicationToken': play_option['Id'] + } ) @Resolver.register -def PLAY_GAME(plugin, gameID, start_time, end_time, game_state, name, gt, cn, rd): - plugin.log('PLAY_GAME start_time: %s' % start_time, lvl=plugin.DEBUG) - plugin.log('PLAY_GAME end_time: %s' % end_time, lvl=plugin.DEBUG) - headers = get_headers() - if not headers: - yield False - return +def PLAY_GAME(plugin, gameID, videoProfileId, applicationToken): + access_token = get_token() deviceinfos = get_device_ids() - DEVICEID = deviceinfos['PCID'] - PCID = deviceinfos['PCID'] - payload_data = { - 'type': 'game', - 'extid': gameID, - 'drmtoken': True, - 'deviceid': DEVICEID, - 'pcid': PCID, - 'gt': gt, - 'gs': game_state, - 'format': 'json' - } - if cn: - payload_data['cam'] = cn - if end_time: - duration = end_time - start_time - payload_data.update({'st': start_time}) - payload_data.update({'dur': duration}) - game_type = 'archive' - else: - game_type = 'live' - plugin.log('PLAY_GAME: Fetching url %s' % PUBLISH_ENDPOINT, lvl=plugin.DEBUG) - plugin.log('PLAY_GAME: params %s' % payload_data, lvl=plugin.DEBUG) - plugin.log('PLAY_GAME: game type %s' % game_type, lvl=plugin.DEBUG) - Response = urlquick.post( - PUBLISH_ENDPOINT, - data=payload_data, - headers=headers, - max_age=0 - ).json() - url = Response['path'] - drm = Response['drmToken'] - try: - stream_type = Response['streamType'] - if stream_type == 'dash': - protocol = 'mpd' - else: - protocol = 'hls' - except: - protocol = 'hls' - - headers = {'User-Agent': USER_AGENT} - start_point = None - live_play_type = int(Script.setting.get_string('live_play_type')) - ret = None - if game_type == 'live': - if live_play_type == 0: - line1 = "Start from Beginning" - line2 = "Go LIVE" - ret = xbmcgui.Dialog().select("Game Options", [line1, line2]) - if ret == -1: - yield None - return - if ret == 0 or live_play_type == 2: - url = url.replace('br_long_master', 'master') - content = urlquick.get(url, headers=headers).text - sample = re.findall('(.*video.*\.m3u8?)', content)[0] - match = re.search('(https?)://([^:]+)/([^?]+?)\?(.+)$', url) - baseurl = os.path.dirname(match.group(1)+'://'+match.group(2)+'/'+match.group(3)) - ql_url = baseurl + '/' + sample - content = urlquick.get(ql_url, headers=headers, max_age=0).text - durations = re.findall('\#EXTINF\:([0-9]+\.[0-9]+)\,', content) - duration = sum([float(i) for i in durations]) - if ret == 0 or live_play_type == 2: - stream_start = re.findall('PROGRAM\-DATE\-TIME\:(.*)', content)[0] - stream_start = time.strptime(stream_start, '%Y-%m-%dT%H:%M:%S.%fZ') - stream_start_ts = calendar.timegm(stream_start) * 1000 - start_point = str(int((start_time - stream_start_ts) / 1000)) - elif ret == 1 or live_play_type == 1 : - start_point = str(duration).split('.')[0] - + headers = { + 'content-type': 'text/plain;charset=UTF-8', + 'authorizationtoken': access_token, + 'azukiimc': 'IMC7.1.0_AN_D3.0.0_S0', + 'deviceprofile': b64encode(('{"model":"Unknown","osVersion":"89.0.4389.114","vendorName":"Unknown","osName":"HTML5","deviceUUID":"%s"}' % deviceinfos['DEVICEID']).encode('ascii')), + 'ApplicationToken': applicationToken + } + sessionId = uuid.uuid1() + play_options = urlquick.post( + PLAY_ROLL_URL % (videoProfileId, sessionId), + headers=headers, + data=b'{}', + max_age=0, + raise_for_status=False + ).json()['response'] + Script.log(play_options) + url = '%s/%s&ISO3166=US&sessionId=%s' % (play_options['cdns']['cdn'][0]['base_uri'], play_options['manifest_uri'], sessionId) + protocol = play_options['package_type'] + license_url = WIDEVINE_LICENSE_URL % (videoProfileId, sessionId) + liz = Listitem() liz.path = url - liz.label = name - if rd: - yield liz - return + #liz.label = name liz.property[INPUTSTREAM_PROP] = 'inputstream.adaptive' + is_helper = Helper(protocol, drm=DRM) if is_helper.check_inputstream(): liz.property['inputstream.adaptive.manifest_type'] = protocol liz.property['inputstream.adaptive.license_type'] = DRM - license_key = '%s|authorization=bearer %s|R{SSM}|' % (LICENSE_URL, drm) + license_key = '%s|AuthorizationToken=%s&ApplicationToken=%s|R{SSM}|' % (license_url, access_token, applicationToken) liz.property['inputstream.adaptive.license_key'] = license_key liz.property['inputstream.adaptive.manifest_update_parameter'] = 'full' liz.property['inputstream.adaptive.play_timeshift_buffer'] = 'true' + liz.property['ResumeTime'] = '2000' + yield liz - if start_point: - plugin.log('PLAY_GAME start_point: %s' % start_point, lvl=plugin.DEBUG) - liz.property['ResumeTime'] = start_point - liz.property['TotalTime'] = '14400' + yield False + return - yield liz - else: - yield False - return