Skip to content
Closed
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
Binary file added plugin.video.mlbtv/.DS_Store
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this file.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do, Sorry for the late response. I have a few more changes that just came in so I will close this PR and start a new one.

Binary file not shown.
10 changes: 6 additions & 4 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="2025.3.18+matrix.1" provider-name="eracknaphobia, tonywagner">
<addon id="plugin.video.mlbtv" name="MLB.TV®" version="2025.4.1+matrix.1" provider-name="eracknaphobia, tonywagner">
<requires>
<import addon="xbmc.python" version="3.0.0"/>
<import addon="script.module.pytz" />
Expand All @@ -22,9 +22,11 @@
</description>
<disclaimer lang="en_GB">Requires an MLB.tv account</disclaimer>
<news>
- ability to change skip timing adjustment settings during playback
- initial 2025 updates
- fix for Japan exhibitions
- automatic blackout/entitlement detection
- support for SNLA and SNY subscriptions
- skip timing, game changer, and hide scores ticker updates for 2025
- jump to live when skipping start dialog and allowing spoilers
- restored Big Inning schedule
</news>
<language>en</language>
<platform>all</platform>
Expand Down
4 changes: 4 additions & 0 deletions plugin.video.mlbtv/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@
elif mode == 301:
featured_stream_select(featured_video, name, description, start_inning, game_pk)

# Linear channel stream select
elif mode == 302:
linear_channel_stream_select(featured_video, name, description)

# Logout
elif mode == 400:
from resources.lib.account import Account
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,4 +420,20 @@ msgctxt "#30439"
msgid "MLB Network live stream"
msgstr ""

msgctxt "#30440"
msgid "SportsNet LA"
msgstr ""

msgctxt "#30441"
msgid "SNLA live stream"
msgstr ""

msgctxt "#30442"
msgid "SNY"
msgstr ""

msgctxt "#30443"
msgid "SNY live stream"
msgstr ""


150 changes: 148 additions & 2 deletions plugin.video.mlbtv/resources/lib/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import time, uuid
import random
import string
import base64

if sys.version_info[0] > 2:
from urllib.parse import quote
Expand Down Expand Up @@ -67,6 +68,7 @@ def login(self):
self.addon.setSetting('device_id', '')
self.addon.setSetting('session_key', '')
self.addon.setSetting('entitlements', '')
self.addon.setSetting('okta_id', '')
sys.exit()


Expand All @@ -77,6 +79,7 @@ def logout(self):
self.addon.setSetting('device_id', '')
self.addon.setSetting('session_key', '')
self.addon.setSetting('entitlements', '')
self.addon.setSetting('okta_id', '')
self.addon.setSetting('username', '')
self.addon.setSetting('password', '')

Expand All @@ -92,7 +95,7 @@ def login_token(self):
def access_token(self):
return self.login_token()

def get_stream(self, content_id):
def get_playback(self, content_id):
if self.addon.getSetting('device_id') == '' or self.addon.getSetting('session_key') == '':
self.get_device_session_id()
headers = {
Expand Down Expand Up @@ -167,6 +170,12 @@ def get_stream(self, content_id):
stream_url = r.json()['data']['initPlaybackSession']['playback']['url']
xbmc.log(f'Stream URL: {stream_url}')
headers = 'User-Agent=' + UA_PC
token = r.json()['data']['initPlaybackSession']['playback']['token']
xbmc.log(f'Token: {token}')
return stream_url, headers, token

def get_stream(self, content_id):
stream_url, headers, token = self.get_playback(content_id)
return stream_url, headers

def get_device_session_id(self):
Expand Down Expand Up @@ -240,4 +249,141 @@ def get_broadcast_start_time(self, stream_url):
return parse(line[25:])
except Exception as e:
xbmc.log('error getting get_broadcast_start_time ' + str(e))
return None
return None

def okta_id(self):
if self.addon.getSetting('okta_id') == '':
self.get_okta_id()
return self.addon.getSetting('okta_id')

def get_okta_id(self):
try:
# get a new playback token for a past free game
url, headers, token = self.get_playback('b7f0fff7-266f-4171-aa2d-af7988dc9302')
if token:
encoded_okta_id = token.split('_')[1]
okta_id = base64.b64decode(encoded_okta_id.encode('ascii') + b'==').decode('ascii')
self.addon.setSetting('okta_id', okta_id)
except Exception as e:
xbmc.log('error getting okta_id ' + str(e))
sys.exit(0)

def get_event_stream(self, url):
xbmc.log('fetching event video stream from url')

headers = {
'User-Agent': UA_PC,
'Authorization': 'Bearer ' + self.login_token(),
'Accept': '*/*',
'Origin': 'https://www.mlb.com',
'Referer': 'https://www.mlb.com'
}
r = requests.get(url, headers=headers, verify=VERIFY)

text_source = r.text
#xbmc.log(text_source)

# sometimes the returned content is already a stream playlist
if text_source.startswith('#EXTM3U'):
xbmc.log('video url is already a stream playlist')
video_stream_url = url
# otherwise it is JSON containing the stream URL
else:
json_source = r.json()
if 'data' in json_source and len(json_source['data']) > 0 and 'value' in json_source['data'][0]:
video_stream_url = json_source['data'][0]['value']
xbmc.log('found video stream url : ' + video_stream_url)
return video_stream_url


def get_linear_stream(self, network):
if self.addon.getSetting('device_id') == '' or self.addon.getSetting('session_key') == '':
self.get_device_session_id()
url = 'https://media-gateway.mlb.com/graphql'
headers = {
'User-Agent': UA_PC,
'Authorization': 'Bearer ' + self.login_token(),
'Content-Type': 'application/json',
'Accept': 'application/json, text/plain, */*',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'en-US,en;q=0.5',
'connection': 'keep-alive',
'content-type': 'application/json',
'x-client-name': 'WEB',
'x-client-version': '7.8.1',
'cache-control': 'no-cache',
'origin': 'https://www.mlb.com',
'pragma': 'no-cache',
'priority': 'u=1, i',
'referer': 'https://www.mlb.com/',
'sec-ch-ua': '"Chromium";v="133", "Google Chrome";v="133", "Not-A.Brand";v="8"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"macOS"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-site'
}
data = {
"operationName": "contentCollections",
"query": '''query contentCollections(
$categories: [ContentGroupCategory!]
$includeRestricted: Boolean = false
$includeSpoilers: Boolean = false
$limit: Int = 10,
$skip: Int = 0\n ) {
contentCollections(
categories: $categories
includeRestricted: $includeRestricted
includeSpoilers: $includeSpoilers
limit: $limit
skip: $skip
) {
title
category
contents {
assetTrackingKey
contentDate
contentId
contentRestrictions
description
duration
language
mediaId
officialDate
title
mediaState {
state
mediaType
}
thumbnails {
thumbnailType
templateUrl
thumbnailUrl
}
}
}
}''',
"variables": {
"categories": network,
"limit": "25"
}
}
xbmc.log(str(data))
r = requests.post(url, headers=headers, json=data, verify=VERIFY)
xbmc.log(r.text)
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()

availableStreams = r.json()['data']['contentCollections'][0]['contents']
for stream in availableStreams:
try:
stream_url, headers = self.get_stream(stream['mediaId'])
xbmc.log(f'Stream URL: {stream_url}')
return stream_url
except:
pass
3 changes: 2 additions & 1 deletion plugin.video.mlbtv/resources/lib/globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@
FINAL = 'FF666666'
FREE = 'FF43CD80'


#Score ticker network prefix
SCORES_TICKER_NETWORK = 'FDSN'

#Images
ICON = os.path.join(ROOTDIR,"icon.png")
Expand Down
Loading