Skip to content

Commit 679e469

Browse files
[plugin.video.mlbtv] 2025.7.18+matrix.1 (#4691)
1 parent 36b9383 commit 679e469

9 files changed

Lines changed: 762 additions & 96 deletions

File tree

plugin.video.mlbtv/addon.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2-
<addon id="plugin.video.mlbtv" name="MLB.TV®" version="2025.6.19+matrix.1" provider-name="eracknaphobia, tonywagner">
2+
<addon id="plugin.video.mlbtv" name="MLB.TV®" version="2025.7.18+matrix.1" provider-name="eracknaphobia, tonywagner">
33
<requires>
44
<import addon="xbmc.python" version="3.0.0"/>
55
<import addon="script.module.pytz" />
@@ -23,6 +23,10 @@
2323
<disclaimer lang="en_GB">Requires an MLB.tv account</disclaimer>
2424
<news>
2525
- updated Big Inning schedule
26+
- fix for disable captions on catch up
27+
- fix and improvements for play all recaps option
28+
- support for single team packages in stream selection
29+
- support for Stream Finder (auto-switching based on custom criteria)
2630
</news>
2731
<language>en</language>
2832
<platform>all</platform>

plugin.video.mlbtv/main.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@
143143
search_txt = ''
144144
dialog = xbmcgui.Dialog()
145145
game_day = dialog.input('Enter date (yyyy-mm-dd)', type=xbmcgui.INPUT_ALPHANUM)
146-
mat = re.match('(\d{4})-(\d{2})-(\d{2})$', game_day)
146+
mat = re.match(r'(\d{4})-(\d{2})-(\d{2})$', game_day)
147147
if mat is not None:
148148
# Refresh will erase history, so navigating back won't bring up the date prompt again
149149
xbmc.executebuiltin('Container.Refresh("plugin://plugin.video.mlbtv/?mode=101&game_day='+game_day+'&start_inning='+str(start_inning)+'")')
@@ -180,6 +180,12 @@
180180
mlbmonitor = MLBMonitor()
181181
mlbmonitor.change_monitor(blackout.split(','))
182182

183+
# Stream Finder
184+
elif mode == 501:
185+
from resources.lib.mlbmonitor import MLBMonitor
186+
mlbmonitor = MLBMonitor()
187+
mlbmonitor.finder_monitor(blackout.split(','))
188+
183189
# play all recaps or condensed games for selected date
184190
elif mode == 900:
185191
playAllHighlights(stream_date)
156 KB
Loading

plugin.video.mlbtv/resources/language/resource.language.en_gb/strings.po

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ msgid "Choose Stream Quality"
197197
msgstr ""
198198

199199
msgctxt "#30375"
200-
msgid "Watch all the day's highlights for "
200+
msgid "Click the date to watch all recaps or condensed games for "
201201
msgstr ""
202202

203203
msgctxt "#30380"
@@ -436,4 +436,12 @@ msgctxt "#30443"
436436
msgid "SNY live stream"
437437
msgstr ""
438438

439+
msgctxt "#30444"
440+
msgid "Stream Finder"
441+
msgstr ""
442+
443+
msgctxt "#30445"
444+
msgid "Automatically switches between games according to your preferences. This addon is not affiliated with Baseball Reference, do not contact them for support. Visit http://bit.ly/bbrefsf to create and export your preferences, then upload and save them to Kodi at "
445+
msgstr ""
446+
439447

plugin.video.mlbtv/resources/lib/globals.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
PREV_ICON = os.path.join(ROOTDIR,"icon.png")
8080
NEXT_ICON = os.path.join(ROOTDIR,"icon.png")
8181
BLACK_IMAGE = os.path.join(ROOTDIR, "resources", "img", "black.png")
82+
STREAM_FINDER_ICON = os.path.join(ROOTDIR, "resources", "img", "stream_finder_icon.png")
8283

8384
API_URL = 'https://statsapi.mlb.com'
8485
#User Agents

plugin.video.mlbtv/resources/lib/mlb.py

Lines changed: 88 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ def todays_games(game_day, start_inning='False', sport=MLB_ID, teams='None'):
3636

3737
addDir('[B]<< %s[/B]' % LOCAL_STRING(30010), 101, PREV_ICON, FANART, prev_day.strftime("%Y-%m-%d"), start_inning, sport, teams)
3838

39-
date_display = '[B][I]' + colorString(display_day.strftime("%A, %m/%d/%Y"), GAMETIME_COLOR) + '[/I][/B]'
39+
# add recap note to past day titles
40+
recap_note = ''
41+
if sport == MLB_ID and game_day < today:
42+
recap_note = ' (watch all recaps)'
43+
date_display = '[B][I]' + colorString(display_day.strftime("%A, %m/%d/%Y") + recap_note, GAMETIME_COLOR) + '[/I][/B]'
4044

4145
addPlaylist(date_display, str(game_day), 900, ICON, FANART)
4246

@@ -130,6 +134,8 @@ def todays_games(game_day, start_inning='False', sport=MLB_ID, teams='None'):
130134
game_changer_start = game_changer_starts[1]
131135
game_changer_end = game_changer_starts[len(game_changer_starts) - 2]
132136
create_game_changer_listitem(blackouts, inprogress_exists, game_changer_start, game_changer_end)
137+
create_stream_finder_listitem(blackouts, inprogress_exists, game_changer_start, game_changer_end)
138+
133139

134140
try:
135141
for game in remaining_games:
@@ -684,7 +690,74 @@ def create_game_changer_listitem(blackouts, inprogress_exists, game_changer_star
684690
xbmcplugin.setContent(addon_handle, 'episodes')
685691

686692

693+
# display a Stream Finder item within a game list
694+
def create_stream_finder_listitem(blackouts, inprogress_exists, game_changer_start, game_changer_end):
695+
display_title = LOCAL_STRING(30444)
696+
697+
# format the time for display
698+
game_time = get_display_time(UTCToLocal(stringToDate(game_changer_start, "%Y-%m-%dT%H:%M:%SZ"))) + ' - ' + get_display_time(UTCToLocal(stringToDate(game_changer_end, "%Y-%m-%dT%H:%M:%SZ") + timedelta(hours=3) + timedelta(minutes=30)))
699+
700+
if inprogress_exists:
701+
display_title = LOCAL_STRING(30367) + LOCAL_STRING(30444)
702+
game_time = colorString(game_time, LIVE)
703+
704+
name = game_time + ' ' + display_title
705+
706+
desc = LOCAL_STRING(30445) + 'http://' + xbmc.getIPAddress() + ':43670'
707+
708+
# create the list item
709+
liz=xbmcgui.ListItem(name)
710+
liz.setInfo( type="Video", infoLabels={ "Title": name, 'plot': desc } )
711+
liz.setProperty("IsPlayable", "true")
712+
liz.setArt({'icon': STREAM_FINDER_ICON, 'thumb': STREAM_FINDER_ICON, 'fanart': FANART})
713+
u=sys.argv[0]+"?mode="+str(501)+"&name="+urllib.quote_plus(name)+"&description="+urllib.quote_plus(desc)+"&blackout="+urllib.quote_plus(','.join(blackouts))
714+
xbmcplugin.addDirectoryItem(handle=addon_handle,url=u,listitem=liz,isFolder=False)
715+
xbmcplugin.setContent(addon_handle, 'episodes')
716+
717+
687718
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'):
719+
# fetch the entitlements using the game_pk
720+
from .account import Account
721+
account = Account()
722+
login_token = account.login_token()
723+
okta_id = account.okta_id()
724+
725+
url = 'https://mastapi.mobile.mlbinfra.com/api/epg/v3/search?exp=MLB&gamePk=' + game_pk
726+
headers = {
727+
'accept': '*/*',
728+
'accept-language': 'en-US,en;q=0.9',
729+
'cache-control': 'no-cache',
730+
'content-type': 'application/json',
731+
'origin': 'https://www.mlb.com',
732+
'pragma': 'no-cache',
733+
'priority': 'u=1, i',
734+
'referer': 'https://www.mlb.com/',
735+
'sec-ch-ua': '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"',
736+
'sec-ch-ua-mobile': '?0',
737+
'sec-ch-ua-platform': '"macOS"',
738+
'sec-fetch-dest': 'empty',
739+
'sec-fetch-mode': 'cors',
740+
'sec-fetch-site': 'same-site',
741+
'user-agent': UA_PC
742+
}
743+
if login_token is not None and okta_id is not None:
744+
headers['authorization'] = 'Bearer ' + login_token
745+
headers['x-okta-id'] = okta_id
746+
r = requests.get(url,headers=headers, verify=VERIFY)
747+
json_source = r.json()
748+
entitled_feeds = []
749+
blackout_feeds = []
750+
if 'results' in json_source and len(json_source['results']) > 0:
751+
for result in json_source['results']:
752+
for feed in result['videoFeeds']:
753+
if feed['entitled'] == True:
754+
entitled_feeds.append(feed['mediaId'])
755+
if feed['blackedOut'] == True:
756+
blackout_feeds.append(feed['mediaId'])
757+
for feed in result['audioFeeds']:
758+
if feed['entitled'] == True:
759+
entitled_feeds.append(feed['mediaId'])
760+
688761
# fetch the epg content using the game_pk
689762
#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'
690763
url = f'{API_URL}/api/v1/schedule?gamePk={game_pk}&hydrate=broadcasts(all),game(content(highlights(highlights)))'
@@ -739,9 +812,9 @@ def stream_select(game_pk, spoiler='True', suspended='False', start_inning='Fals
739812
# ignore streams that haven't started yet, audio streams, and in-market streams
740813
if item['mediaState']['mediaStateCode'] != 'MEDIA_OFF' and item['type'] == 'TV': # and not item['mediaFeedType'].startswith('IN_'):
741814
# check if our favorite team (if defined) is associated with this stream
742-
# or if no favorite team match, look for the home or national streams
815+
# or if no favorite team match, prefer the home or national streams
743816
#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' )):
744-
if (FAV_TEAM != 'None' and ((item['homeAway'] == 'home' and str(json_source['dates'][0]['games'][0]['teams']['home']['team']['id']) == str(getFavTeamId())) or (item['homeAway'] == 'away' and str(json_source['dates'][0]['games'][0]['teams']['away']['team']['id']) == str(getFavTeamId())))) or (selected_content_id is None and (item['homeAway'] == 'home' or item['isNational'] == True )):
817+
if item['mediaId'] in entitled_feeds and item['mediaId'] not in blackout_feeds and ((FAV_TEAM != 'None' and ((item['homeAway'] == 'home' and str(json_source['dates'][0]['games'][0]['teams']['home']['team']['id']) == str(getFavTeamId())) or (item['homeAway'] == 'away' and str(json_source['dates'][0]['games'][0]['teams']['away']['team']['id']) == str(getFavTeamId())))) or (item['homeAway'] == 'home' or item['isNational'] == True ) or selected_content_id is None):
745818
# prefer live streams (suspended games can have both a live and archived stream available)
746819
if item['mediaState']['mediaStateCode'] == 'MEDIA_ON':
747820
selected_content_id = item['mediaId']
@@ -763,7 +836,7 @@ def stream_select(game_pk, spoiler='True', suspended='False', start_inning='Fals
763836

764837
# if coming from the game changer, just return a flag to indicate whether we need to start an overlay
765838
if overlay_check == 'True':
766-
if HIDE_SCORES_TICKER == 'true' and stream_type == 'video' and selected_call_letters.startswith(SCORES_TICKER_NETWORK):
839+
if HIDE_SCORES_TICKER == 'true' and stream_type == 'video' and selected_call_letters is not None and selected_call_letters.startswith(SCORES_TICKER_NETWORK):
767840
return True
768841
else:
769842
return False
@@ -847,14 +920,14 @@ def stream_select(game_pk, spoiler='True', suspended='False', start_inning='Fals
847920
title += ' (' + suspended_label + ')'
848921

849922
# display non-entitlement status for a stream, if applicable
850-
if blackout == 'True':
923+
if blackout == 'True' or item['mediaId'] not in entitled_feeds:
851924
title = blackoutString(title)
852925
title += ' (not entitled)'
853926
# display blackout status for video, if available
854-
elif item['type'] == 'TV' and blackout != 'False':
927+
elif item['type'] == 'TV' and (blackout != 'False' or item['mediaId'] in blackout_feeds):
855928
title = blackoutString(title)
856929
title += ' (blackout until ~'
857-
if blackout == 'True':
930+
if blackout == 'True' or item['mediaId'] in blackout_feeds:
858931
title += '2.5 hours after'
859932
else:
860933
blackout_display_time = get_display_time(UTCToLocal(blackout))
@@ -943,6 +1016,8 @@ def stream_select(game_pk, spoiler='True', suspended='False', start_inning='Fals
9431016
p = dialog.select(LOCAL_STRING(30396), start_options)
9441017
# catch up
9451018
if p == 0:
1019+
if DISABLE_CLOSED_CAPTIONS == 'true' and not stream_url.startswith('http://127.0.0.1:43670/'):
1020+
stream_url = 'http://127.0.0.1:43670/' + stream_url
9461021
# create an item for the video stream
9471022
listitem = stream_to_listitem(stream_url, headers, description, name, icon, fanart)
9481023
# pass along the highlights and the video stream item to play as a playlist and stop processing here
@@ -1015,12 +1090,15 @@ def stream_select(game_pk, spoiler='True', suspended='False', start_inning='Fals
10151090
broadcast_start_offset = '-1'
10161091
# if not live and no spoilers and not audio, generate a random number of segments to pad at end of proxy stream url
10171092
elif DISABLE_VIDEO_PADDING == 'false' and is_live is False and spoiler == 'False' and stream_type != 'audio':
1018-
pad = random.randint((3600 / SECONDS_PER_SEGMENT), (7200 / SECONDS_PER_SEGMENT))
1093+
pad = random.randint((3600 // SECONDS_PER_SEGMENT), (7200 // SECONDS_PER_SEGMENT))
10191094
# pass padding as URL querystring parameter
10201095
stream_url = 'http://127.0.0.1:43670/' + stream_url + '?pad=' + str(pad)
10211096

10221097
# valid stream url
10231098
if '.m3u8' in stream_url:
1099+
if DISABLE_CLOSED_CAPTIONS == 'true' and not stream_url.startswith('http://127.0.0.1:43670/'):
1100+
stream_url = 'http://127.0.0.1:43670/' + stream_url
1101+
10241102
play_stream(stream_url, headers, description, title=name, icon=icon, fanart=fanart, start=broadcast_start_offset, stream_type=stream_type, music_type_unset=from_context_menu)
10251103

10261104
# start the monitor if a skip type or start inning has been requested and we have a broadcast start timestamp
@@ -1379,12 +1457,12 @@ def playAllHighlights(stream_date):
13791457
for item in game['content']['highlights']['highlights']['items']:
13801458
try:
13811459
title = item['headline'].strip().lower()
1382-
if (n == 0 and (' vs ' in title or ' vs. ' in title or ' versus ' in title or ' at ' in title or '@' in title) and (title.endswith(' highlights') or title.endswith(' recap'))) or (n == 1 and title.includes('condensed')):
1460+
if (n == 0 and (' vs ' in title or ' vs. ' in title or ' versus ' in title or ' at ' in title or '@' in title) and (title.endswith(' highlights') or title.endswith(' recap'))) or (n == 1 and 'condensed' in title):
13831461
for playback in item['playbacks']:
13841462
if 'hlsCloud' in playback['name']:
13851463
clip_url = playback['url']
13861464
break
1387-
listitem = xbmcgui.ListItem(clip_url)
1465+
listitem = xbmcgui.ListItem(item['headline'])
13881466
icon = item['image']['cuts'][0]['src']
13891467
listitem.setArt({'icon': icon, 'thumb': icon, 'fanart': fanart})
13901468
listitem.setInfo(type="Video", infoLabels={"Title": item['headline']})

0 commit comments

Comments
 (0)