Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[plugin.video.mlbtv@matrix] 2024.7.22+matrix.1 #4543

Merged
merged 1 commit into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions plugin.video.mlbtv/addon.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.mlbtv" name="MLB.TV®" version="2024.3.29+matrix.1" provider-name="eracknaphobia, tonywagner">
<addon id="plugin.video.mlbtv" name="MLB.TV®" version="2024.8.6+matrix.1" provider-name="eracknaphobia, tonywagner">
<requires>
<import addon="xbmc.python" version="3.0.0"/>
<import addon="script.module.pytz" />
Expand All @@ -22,12 +22,7 @@
</description>
<disclaimer lang="en_GB">Requires an MLB.tv account</disclaimer>
<news>
- 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 highlights
</news>
<language>en</language>
<platform>all</platform>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -412,4 +412,12 @@ msgctxt "#30437"
msgid "Disable closed captions"
msgstr ""

msgctxt "#30438"
msgid "MLB Network"
msgstr ""

msgctxt "#30439"
msgid "MLB Network live stream"
msgstr ""


309 changes: 146 additions & 163 deletions plugin.video.mlbtv/resources/lib/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,15 @@
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.did = self.device_id()
self.util = Util()

def device_id(self):
if self.addon.getSetting('device_id') == '':
self.addon.setSetting('device_id', str(uuid.uuid4()))

return self.addon.getSetting('device_id')
self.media_url = 'https://media-gateway.mlb.com/graphql'

def login(self):
# Check if username and password are provided
Expand Down Expand Up @@ -63,6 +55,7 @@ def login(self):
login_token_expiry = datetime.now() + timedelta(seconds=int(r.json()['expires_in']))
self.addon.setSetting('login_token', login_token)
self.addon.setSetting('login_token_expiry', str(login_token_expiry))
self.get_device_session_id()
else:
dialog = xbmcgui.Dialog()
msg = LOCAL_STRING(30263)
Expand All @@ -71,26 +64,22 @@ def login(self):
dialog.notification(LOCAL_STRING(30262), msg, ICON, 5000, False)
self.addon.setSetting('login_token', '')
self.addon.setSetting('login_token_expiry', '')
self.addon.setSetting('device_id', '')
self.addon.setSetting('session_key', '')
self.addon.setSetting('entitlements', '')
sys.exit()


def logout(self):
self.util.delete_cookies()
self.addon.setSetting('login_token', '')
self.addon.setSetting('login_token_expiry', '')
self.addon.setSetting('device_id', '')
self.addon.setSetting('session_key', '')
self.addon.setSetting('entitlements', '')
self.addon.setSetting('username', '')
self.addon.setSetting('password', '')

def media_entitlement(self):
url = 'https://media-entitlement.mlb.com/api/v3/jwt?os=Android&appname=AtBat&did=' + self.device_id()
headers = {'User-Agent': UA_ANDROID,
'Authorization': 'Bearer ' + self.login_token()
}

r = requests.get(url, headers=headers, verify=self.verify)

return r.text

# need to login to access featured videos like Big Inning and MiLB games
def login_token(self):
if self.addon.getSetting('login_token_expiry') == '' or \
Expand All @@ -101,80 +90,72 @@ 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

def get_playback_url(self, content_id):
auth = self.access_token()
url = 'https://search-api-mlbtv.mlb.com/svc/search/v2/graphql/persisted/query/core/Airings' \
'?variables=%7B%22contentId%22%3A%22' + content_id + '%22%7D'

headers = {
'Accept': 'application/json',
'Authorization': 'Bearer ' + auth,
'X-BAMSDK-Version': 'v4.3.0',
'X-BAMSDK-Platform': 'android-tv',
'User-Agent': 'BAMSDK/v4.3.0 (mlbaseball-7993996e 8.1.0; v2.0/v4.3.0; android; tv)'
}

r = requests.get(url, headers=headers, cookies=self.util.load_cookies(), verify=self.verify)
if not r.ok:
dialog = xbmcgui.Dialog()
msg = ""
for item in r.json()['errors']:
msg += item['code'] + '\n'
dialog.notification(LOCAL_STRING(30270), msg, self.icon, 5000, False)
sys.exit()

json_source = r.json()

playback_url = json_source['data']['Airings'][0]['playbackUrls'][0]['href']

broadcast_start_offset = '1'
broadcast_start_timestamp = None
try:
# make sure we have milestone data
if 'data' in json_source and 'Airings' in json_source['data'] and len(json_source['data']['Airings']) > 0 and 'milestones' in json_source['data']['Airings'][0]:
for milestone in json_source['data']['Airings'][0]['milestones']:
if milestone['milestoneType'] == 'BROADCAST_START':
offset_index = 1
startDatetime_index = 0
if milestone['milestoneTime'][0]['type'] == 'offset':
offset_index = 0
startDatetime_index = 1
broadcast_start_offset = str(milestone['milestoneTime'][offset_index]['start'])
broadcast_start_timestamp = parse(milestone['milestoneTime'][startDatetime_index]['startDatetime']) - timedelta(seconds=milestone['milestoneTime'][offset_index]['start'])
break
except:
pass

return auth, playback_url, broadcast_start_offset, broadcast_start_timestamp
return self.login_token()

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')
if self.addon.getSetting('device_id') == '' or self.addon.getSetting('session_key') == '':
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": self.addon.getSetting('device_id'),
"sessionId": self.addon.getSetting('session_key')
}
}
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 = ""
Expand All @@ -183,78 +164,80 @@ 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)
return stream_url, headers

def get_device_session_id(self):
headers = {
'User-Agent': UA_PC,
'Authorization': 'Bearer ' + self.login_token(),
'Content-Type': 'application/json',
'Accept': 'application/json'
}

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

return stream_url
r = requests.post(self.media_url, headers=headers, json=data)
device_id = r.json()['data']['initSession']['deviceId']
session_id = r.json()['data']['initSession']['sessionId']
entitlements = []
for entitlement in r.json()['data']['initSession']['entitlements']:
entitlements.append(entitlement['code'])

self.addon.setSetting('device_id', device_id)
self.addon.setSetting('session_key', session_id)
self.addon.setSetting('entitlements', json.dumps(entitlements))

def get_entitlements(self):
if self.addon.getSetting('entitlements') == '':
self.get_device_session_id()
return self.addon.getSetting('entitlements')

def get_broadcast_start_time(self, stream_url):
try:
variant_url = stream_url.replace('.m3u8', '_5600K.m3u8')
r = requests.get(variant_url, headers={'User-Agent': UA_PC}, verify=self.verify)
content = r.text

line_array = content.splitlines()
for line in line_array:
if line.startswith('#EXT-X-PROGRAM-DATE-TIME:'):
return parse(line[25:])
except Exception as e:
xbmc.log('error getting get_broadcast_start_time ' + str(e))
return None
Loading
Loading