Skip to content

Commit

Permalink
wip #73 and #72
Browse files Browse the repository at this point in the history
  • Loading branch information
eracknaphobia committed Jul 22, 2024
1 parent b9d58a3 commit c8f9afa
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 170 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
*.pyo
*.pyo
*.pyc
10 changes: 3 additions & 7 deletions 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.7.22+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,8 @@
</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 invalid access error
- Restore radio feeds
</news>
<language>en</language>
<platform>all</platform>
Expand Down
219 changes: 117 additions & 102 deletions resources/lib/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -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') == '':
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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 = ""
Expand All @@ -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
11 changes: 3 additions & 8 deletions resources/lib/globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)'))):
Expand Down
Loading

0 comments on commit c8f9afa

Please sign in to comment.