From c8f9afa2190c8f004b256f2590f0c2a86e5c6b95 Mon Sep 17 00:00:00 2001 From: eracknaphobia Date: Mon, 22 Jul 2024 13:12:46 -0400 Subject: [PATCH] wip #73 and #72 --- .gitignore | 3 +- addon.xml | 10 +- resources/lib/account.py | 219 +++++++++++++++++++++------------------ resources/lib/globals.py | 11 +- resources/lib/mlb.py | 95 ++++++++--------- 5 files changed, 168 insertions(+), 170 deletions(-) diff --git a/.gitignore b/.gitignore index 24fae75..34fcef1 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -*.pyo \ No newline at end of file +*.pyo +*.pyc diff --git a/addon.xml b/addon.xml index d195e70..858ec96 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -22,12 +22,8 @@ Requires an MLB.tv account - - further fixed stream padding to avoid timeline spoilers - - fixed Big Inning schedule - - updated affiliates list - - require InputStream Adaptive - - more graceful stream padding - - potential Omega compatibility fix + - Fix invalid access error + - Restore radio feeds en all diff --git a/resources/lib/account.py b/resources/lib/account.py index c37dbdb..f82d58d 100644 --- a/resources/lib/account.py +++ b/resources/lib/account.py @@ -15,17 +15,16 @@ class Account: addon = xbmcaddon.Addon() username = '' - password = '' - session_key = '' + password = '' icon = addon.getAddonInfo('icon') - verify = True + verify = False def __init__(self): self.username = self.addon.getSetting('username') - self.password = self.addon.getSetting('password') - self.session_key = self.addon.getSetting('session_key') + self.password = self.addon.getSetting('password') self.did = self.device_id() self.util = Util() + self.media_url = 'https://media-gateway.mlb.com/graphql' def device_id(self): if self.addon.getSetting('device_id') == '': @@ -101,20 +100,7 @@ def login_token(self): def access_token(self): - url = 'https://us.edge.bamgrid.com/token' - headers = {'Accept': 'application/json', - 'Authorization': 'Bearer bWxidHYmYW5kcm9pZCYxLjAuMA.6LZMbH2r--rbXcgEabaDdIslpo4RyZrlVfWZhsAgXIk', - 'Content-Type': 'application/x-www-form-urlencoded' - } - payload = 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange&subject_token=%s' \ - '&subject_token_type=urn:ietf:params:oauth:token-type:jwt&platform=android-tv' \ - % self.media_entitlement() - - r = requests.post(url, headers=headers, data=payload, verify=self.verify) - access_token = r.json()['access_token'] - # refresh_token = r.json()['refresh_token'] - - return access_token + return self.login_token() def get_playback_url(self, content_id): auth = self.access_token() @@ -163,18 +149,68 @@ def get_playback_url(self, content_id): return auth, playback_url, broadcast_start_offset, broadcast_start_timestamp def get_stream(self, content_id): - auth, url, broadcast_start_offset, broadcast_start_timestamp = self.get_playback_url(content_id) - - url = url.replace('{scenario}','browser~csai') + device_id, session_id = self.get_device_session_id() headers = { - 'Accept': 'application/vnd.media-service+json; version=2', - 'Authorization': auth, - 'X-BAMSDK-Version': '3.0', - 'X-BAMSDK-Platform': 'windows', - 'User-Agent': UA_PC + 'User-Agent': UA_PC, + 'Authorization': 'Bearer ' + self.login_token(), + 'Content-Type': 'application/json', + 'Accept': 'application/json' } - - r = requests.get(url, headers=headers, cookies=self.util.load_cookies(), verify=self.verify) + data = { + "operationName": "initPlaybackSession", + "query": '''mutation initPlaybackSession( + $adCapabilities: [AdExperienceType] + $mediaId: String! + $deviceId: String! + $sessionId: String! + $quality: PlaybackQuality + ) { + initPlaybackSession( + adCapabilities: $adCapabilities + mediaId: $mediaId + deviceId: $deviceId + sessionId: $sessionId + quality: $quality + ) { + playbackSessionId + playback { + url + token + expiration + cdn + } + adScenarios { + adParamsObj + adScenarioType + adExperienceType + } + adExperience { + adExperienceTypes + adEngineIdentifiers { + name + value + } + adsEnabled + } + heartbeatInfo { + url + interval + } + trackingObj + } + }''', + "variables": { + "adCapabilities": ["GOOGLE_STANDALONE_AD_PODS"], + "mediaId": content_id, + "quality": "PLACEHOLDER", + "deviceId": device_id, + "sessionId": session_id + } + } + xbmc.log(str(data)) + r = requests.post(self.media_url, headers=headers, json=data, verify=VERIFY) + xbmc.log(r.text) + #r = requests.get(url, headers=headers, cookies=self.util.load_cookies(), verify=self.verify) if not r.ok: dialog = xbmcgui.Dialog() msg = "" @@ -183,78 +219,57 @@ def get_stream(self, content_id): dialog.notification(LOCAL_STRING(30270), msg, self.icon, 5000, False) sys.exit() - if 'complete' in r.json()['stream']: - stream_url = r.json()['stream']['complete'] - else: - stream_url = r.json()['stream']['slide'] - - # skip asking for quality if it's an audio-only stream - if QUALITY == 'Always Ask' and '_AUDIO_' not in stream_url: - stream_url = self.get_stream_quality(stream_url) + stream_url = r.json()['data']['initPlaybackSession']['playback']['url'] + xbmc.log(f'Stream URL: {stream_url}') headers = 'User-Agent=' + UA_PC - headers += '&Authorization=' + auth - headers += '&Cookie=' - cookies = requests.utils.dict_from_cookiejar(self.util.load_cookies()) - if sys.version_info[0] <= 2: - cookies = cookies.iteritems() - for key, value in cookies: - headers += key + '=' + value + '; ' - - #CDN - akc_url = 'hlslive-akc' - l3c_url = 'hlslive-l3c' - if CDN == 'Akamai' and akc_url not in stream_url: - stream_url = stream_url.replace(l3c_url, akc_url) - elif CDN == 'Level 3' and l3c_url not in stream_url: - stream_url = stream_url.replace(akc_url, l3c_url) - - return stream_url, headers, broadcast_start_offset, broadcast_start_timestamp - - def get_stream_quality(self, stream_url): - #Check if inputstream adaptive is on, if so warn user and return master m3u8 - if xbmc.getCondVisibility('System.HasAddon(inputstream.adaptive)'): - dialog = xbmcgui.Dialog() - dialog.ok(LOCAL_STRING(30370), LOCAL_STRING(30371)) - return stream_url - - stream_title = [] - stream_urls = [] - headers = {'User-Agent': UA_PC} - - r = requests.get(stream_url, headers=headers, verify=False) - master = r.text - - line = re.compile("(.+?)\n").findall(master) - - for temp_url in line: - if '#EXT' not in temp_url: - bandwidth = '' - # first check for bandwidth at beginning of URL (MLB game streams) - match = re.search(r'^(\d+?)K', temp_url, re.IGNORECASE) - if match is not None: - bandwidth = match.group() - # if we didn't find the correct bandwidth at the beginning of the URL - if match is None or len(bandwidth) > 6: - # check for bandwidth after an underscore (MILB games and featured videos) - match = re.search(r'_(\d+?)K', temp_url, re.IGNORECASE) - bandwidth = match.group() - # remove preceding underscore - bandwidth = bandwidth[1:] - if 0 < len(bandwidth) < 6: - bandwidth = bandwidth.replace('K', ' kbps') - stream_title.append(bandwidth) - stream_urls.append(temp_url) - - stream_title.sort(key=self.util.natural_sort_key, reverse=True) - stream_urls.sort(key=self.util.natural_sort_key, reverse=True) - dialog = xbmcgui.Dialog() - ret = dialog.select(LOCAL_STRING(30372), stream_title) - if ret >= 0: - if 'http' not in stream_urls[ret]: - stream_url = stream_url.replace(stream_url.rsplit('/', 1)[-1], stream_urls[ret]) - else: - stream_url = stream_urls[ret] - else: - sys.exit() + return stream_url, headers, '1', None + + def get_device_session_id(self): + headers = { + 'User-Agent': UA_PC, + 'Authorization': 'Bearer ' + self.login_token(), + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + + data = { + "operationName": "initSession", + "query": '''mutation initSession($device: InitSessionInput!, $clientType: ClientType!, $experience: ExperienceTypeInput) { + initSession(device: $device, clientType: $clientType, experience: $experience) { + deviceId + sessionId + entitlements { + code + } + location { + countryCode + regionName + zipCode + latitude + longitude + } + clientExperience + features + } + }''', + "variables": { + "device": { + "appVersion": "7.8.2", + "deviceFamily": "desktop", + "knownDeviceId": "", + "languagePreference": "ENGLISH", + "manufacturer": "Google Inc.", + "model": "", + "os": "windows", + "osVersion": "10" + }, + "clientType": "WEB" + } + } + + r = requests.post(self.media_url, headers=headers, json=data) + device_id = r.json()['data']['initSession']['deviceId'] + session_id = r.json()['data']['initSession']['sessionId'] + + return device_id, session_id - return stream_url diff --git a/resources/lib/globals.py b/resources/lib/globals.py index b5d6e7b..40fd943 100644 --- a/resources/lib/globals.py +++ b/resources/lib/globals.py @@ -79,17 +79,10 @@ NEXT_ICON = os.path.join(ROOTDIR,"icon.png") BLACK_IMAGE = os.path.join(ROOTDIR, "resources", "img", "black.png") -if SINGLE_TEAM == 'true': - MASTER_FILE_TYPE = 'master_wired.m3u8' - PLAYBACK_SCENARIO = 'HTTP_CLOUD_WIRED' -else: - MASTER_FILE_TYPE = 'master_wired60.m3u8' - PLAYBACK_SCENARIO = 'HTTP_CLOUD_WIRED_60' - API_URL = 'https://statsapi.mlb.com' #User Agents UA_IPAD = 'AppleCoreMedia/1.0 ( iPad; compatible; 3ivx HLS Engine/2.0.0.382; Win8; x64; 264P AACP AC3P AESD CLCP HTPC HTPI HTSI MP3P MTKA)' -UA_PC = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36' +UA_PC = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36' UA_ANDROID = 'okhttp/3.12.1' VERIFY = True @@ -433,6 +426,8 @@ def load_cookies(): def stream_to_listitem(stream_url, headers, description, title, icon, fanart, start='1', stream_type='video', music_type_unset=False): # check if our stream is HLS + xbmc.log(f'URL: {stream_url} Headers: {headers} start: {start}') + headers = 'User-Agent=' + UA_PC if '.m3u8' in stream_url: # if not audio only, check if inputstream.adaptive is present and enabled, depending on Kodi version if stream_type != 'audio' and (xbmc.getCondVisibility('System.HasAddon(inputstream.adaptive)') or (KODI_VERSION >= 19 and xbmc.getCondVisibility('System.AddonIsEnabled(inputstream.adaptive)'))): diff --git a/resources/lib/mlb.py b/resources/lib/mlb.py index 0e7478c..68a37f8 100644 --- a/resources/lib/mlb.py +++ b/resources/lib/mlb.py @@ -644,14 +644,15 @@ def create_game_changer_listitem(blackouts, inprogress_exists, game_changer_star def stream_select(game_pk, spoiler='True', suspended='False', start_inning='False', blackout='False', description=None, name=None, icon=None, fanart=None, from_context_menu=False, autoplay=False, overlay_check='False', gamechanger='False'): # fetch the epg content using the game_pk - url = API_URL + '/api/v1/game/' + game_pk + '/content' + url = f'{API_URL}/api/v1/schedule?gamePk={game_pk}&hydrate=team,linescore,xrefId,flags,review,broadcasts(all),,seriesStatus(useOverride=true),statusFlags,story&sortBy=gameDate,gameStatus,gameType' headers = { - 'User-Agent': UA_ANDROID + 'User-Agent': UA_PC } r = requests.get(url, headers=headers, verify=VERIFY) json_source = r.json() # start with just video content, assumed to be at index 0 - epg = json_source['media']['epg'][0]['items'] + #epg = json_source['media']['epg'][0]['items'] + epg = json_source['dates'][0]['games'][0]['broadcasts'] # define some default variables selected_content_id = None @@ -683,27 +684,27 @@ def stream_select(game_pk, spoiler='True', suspended='False', start_inning='Fals # and if it's not blacked out or the blackout time has passed if ((AUTO_SELECT_STREAM == 'true' and from_context_menu is False and suspended != 'archive') or autoplay is True) and (blackout == 'False' or (blackout != 'True' and blackout < now)): # loop through the streams to determine the best match - for item in epg: + for item in epg: # ignore streams that haven't started yet, audio streams (without a mediaFeedType), and in-market streams - if item['mediaState'] != 'MEDIA_OFF' and 'mediaFeedType' in item and not item['mediaFeedType'].startswith('IN_'): + if item['mediaState']['mediaStateCode'] != 'MEDIA_OFF' and 'mediaFeedType' in item and not item['mediaFeedType'].startswith('IN_'): # check if our favorite team (if defined) is associated with this stream # or if no favorite team match, look for the home or national streams if (FAV_TEAM != 'None' and 'mediaFeedSubType' in item and item['mediaFeedSubType'] == getFavTeamId()) or (selected_content_id is None and 'mediaFeedType' in item and (item['mediaFeedType'] == 'HOME' or item['mediaFeedType'] == 'NATIONAL' )): # prefer live streams (suspended games can have both a live and archived stream available) - if item['mediaState'] == 'MEDIA_ON': - selected_content_id = item['contentId'] - selected_media_state = item['mediaState'] - selected_call_letters = item['callLetters'] + if item['mediaState']['mediaStateCode'] == 'MEDIA_ON': + selected_content_id = item['mediaId'] + selected_media_state = item['mediaState']['mediaStateCode'] + selected_call_letters = item['callSign'] if 'mediaFeedType' in item: selected_media_type = item['mediaFeedType'] # once we've found a fav team live stream, we don't need to search any further if FAV_TEAM != 'None' and 'mediaFeedSubType' in item and item['mediaFeedSubType'] == getFavTeamId(): break # fall back to the first available archive stream, but keep search in case there is a live stream (suspended) - elif item['mediaState'] == 'MEDIA_ARCHIVE' and selected_content_id is None: - selected_content_id = item['contentId'] - selected_media_state = item['mediaState'] - selected_call_letters = item['callLetters'] + elif item['mediaState']['mediaStateCode'] == 'MEDIA_ARCHIVE' and selected_content_id is None: + selected_content_id = item['mediaId'] + selected_media_state = item['mediaState']['mediaStateCode'] + selected_call_letters = item['callSign'] if 'mediaFeedType' in item: selected_media_type = item['mediaFeedType'] @@ -739,9 +740,9 @@ def stream_select(game_pk, spoiler='True', suspended='False', start_inning='Fals airings = None game_date = None - # if no video, not live, if suspended, or if live and not resuming, add audio streams to the video streams - if len(json_source['media']['epg']) >= 3 and 'items' in json_source['media']['epg'][2] and (len(epg) == 0 or (epg[0]['mediaState'] != "MEDIA_ON" or suspended != 'False' or (epg[0]['mediaState'] == "MEDIA_ON" and sys.argv[3] != 'resume:true'))): - epg += json_source['media']['epg'][2]['items'] + # # if no video, not live, if suspended, or if live and not resuming, add audio streams to the video streams + # if len(json_source['media']['epg']) >= 3 and 'items' in json_source['media']['epg'][2] and (len(epg) == 0 or (epg[0]['mediaState'] != "MEDIA_ON" or suspended != 'False' or (epg[0]['mediaState'] == "MEDIA_ON" and sys.argv[3] != 'resume:true'))): + # epg += json_source['media']['epg'][2]['items'] for item in epg: #xbmc.log(str(item)) @@ -753,28 +754,18 @@ def stream_select(game_pk, spoiler='True', suspended='False', start_inning='Fals media_feed_type = str(item['type']) # only display if the stream is available (either live or archive) and not in_market - if item['mediaState'] != 'MEDIA_OFF' and not media_feed_type.startswith('IN_'): + if item['mediaState']['mediaStateCode'] != 'MEDIA_OFF' and not media_feed_type.startswith('IN_'): title = media_feed_type.title() title = title.replace('_', ' ') - - # add TV to video stream - if 'mediaFeedType' in item: - title += LOCAL_STRING(30392) - # add language to audio stream - else: - if item['language'] == 'en': - title += LOCAL_STRING(30394) - elif item['language'] == 'es': - title += LOCAL_STRING(30395) - title += LOCAL_STRING(30393) - title = title + " (" + item['callLetters'] + ")" + + title = f"{item['type']} ({item['callSign']})" # modify stream title based on suspension status, if necessary if suspended != 'False': suspended_label = 'partial' try: # if the game hasn't finished, we can simply tell the status from the mediaState - if suspended == 'live' and item['mediaState'] == 'MEDIA_ARCHIVE': + if suspended == 'live' and item['mediaState']['mediaStateCode'] == 'MEDIA_ARCHIVE': suspended_label = 'Suspended' elif suspended == 'live': suspended_label = 'Resumed' @@ -786,7 +777,7 @@ def stream_select(game_pk, spoiler='True', suspended='False', start_inning='Fals airings = get_airings_data(game_pk=game_pk) if game_date is not None and airings is not None and 'data' in airings and 'Airings' in airings['data']: for airing in airings['data']['Airings']: - if airing['contentId'] == item['contentId']: + if airing['contentId'] == item['mediaId']: # compare the start date of the airing with the provided game_date start_date = get_eastern_game_date(parse(airing['startDate'])) # same day means it is resumed @@ -813,24 +804,24 @@ def stream_select(game_pk, spoiler='True', suspended='False', start_inning='Fals # insert home/national video streams at the top of the list if 'mediaFeedType' in item and ('HOME' in title.upper() or 'NATIONAL' in title.upper()): - content_id.insert(0, item['contentId']) - media_state.insert(0, item['mediaState']) - call_letters.insert(0, item['callLetters']) + content_id.insert(0, item['mediaId']) + media_state.insert(0, item['mediaState']['mediaStateCode']) + call_letters.insert(0, item['callSign']) media_type.insert(0, media_feed_type) stream_title.insert(highlight_offset, title) # otherwise append other streams to end of list else: - content_id.append(item['contentId']) - media_state.append(item['mediaState']) - call_letters.append(item['callLetters']) + content_id.append(item['mediaId']) + media_state.append(item['mediaState']['mediaStateCode']) + call_letters.append(item['callSign']) media_type.append(media_feed_type) stream_title.append(title) # add an option to directly play live YouTube streams in YouTube add-on if 'youtube' in item and 'videoId' in item['youtube']: content_id.insert(0, item['youtube']['videoId']) - media_state.insert(0, item['mediaState']) - call_letters.insert(0, item['callLetters']) + media_state.insert(0, item['mediaState']['mediaStateCode']) + call_letters.insert(0, item['callSign']) media_type.insert(0, media_feed_type) stream_title.insert(highlight_offset, LOCAL_STRING(30414)) @@ -848,7 +839,7 @@ def stream_select(game_pk, spoiler='True', suspended='False', start_inning='Fals # stream selection elif n > -1 and stream_title[n] != LOCAL_STRING(30391): # check if selected stream is a radio stream - if LOCAL_STRING(30393) in stream_title[n]: + if "FM (" in stream_title[n] or "AM (" in stream_title[n]: stream_type = 'audio' # directly play live YouTube streams in YouTube add-on, if requested if stream_title[n] == LOCAL_STRING(30414): @@ -958,7 +949,7 @@ def stream_select(game_pk, spoiler='True', suspended='False', start_inning='Fals selected_media_type = 'HOME' for item in json_source['media']['epg'][2]['items']: if 'type' in item and item['type'] != selected_media_type and 'contentId' in item: - alt_stream_url, dummy_a, dummy_b, dummy_c = account.get_stream(item['contentId']) + alt_stream_url, dummy_a, dummy_b, dummy_c = account.get_stream(item['mediaId']) alt_stream_url = re.sub('/(master_radio_complete|master_radio)', '/48K/48_complete', alt_stream_url) if 'language' in item and item['language'] == 'en': alternate_english = alt_stream_url @@ -1470,17 +1461,17 @@ def get_blackout_status(game, regional_fox_games_exist): def check_regional_fox_games(games): fox_start_time = None regional_fox_games_exist = False - for game in games: - if 'content' in game and 'media' in game['content'] and 'epg' in game['content']['media']: - for epg in game['content']['media']['epg']: - if epg['title'] == 'MLBTV': - for item in epg['items']: - if item['callLetters'] == 'FOX': - if fox_start_time is not None and game['gameDate'] == fox_start_time: - regional_fox_games_exist = True - else: - fox_start_time = game['gameDate'] - break + try: + for game in games: + for broadcast in game['broadcasts']: + if broadcast['callSign'] == 'FOX': + if fox_start_time is not None and game['gameDate'] == fox_start_time: + regional_fox_games_exist = True + else: + fox_start_time = game['gameDate'] + break + except: + pass return regional_fox_games_exist