mlbserver 2025.3.29 → 2025.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/index.js +76 -13
  2. package/package.json +1 -1
  3. package/session.js +169 -102
package/index.js CHANGED
@@ -1041,7 +1041,7 @@ app.get('/gamechanger.m3u8', async function(req, res) {
1041
1041
 
1042
1042
  for ( gamechanger_resolution in GAMECHANGER_RESOLUTIONS ) {
1043
1043
  if ( resolution == gamechanger_resolution ) {
1044
- body += '#EXT-X-STREAM-INF:BANDWIDTH=' + GAMECHANGER_RESOLUTIONS[gamechanger_resolution].bandwidth + '000,RESOLUTION=' + GAMECHANGER_RESOLUTIONS[gamechanger_resolution].resolution + ',FRAME-RATE=' + GAMECHANGER_RESOLUTIONS[gamechanger_resolution].frame_rate + ',CODECS="mp4a.40.2,avc1.' + GAMECHANGER_RESOLUTIONS[gamechanger_resolution].codec + '",CLOSED-CAPTIONS="cc",AUDIO="aac"' + '\n' + '/gamechangerplaylist?id=' + id + '&resolution=' + gamechanger_resolution + includeTeams + excludeTeams + content_protect + '\n'
1044
+ body += '#EXT-X-STREAM-INF:BANDWIDTH=' + GAMECHANGER_RESOLUTIONS[gamechanger_resolution].bandwidth + '000,RESOLUTION=' + GAMECHANGER_RESOLUTIONS[gamechanger_resolution].resolution + ',FRAME-RATE=' + GAMECHANGER_RESOLUTIONS[gamechanger_resolution].frame_rate + ',CODECS="mp4a.40.2,avc1.' + GAMECHANGER_RESOLUTIONS[gamechanger_resolution].codec + '",CLOSED-CAPTIONS="cc",AUDIO="aac"' + '\n' + '/gamechangerplaylist.m3u8?id=' + id + '&resolution=' + gamechanger_resolution + includeTeams + excludeTeams + content_protect + '\n'
1045
1045
  break
1046
1046
  }
1047
1047
  }
@@ -1055,10 +1055,10 @@ app.get('/gamechanger.m3u8', async function(req, res) {
1055
1055
 
1056
1056
 
1057
1057
  // Listen for gamechanger playlist requests
1058
- app.get('/gamechangerplaylist', async function(req, res) {
1058
+ app.get('/gamechangerplaylist.m3u8', async function(req, res) {
1059
1059
  if ( ! (await protect(req, res)) ) return
1060
1060
 
1061
- session.requestlog('gamechangerplaylist', req, true)
1061
+ session.requestlog('gamechangerplaylist.m3u8', req, true)
1062
1062
 
1063
1063
  let gamechangerAccess = new Date()
1064
1064
 
@@ -1562,11 +1562,11 @@ app.get('/', async function(req, res) {
1562
1562
 
1563
1563
  let currentDate = new Date()
1564
1564
 
1565
+ let entitlements = await session.getEntitlements()
1565
1566
  // MLB Network live stream for eligible USA subscribers
1566
1567
  try {
1567
- let entitlements = await session.getEntitlements()
1568
1568
  if ( entitlements.includes('MLBN') || entitlements.includes('EXECMLB') || entitlements.includes('MLBTVMLBNADOBEPASS') ) {
1569
- body += '<tr><td><span class="tooltip">MLB Network<span class="tooltiptext">MLB Network live stream is now available in the USA for paid MLBTV subscribers or as a paid add-on, in addition to authenticated TV subscribers. <a href="https://www.mlb.com/news/mlb-network-launches-direct-to-consumer-streaming-option">See here for more information</a>.</span></span></td><td>'
1569
+ body += '<tr><td><span class="tooltip">MLB Network<span class="tooltiptext">MLB Network live stream is now available in the USA for paid MLBTV subscribers or as a paid add-on, in addition to authenticated TV subscribers. <a href="https://support.mlb.com/s/article/MLB-Network-Streaming-FAQ">See here for more information</a>.</span></span></td><td>'
1570
1570
  let querystring = '?event=MLBN'
1571
1571
  let multiviewquerystring = querystring + '&resolution=' + DEFAULT_MULTIVIEW_RESOLUTION
1572
1572
  if ( linkType == VALID_LINK_TYPES[0] ) {
@@ -1589,6 +1589,58 @@ app.get('/', async function(req, res) {
1589
1589
  session.debuglog('MLB Network detect error : ' + e.message)
1590
1590
  }
1591
1591
 
1592
+ // SNLA live stream for entitled subscribers
1593
+ try {
1594
+ if ( entitlements.includes('SNLA_119') ) {
1595
+ body += '<tr><td><span class="tooltip">SportsNet LA<span class="tooltiptext">SNLA live stream for entitled subscribers. <a href="https://support.mlb.com/s/article/SNLA-Plus-Subscription-Packages">See here for more information</a>.</span></span></td><td>'
1596
+ let querystring = '?event=SNLA'
1597
+ let multiviewquerystring = querystring + '&resolution=' + DEFAULT_MULTIVIEW_RESOLUTION
1598
+ if ( linkType == VALID_LINK_TYPES[0] ) {
1599
+ if ( startFrom != VALID_START_FROM[0] ) querystring += '&startFrom=' + startFrom
1600
+ if ( controls != VALID_CONTROLS[0] ) querystring += '&controls=' + controls
1601
+ }
1602
+ if ( resolution != VALID_RESOLUTIONS[0] ) querystring += '&resolution=' + resolution
1603
+ if ( linkType == VALID_LINK_TYPES[1] ) {
1604
+ if ( force_vod != VALID_FORCE_VOD[0] ) querystring += '&force_vod=' + force_vod
1605
+ } else if ( linkType == VALID_LINK_TYPES[4] ) {
1606
+ querystring += '&filename=' + gameDate + ' SNLA'
1607
+ }
1608
+ querystring += content_protect_b
1609
+ multiviewquerystring += content_protect_b
1610
+ body += '<a href="' + thislink + querystring + '">SNLA</a>'
1611
+ body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)">'
1612
+ body += '</td></tr>' + "\n"
1613
+ } // end entitlements check
1614
+ } catch (e) {
1615
+ session.debuglog('SNLA detect error : ' + e.message)
1616
+ }
1617
+
1618
+ // SNY live stream for entitled subscribers
1619
+ try {
1620
+ if ( entitlements.includes('SNY_121') ) {
1621
+ body += '<tr><td><span class="tooltip">SNY<span class="tooltiptext">SNY live stream for entitled subscribers. <a href="https://support.mlb.com/s/article/SNY-In-Market-Offering">See here for more information</a>.</span></span></td><td>'
1622
+ let querystring = '?event=SNY'
1623
+ let multiviewquerystring = querystring + '&resolution=' + DEFAULT_MULTIVIEW_RESOLUTION
1624
+ if ( linkType == VALID_LINK_TYPES[0] ) {
1625
+ if ( startFrom != VALID_START_FROM[0] ) querystring += '&startFrom=' + startFrom
1626
+ if ( controls != VALID_CONTROLS[0] ) querystring += '&controls=' + controls
1627
+ }
1628
+ if ( resolution != VALID_RESOLUTIONS[0] ) querystring += '&resolution=' + resolution
1629
+ if ( linkType == VALID_LINK_TYPES[1] ) {
1630
+ if ( force_vod != VALID_FORCE_VOD[0] ) querystring += '&force_vod=' + force_vod
1631
+ } else if ( linkType == VALID_LINK_TYPES[4] ) {
1632
+ querystring += '&filename=' + gameDate + ' SNY'
1633
+ }
1634
+ querystring += content_protect_b
1635
+ multiviewquerystring += content_protect_b
1636
+ body += '<a href="' + thislink + querystring + '">SNY</a>'
1637
+ body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)">'
1638
+ body += '</td></tr>' + "\n"
1639
+ } // end entitlements check
1640
+ } catch (e) {
1641
+ session.debuglog('SNY detect error : ' + e.message)
1642
+ }
1643
+
1592
1644
  if ( (mediaType == 'MLBTV') && ((level_ids == levels['MLB']) || level_ids.startsWith(levels['MLB'] + ',')) ) {
1593
1645
  // Recap Rundown beginning in 2023, disabled because it stopped working
1594
1646
  /*if ( (gameDate <= yesterday) && (gameDate >= '2023-03-31') && cache_data.dates && cache_data.dates[0] && cache_data.dates[0].games && (cache_data.dates[0].games.length > 0) ) {
@@ -1618,12 +1670,13 @@ app.get('/', async function(req, res) {
1618
1670
  //big_inning = await session.generateBigInningSchedule(gameDate)
1619
1671
  }
1620
1672
  if ( big_inning && big_inning.start ) {
1621
- body += '<tr><td><span class="tooltip">' + new Date(big_inning.start).toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + ' - ' + new Date(big_inning.end).toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + '<span class="tooltiptext">Big Inning is the live look-in and highlights show. <a href="https://www.mlb.com/live-stream-games/big-inning">See here for more information</a>.</span></span></td><td>'
1673
+ body += '<tr><td><span class="tooltip">' + new Date(big_inning.start).toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + ' - ' + new Date(big_inning.end).toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + '<span class="tooltiptext">Big Inning is the live look-in and highlights show. <a href="https://support.mlb.com/s/article/What-Is-MLB-Big-Inning">See here for more information</a>.</span></span></td><td>'
1622
1674
  let compareStart = new Date(big_inning.start)
1623
1675
  compareStart.setMinutes(compareStart.getMinutes()-10)
1624
1676
  let compareEnd = new Date(big_inning.end)
1625
1677
  compareEnd.setHours(compareEnd.getHours()+1)
1626
1678
  if ( (currentDate >= compareStart) && (currentDate < compareEnd) ) {
1679
+ body += '<tr><td><span class="tooltip">Big Inning<span class="tooltiptext">Big Inning is the live look-in and highlights show. <a href="https://support.mlb.com/s/article/What-Is-MLB-Big-Inning">See here for more information</a>.</span></span></td><td>'
1627
1680
  let querystring = '?event=biginning'
1628
1681
  let multiviewquerystring = querystring + '&resolution=' + DEFAULT_MULTIVIEW_RESOLUTION
1629
1682
  if ( linkType == VALID_LINK_TYPES[0] ) {
@@ -1659,15 +1712,15 @@ app.get('/', async function(req, res) {
1659
1712
  compareEnd.setHours(compareEnd.getHours()+4)
1660
1713
  body += '<tr><td><span class="tooltip">' + compareStart.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + ' - ' + compareEnd.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + '<span class="tooltiptext">The game changer stream will automatically switch between the highest leverage active live non-blackout games, and should be available whenever there are such games available. Does not support adaptive bitrate switching, will default to 720p60 resolution if not specified.</span></span></td><td>'
1661
1714
  if ( (currentDate >= compareStart) && (currentDate < compareEnd) ) {
1662
- let streamURL = server + '/gamechanger.m3u8'
1663
- let multiviewquerystring = streamURL + '?resolution=' + DEFAULT_MULTIVIEW_RESOLUTION + content_protect_b
1715
+ let streamURL = server + '/gamechanger.m3u8?'
1716
+ let multiviewquerystring = streamURL + 'resolution=' + DEFAULT_MULTIVIEW_RESOLUTION + content_protect_b
1664
1717
  streamURL += content_protect_a
1665
- if ( resolution != VALID_RESOLUTIONS[0] ) streamURL += '&resolution=' + resolution
1718
+ if ( resolution != VALID_RESOLUTIONS[0] ) streamURL += 'resolution=' + resolution + '&'
1666
1719
  if ( linkType != VALID_LINK_TYPES[1] ) {
1667
- streamURL = thislink + '?src=' + encodeURIComponent(streamURL) + '&startFrom=' + VALID_START_FROM[1] + content_protect_b
1720
+ streamURL = thislink + '?src=' + encodeURIComponent(streamURL) + 'startFrom=' + VALID_START_FROM[1] + content_protect_b + '&'
1668
1721
  }
1669
1722
  if ( linkType == VALID_LINK_TYPES[4] ) {
1670
- streamURL += '&filename=' + gameDate + ' Game Changer'
1723
+ streamURL += 'filename=' + gameDate + ' Game Changer' + '&'
1671
1724
  }
1672
1725
  body += '<a href="' + streamURL + '">Game Changer</a>'
1673
1726
  body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + multiviewquerystring + '" onclick="addmultiview(this, [], excludeTeams)">'
@@ -2196,7 +2249,7 @@ app.get('/', async function(req, res) {
2196
2249
  resolution = 'best'
2197
2250
  }
2198
2251
 
2199
- body += '<p><span class="tooltip">All<span class="tooltiptext">Will include all live MLB broadcasts (all games plus MLB Network, Big Inning, Game Changer, and Multiview). If favorite team(s) have been provided, it will also include affiliate games for those organizations. Channels/games subject to blackout will be omitted by default. See below for an additional option to override that.</span></span>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + content_protect_b + '">channels.m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + content_protect_b + '">guide.xml</a> and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + content_protect_b + '">calendar.ics</a></p>' + "\n"
2252
+ body += '<p><span class="tooltip">All<span class="tooltiptext">Will include all entitled live MLB broadcasts (games plus Big Inning, Game Changer, and Multiview, as well as MLB Network, SNLA, and/or SNY as appropriate). If favorite team(s) have been provided, it will also include affiliate games for those organizations. Channels/games subject to blackout will be omitted by default. See below for an additional option to override that.</span></span>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + content_protect_b + '">channels.m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + content_protect_b + '">guide.xml</a> and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + content_protect_b + '">calendar.ics</a></p>' + "\n"
2200
2253
 
2201
2254
  let include_teams = 'ath,national'
2202
2255
  if ( session.credentials.fav_teams.length > 0 ) {
@@ -2211,7 +2264,17 @@ app.get('/', async function(req, res) {
2211
2264
 
2212
2265
  body += '<p><span class="tooltip">Include (or exclude) LIDOM<span class="tooltiptext">Dominican Winter League, aka Liga de Beisbol Dominicano. Live stream only, does not support starting from the beginning or certain innings, skip options, etc.</span></span>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=lidom' + content_protect_b + '">m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=lidom' + content_protect_b + '">xml</a> and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=lidom' + content_protect_b + '">ics</a></p>' + "\n"
2213
2266
 
2214
- body += '<p><span class="tooltip">Include (or exclude) MLB Network<span class="tooltiptext">MLB Network live stream is now available in the USA for paid MLBTV subscribers or as a paid add-on, in addition to authenticated TV subscribers. <a href="https://www.mlb.com/news/mlb-network-launches-direct-to-consumer-streaming-option">See here for more information</a>.</span></span>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=mlbn' + content_protect_b + '">m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=mlbn' + content_protect_b + '">xml</a> and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=mlb' + content_protect_b + '">ics</a></p>' + "\n"
2267
+ if ( entitlements.includes('MLBN') || entitlements.includes('EXECMLB') || entitlements.includes('MLBTVMLBNADOBEPASS') ) {
2268
+ body += '<p><span class="tooltip">Include (or exclude) MLB Network<span class="tooltiptext">MLB Network live stream is now available in the USA for paid MLBTV subscribers or as a paid add-on, in addition to authenticated TV subscribers. <a href="https://support.mlb.com/s/article/MLB-Network-Streaming-FAQ">See here for more information</a>.</span></span>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=mlbn' + content_protect_b + '">m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=mlbn' + content_protect_b + '">xml</a></p>' + "\n"
2269
+ }
2270
+
2271
+ if ( entitlements.includes('SNLA_119') ) {
2272
+ body += '<p><span class="tooltip">Include (or exclude) SportsNet LA<span class="tooltiptext">SNLA live stream for entitled subscribers. <a href="https://support.mlb.com/s/article/SNLA-Plus-Subscription-Packages">See here for more information</a>.</span></span>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=snla' + content_protect_b + '">m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=snla' + content_protect_b + '">xml</a></p>' + "\n"
2273
+ }
2274
+
2275
+ if ( entitlements.includes('SNY_121') ) {
2276
+ body += '<p><span class="tooltip">Include (or exclude) SNY<span class="tooltiptext">SNY live stream for entitled subscribers. <a href="https://support.mlb.com/s/article/SNLA-Plus-Subscription-Packages">See here for more information</a>.</span></span>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=sny' + content_protect_b + '">m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=sny' + content_protect_b + '">xml</a></p>' + "\n"
2277
+ }
2215
2278
 
2216
2279
  body += '<p><span class="tooltip">Include (or exclude) Big Inning<span class="tooltiptext">Big Inning is the live look-in and highlights show. <a href="https://www.mlb.com/live-stream-games/big-inning">See here for more information</a>.</span></span>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=biginning' + content_protect_b + '">m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=biginning' + content_protect_b + '">xml</a> and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=biginning' + content_protect_b + '">ics</a></p>' + "\n"
2217
2280
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mlbserver",
3
- "version": "2025.03.29",
3
+ "version": "2025.04.01",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
package/session.js CHANGED
@@ -38,12 +38,16 @@ const WINTER_LEAGUES = [AFL_ID, LIDOM_ID]
38
38
  const BREAK_TYPES = ['Game Advisory', 'Pitching Substitution', 'Offensive Substitution', 'Defensive Sub', 'Defensive Switch', 'Runner Placed On Base']
39
39
  // These are the events to keep, in addition to the last event of each at-bat, if we're skipping pitches
40
40
  const ACTION_TYPES = ['Wild Pitch', 'Passed Ball', 'Stolen Base', 'Caught Stealing', 'Pickoff', 'Error', 'Out', 'Balk', 'Defensive Indiff', 'Other Advance']
41
+ // These are some idle events to skip
42
+ const IDLE_TYPES = ['Mound Visit', 'Batter Timeout', 'Pitcher Step Off', 'challenge']
41
43
  const EVENT_START_PADDING = -3
42
- const PITCH_END_PADDING = -4
44
+ const PITCH_END_PADDING = 2
43
45
  const ACTION_END_PADDING = 7
44
46
  const MINIMUM_BREAK_DURATION = 5
45
47
  // extra padding for MLB events (2025)
46
48
  const MLB_PADDING = 39
49
+ // extra Game Changer padding for MLB (2025)
50
+ const MLB_GAMECHANGER_PADDING = 20
47
51
 
48
52
  const LI_TABLE = {
49
53
  1: {
@@ -1269,7 +1273,7 @@ class sessionClass {
1269
1273
  this.save_cache_data()
1270
1274
  }
1271
1275
 
1272
- cacheStreamURL(mediaId, streamURL, streamURLToken='', streamURLExpiration='') {
1276
+ cacheStreamURL(mediaId, streamURL, streamURLToken='', streamURLExpiration='', rawStreamURL='') {
1273
1277
  this.createMediaCache(mediaId)
1274
1278
  this.cache.media[mediaId].streamURL = streamURL
1275
1279
  if (streamURLToken != '') {
@@ -1282,6 +1286,7 @@ class sessionClass {
1282
1286
  } else {
1283
1287
  this.cache.media[mediaId].streamURLExpiry = new Date(streamURLExpiration)
1284
1288
  }
1289
+ this.cache.media[mediaId].rawStreamURL = rawStreamURL
1285
1290
  this.save_cache_data()
1286
1291
  }
1287
1292
 
@@ -1572,6 +1577,9 @@ class sessionClass {
1572
1577
  if ( this.cache.media && this.cache.media[mediaId] && this.cache.media[mediaId].streamURL && this.cache.media[mediaId].streamURLToken && this.cache.media[mediaId].streamURLExpiry && (Date.parse(this.cache.media[mediaId].streamURLExpiry) > new Date()) ) {
1573
1578
  this.debuglog('using cached streamURL and token')
1574
1579
  let streamInfo = {streamURL: this.cache.media[mediaId].streamURL, streamURLToken: this.cache.media[mediaId].streamURLToken}
1580
+ if ( this.cache.media[mediaId].rawStreamURL ) {
1581
+ streamInfo['rawStreamURL'] = this.cache.media[mediaId].rawStreamURL
1582
+ }
1575
1583
  return streamInfo
1576
1584
  } else if ( this.cache.media && this.cache.media[mediaId] && this.cache.media[mediaId].blackout && this.cache.media[mediaId].blackoutExpiry && (Date.parse(this.cache.media[mediaId].blackoutExpiry) > new Date()) ) {
1577
1585
  this.log('mediaId recently blacked out, skipping')
@@ -1611,17 +1619,17 @@ class sessionClass {
1611
1619
  var response = await this.httpPost(reqObj)
1612
1620
  if ( response ) {
1613
1621
  this.debuglog('getStreamURL response : ' + JSON.stringify(response))
1614
- if ( response.data && response.data.initPlaybackSession && response.data.initPlaybackSession.playback && response.data.initPlaybackSession.playback.url && response.data.initPlaybackSession.playback.token && response.data.initPlaybackSession.playback.expiration ) {
1622
+ if ( response.data && response.data.initPlaybackSession && response.data.initPlaybackSession.playback && response.data.initPlaybackSession.playback.url ) {
1615
1623
  let rawStreamURL = response.data.initPlaybackSession.playback.url
1616
1624
  this.debuglog('getStreamURL rawStreamURL : ' + rawStreamURL)
1617
1625
  let streamURL = rawStreamURL.replace(/[\/]([A-Za-z0-9_]+)[\/]/, '/')
1618
- let streamURLToken = response.data.initPlaybackSession.playback.token
1619
- let streamURLExpiration = response.data.initPlaybackSession.playback.expiration
1626
+ let streamURLToken = response.data.initPlaybackSession.playback.token
1627
+ let streamURLExpiration = response.data.initPlaybackSession.playback.expiration
1620
1628
  this.debuglog('getStreamURL streamURL : ' + streamURL)
1621
1629
  this.debuglog('getStreamURL token : ' + streamURLToken)
1622
1630
  this.debuglog('getStreamURL expiration : ' + streamURLExpiration)
1623
- this.cacheStreamURL(mediaId, streamURL, streamURLToken, streamURLExpiration)
1624
- let streamInfo = {streamURL: streamURL, streamURLToken: streamURLToken}
1631
+ this.cacheStreamURL(mediaId, streamURL, streamURLToken, streamURLExpiration, rawStreamURL)
1632
+ let streamInfo = {streamURL: streamURL, streamURLToken: streamURLToken, rawStreamURL: rawStreamURL}
1625
1633
  return streamInfo
1626
1634
  } else {
1627
1635
  this.log('getStreamURL streamURL not found')
@@ -2588,9 +2596,10 @@ class sessionClass {
2588
2596
  channels = this.sortObj(channels)
2589
2597
  channels = Object.assign(channels, nationalChannels)
2590
2598
 
2599
+
2600
+ let entitlements = await this.getEntitlements()
2591
2601
  // MLB Network live stream for eligible USA subscribers
2592
2602
  try {
2593
- let entitlements = await this.getEntitlements()
2594
2603
  if ( (entitlements.includes('MLBN') || entitlements.includes('EXECMLB') || entitlements.includes('MLBTVMLBNADOBEPASS')) ) {
2595
2604
  if ( (mediaType == 'MLBTV') && ((includeLevels.length == 0) || includeLevels.includes('MLB') || includeLevels.includes('ALL')) ) {
2596
2605
  if ( (excludeTeams.length > 0) && excludeTeams.includes('MLBN') ) {
@@ -2620,6 +2629,70 @@ class sessionClass {
2620
2629
  } catch (e) {
2621
2630
  this.debuglog('getTVData MLB Network detect error : ' + e.message)
2622
2631
  }
2632
+
2633
+ // SNLA live stream for entitled subscribers
2634
+ try {
2635
+ if ( (entitlements.includes('SNLA_119')) ) {
2636
+ if ( (mediaType == 'MLBTV') && ((includeLevels.length == 0) || includeLevels.includes('MLB') || includeLevels.includes('ALL')) ) {
2637
+ if ( (excludeTeams.length > 0) && excludeTeams.includes('SNLA') ) {
2638
+ // do nothing
2639
+ } else if ( (includeTeams.length == 0) || includeTeams.includes('SNLA') ) {
2640
+ this.debuglog('getTVData processing SNLA')
2641
+ let logo = 'https://img.mlbstatic.com/mlb-images/image/upload/t_w640/mlb/fnwk2k0kgn1j8r8vvx3d.png'
2642
+ let channelid = mediaType + '.SNLA'
2643
+ //if ( this.protection.content_protect ) logo += '&amp;content_protect=' + this.protection.content_protect
2644
+ let stream = server + '/stream.m3u8?event=snla&mediaType=Video&resolution=' + resolution
2645
+ if ( this.protection.content_protect ) stream += '&content_protect=' + this.protection.content_protect
2646
+ if ( pipe == 'true' ) stream = await this.convert_stream_to_pipe(stream, channelid)
2647
+ channels[channelid] = await this.create_channel_object(channelid, logo, stream, mediaType)
2648
+
2649
+ let title = 'SportsNet LA'
2650
+ let description = 'Live stream of SNLA'
2651
+
2652
+ let start = this.convertDateToXMLTV(new Date(cache_data.dates[0].date + ' 00:00:00'))
2653
+ let stop = this.convertDateToXMLTV(new Date(cache_data.dates[cache_data.dates.length-1].date + ' 00:00:00'))
2654
+
2655
+ // SNLA guide XML
2656
+ programs += await this.generate_xml_program(channelid, start, stop, title, description, logo, this.convertStringToAirDate(cache_data.dates[0].date))
2657
+ this.debuglog('getTVData completed SNLA')
2658
+ } // end includeTeams check
2659
+ } // end mediaType check
2660
+ } // end entitlements check
2661
+ } catch (e) {
2662
+ this.debuglog('getTVData SNLA detect error : ' + e.message)
2663
+ }
2664
+
2665
+ // SNY live stream for entitled subscribers
2666
+ try {
2667
+ if ( (entitlements.includes('SNY_121')) ) {
2668
+ if ( (mediaType == 'MLBTV') && ((includeLevels.length == 0) || includeLevels.includes('MLB') || includeLevels.includes('ALL')) ) {
2669
+ if ( (excludeTeams.length > 0) && excludeTeams.includes('SNY') ) {
2670
+ // do nothing
2671
+ } else if ( (includeTeams.length == 0) || includeTeams.includes('SNY') ) {
2672
+ this.debuglog('getTVData processing SNY')
2673
+ let logo = 'https://img.mlbstatic.com/mlb-images/image/upload/t_w640/mlb/le5jifzo6oylxtnuf0m1.png'
2674
+ let channelid = mediaType + '.SNY'
2675
+ //if ( this.protection.content_protect ) logo += '&amp;content_protect=' + this.protection.content_protect
2676
+ let stream = server + '/stream.m3u8?event=sny&mediaType=Video&resolution=' + resolution
2677
+ if ( this.protection.content_protect ) stream += '&content_protect=' + this.protection.content_protect
2678
+ if ( pipe == 'true' ) stream = await this.convert_stream_to_pipe(stream, channelid)
2679
+ channels[channelid] = await this.create_channel_object(channelid, logo, stream, mediaType)
2680
+
2681
+ let title = 'SNY'
2682
+ let description = 'Live stream of SNY'
2683
+
2684
+ let start = this.convertDateToXMLTV(new Date(cache_data.dates[0].date + ' 00:00:00'))
2685
+ let stop = this.convertDateToXMLTV(new Date(cache_data.dates[cache_data.dates.length-1].date + ' 00:00:00'))
2686
+
2687
+ // SNLA guide XML
2688
+ programs += await this.generate_xml_program(channelid, start, stop, title, description, logo, this.convertStringToAirDate(cache_data.dates[0].date))
2689
+ this.debuglog('getTVData completed SNY')
2690
+ } // end includeTeams check
2691
+ } // end mediaType check
2692
+ } // end entitlements check
2693
+ } catch (e) {
2694
+ this.debuglog('getTVData SNY detect error : ' + e.message)
2695
+ }
2623
2696
 
2624
2697
  // Big Inning
2625
2698
  if ( (mediaType == 'MLBTV') && ((includeLevels.length == 0) || includeLevels.includes('MLB') || includeLevels.includes('ALL')) ) {
@@ -2638,6 +2711,7 @@ class sessionClass {
2638
2711
  let title = 'MLB Big Inning'
2639
2712
  let description = 'Live look-ins and big moments from around the league'
2640
2713
 
2714
+ // disabled Big Inning schedule scraping March 2025
2641
2715
  for (var i = 0; i < cache_data.dates.length; i++) {
2642
2716
  // Scraped Big Inning schedule
2643
2717
  if ( (cache_data.dates[i].date >= today) && cache_data.dates[i].games && (cache_data.dates[i].games.length > 1) && cache_data.dates[i].games[0] && (cache_data.dates[i].games[0].seriesDescription == 'Regular Season') ) {
@@ -2655,9 +2729,9 @@ class sessionClass {
2655
2729
  let stop = this.convertDateToXMLTV(new Date(this.cache.bigInningSchedule[gameDate].end))
2656
2730
 
2657
2731
  // Generated Big Inning schedule (disabled)
2658
- /*let big_inning = await this.generateBigInningSchedule(gameDate)
2659
- let start = this.convertDateToXMLTV(new Date(big_inning.start))
2660
- let stop = this.convertDateToXMLTV(new Date(big_inning.end))*/
2732
+ //let big_inning = await this.generateBigInningSchedule(gameDate)
2733
+ //let start = this.convertDateToXMLTV(new Date(big_inning.start))
2734
+ //let stop = this.convertDateToXMLTV(new Date(big_inning.end))
2661
2735
 
2662
2736
  // Big Inning calendar ICS
2663
2737
  let prefix = 'Watch'
@@ -2670,6 +2744,12 @@ class sessionClass {
2670
2744
  }
2671
2745
  this.debuglog('getTVData completed Big Inning for date ' + cache_data.dates[i].date)
2672
2746
  }
2747
+
2748
+ // generic Big Inning guide XML
2749
+ /*let start = this.convertDateToXMLTV(new Date(cache_data.dates[0].date + ' 00:00:00'))
2750
+ let stop = this.convertDateToXMLTV(new Date(cache_data.dates[cache_data.dates.length-1].date + ' 00:00:00'))
2751
+ programs += await this.generate_xml_program(channelid, start, stop, title, description, logo, this.convertStringToAirDate(cache_data.dates[0].date))*/
2752
+
2673
2753
  this.debuglog('getTVData completed Big Inning')
2674
2754
  }
2675
2755
  }
@@ -3024,8 +3104,11 @@ class sessionClass {
3024
3104
  event_end_padding = pitch_end_padding
3025
3105
  }
3026
3106
  let action_index
3027
- // skip type 1 (breaks) && 2 (idle time) will look at all plays with an endTime
3028
- if ((skip_type <= 2) && cache_data.liveData.plays.allPlays[i].playEvents[j].endTime) {
3107
+ // skip type 1 (breaks) will look at all plays with an endTime
3108
+ if ((skip_type == 1) && cache_data.liveData.plays.allPlays[i].playEvents[j].endTime) {
3109
+ action_index = j
3110
+ // skip type 2 (idle time) will look at all non-idle plays with an endTime
3111
+ } else if ((skip_type == 2) && cache_data.liveData.plays.allPlays[i].playEvents[j].endTime && (!cache_data.liveData.plays.allPlays[i].playEvents[j].details || !cache_data.liveData.plays.allPlays[i].playEvents[j].details.description || !IDLE_TYPES.some(v => cache_data.liveData.plays.allPlays[i].playEvents[j].details.description.includes(v)))) {
3029
3112
  action_index = j
3030
3113
  } else if (skip_type == 3) {
3031
3114
  // skip type 3 excludes non-action pitches (events that aren't last in the at-bat and don't fall under action types)
@@ -3116,17 +3199,15 @@ class sessionClass {
3116
3199
  this.debuglog('getBigInningSchedule')
3117
3200
 
3118
3201
  // temporarily disable Big Inning schedule checking until a new source URL is available
3119
- this.cache.bigInningSchedule = {}
3120
- return
3202
+ /*this.cache.bigInningSchedule = {}
3203
+ return*/
3121
3204
 
3122
3205
  let currentDate = new Date()
3123
3206
  if ( !this.cache || !this.cache.bigInningScheduleCacheExpiry || (currentDate > new Date(this.cache.bigInningScheduleCacheExpiry)) ) {
3124
3207
  if ( !this.cache.bigInningSchedule ) this.cache.bigInningSchedule = {}
3125
3208
  let reqObj = {
3126
- //url: 'https://www.mlb.com/live-stream-games/big-inning',
3127
- url: 'https://www.mlb.com/live-stream-games/help-center/subscription-access-big-inning',
3209
+ url: 'https://www.fubo.tv/welcome/channel/mlb-big-inning',
3128
3210
  headers: {
3129
- 'authority': 'www.mlb.com',
3130
3211
  'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
3131
3212
  'accept-language': 'en-US,en;q=0.9',
3132
3213
  'cache-control': 'no-cache',
@@ -3149,89 +3230,15 @@ class sessionClass {
3149
3230
  if ( response ) {
3150
3231
  // disabled because it's very big!
3151
3232
  //this.debuglog(response)
3152
- // break HTML into array based on table rows
3153
- var rows = response.split('<tr>')
3154
- // start iterating at 2 (after header row)
3155
- for (var i=2; i<rows.length; i++) {
3156
- // split HTML row into array with columns
3157
- let cols = rows[i].split('<td>')
3158
-
3159
- // define some variables that persist for each row
3160
- let parts
3161
- let year
3162
- let month
3163
- let day
3164
- let this_datestring
3165
- let add_date = 0
3166
- let d
3167
-
3168
- for (var j=1; j<cols.length; j++) {
3169
- // split on closing bracket to get column text at resulting array index 0
3170
- let col = cols[j].split('<')
3171
- switch(j){
3172
- // first column is date
3173
- case 1:
3174
- // split date into array
3175
- // old date format (January 1, 1970) (disabled)
3176
- /*parts = col[0].split(' ')
3177
- year = parts[2]
3178
- // get month index, zero-based
3179
- month = new Date(Date.parse(parts[0] +" 1, 2021")).getMonth()
3180
- day = parts[1].substring(0,parts[1].length-3)*/
3181
- // new date format (01/01/70)
3182
- parts = col[0].split('/')
3183
- year = parts[2]
3184
- if ( year.length == 2 ) {
3185
- year = '20' + parts[2]
3186
- }
3187
- // get month index, zero-based
3188
- month = parseInt(parts[0]) - 1
3189
- day = parts[1]
3190
- this_datestring = new Date(year, month, day).toISOString().substring(0,10)
3191
- this.cache.bigInningSchedule[this_datestring] = {}
3192
- // increment month index (not zero-based)
3193
- month += 1
3194
- break
3195
- // remaining columns are times
3196
- default:
3197
- let hour
3198
- let minute = '00'
3199
- let ampm
3200
- // if time has colon, split into array on that to get hour and minute parts
3201
- if ( col[0].indexOf(':') > 0 ) {
3202
- parts = col[0].split(':')
3203
- hour = parseInt(parts[0])
3204
- minute = parts[1].substring(0,2)
3205
- } else {
3206
- hour = parseInt(col[0].substring(0,col[0].length-2))
3207
- }
3208
- ampm = col[0].substring(col[0].length-2,col[0].length)
3209
- // convert hour to 24-hour format
3210
- if ( (ampm == 'PM') || ((hour == 12) && (ampm == 'AM')) ) {
3211
- hour += 12
3212
- }
3213
- // these times are EDT so add 4 for UTC
3214
- hour += 4
3215
- // if hour is beyond 23, note we will have to add 1 day
3216
- if ( hour > 23 ) {
3217
- add_date = 1
3218
- hour -= 24
3219
- }
3220
-
3221
- d = new Date(this_datestring + 'T' + hour.toString().padStart(2, '0') + ':' + minute.toString().padStart(2, '0') + ':00.000+00:00')
3222
- d.setDate(d.getDate()+add_date)
3223
- switch(j){
3224
- // 2nd column is start time
3225
- case 2:
3226
- this.cache.bigInningSchedule[this_datestring].start = d
3227
- break
3228
- // 3rd column is end time
3229
- case 3:
3230
- this.cache.bigInningSchedule[this_datestring].end = d
3231
- break
3232
- }
3233
- break
3234
- }
3233
+
3234
+ let obj = JSON.parse(response.replace(/\\"/g, '"').match('(?<="channelPrograms":{"live":)(.+?(,"totalPages":1}))')[0])
3235
+ for (var i=0; i < obj.data.length; i++) {
3236
+ let est_date = new Date(obj.data[i].airings[0].start).toLocaleString("en-US", {timeZone: 'America/New_York'})
3237
+ let date_array = est_date.split(',')[0].split('/')
3238
+ let this_datestring = date_array[2] + '-' + date_array[0].padStart(2, '0') + '-' + date_array[1].padStart(2, '0')
3239
+ this.cache.bigInningSchedule[this_datestring] = {
3240
+ start: obj.data[i].airings[0].start,
3241
+ end: obj.data[i].airings[0].end
3235
3242
  }
3236
3243
  }
3237
3244
  this.debuglog(JSON.stringify(this.cache.bigInningSchedule))
@@ -3380,6 +3387,60 @@ class sessionClass {
3380
3387
  }
3381
3388
  }
3382
3389
 
3390
+ // Get linear channel stream URL
3391
+ async getLinearStreamURL(network) {
3392
+ try {
3393
+ this.debuglog('getLinearStreamURL')
3394
+
3395
+ let reqObj = {
3396
+ url: GRAPHQL_URL,
3397
+ simple: false,
3398
+ headers: {
3399
+ 'accept': 'application/json, text/plain, */*',
3400
+ 'accept-encoding': 'gzip, deflate, br',
3401
+ 'accept-language': 'en-US,en;q=0.5',
3402
+ 'authorization': 'Bearer ' + await this.getLoginToken() || this.halt('missing loginToken'),
3403
+ 'connection': 'keep-alive',
3404
+ 'content-type': 'application/json',
3405
+ 'x-client-name': 'WEB',
3406
+ 'x-client-version': '7.8.1',
3407
+ 'origin': 'https://www.mlb.com',
3408
+ 'referer': 'https://www.mlb.com/',
3409
+ 'user-agent': USER_AGENT
3410
+ },
3411
+ body: {
3412
+ 'operationName': 'contentCollections',
3413
+ 'query': 'query contentCollections(\n $categories: [ContentGroupCategory!]\n $includeRestricted: Boolean = false\n $includeSpoilers: Boolean = false\n $limit: Int = 10,\n $skip: Int = 0\n ) {\n contentCollections(\n categories: $categories\n includeRestricted: $includeRestricted\n includeSpoilers: $includeSpoilers\n limit: $limit\n skip: $skip\n ) {\n title\n category\n contents {\n assetTrackingKey\n contentDate\n contentId\n contentRestrictions\n description\n duration\n language\n mediaId\n officialDate\n title\n mediaState {\n state\n mediaType\n }\n thumbnails {\n thumbnailType\n templateUrl\n thumbnailUrl\n }\n }\n }\n }',
3414
+ 'variables': {
3415
+ 'categories': network,
3416
+ 'limit': '25'
3417
+ }
3418
+ },
3419
+ json: true,
3420
+ gzip: true
3421
+ }
3422
+ var response = await this.httpPost(reqObj)
3423
+ if ( response ) {
3424
+ this.debuglog('getLinearStreamURL response : ' + JSON.stringify(response))
3425
+ if ( response.data && response.data.contentCollections && (response.data.contentCollections.length > 0) && response.data.contentCollections[0].contents ) {
3426
+ for (var i=0; i<response.data.contentCollections[0].contents.length; i++) {
3427
+ try {
3428
+ let streamInfo = await this.getStreamURL(response.data.contentCollections[0].contents[i].mediaId)
3429
+ if ( streamInfo.rawStreamURL ) {
3430
+ return streamInfo.rawStreamURL
3431
+ }
3432
+ } catch(e) {
3433
+ this.debuglog('getLinearStreamURL getStreamURL error : ' + e.message)
3434
+ }
3435
+ }
3436
+ this.log('getLinearStreamURL stream url not found')
3437
+ }
3438
+ }
3439
+ } catch(e) {
3440
+ this.log('getLinearStreamURL error : ' + e.message)
3441
+ }
3442
+ }
3443
+
3383
3444
  // Get Recap Rundown data
3384
3445
  async getRecapRundownData(dateString) {
3385
3446
  try {
@@ -3473,6 +3534,12 @@ class sessionClass {
3473
3534
  playbackURL = await this.getRecapRundownURL(dateString)
3474
3535
  } else if ( eventName.toUpperCase() == 'MLBN' ) {
3475
3536
  playbackURL = 'https://falcon.mlbinfra.com/api/v1/linear/mlbn'
3537
+ } else if ( eventName.toUpperCase() == 'SNLA' ) {
3538
+ playbackURL = await this.getLinearStreamURL('SNLA_LIVE')
3539
+ return playbackURL
3540
+ } else if ( eventName.toUpperCase() == 'SNY' ) {
3541
+ playbackURL = await this.getLinearStreamURL('SNY_LIVE')
3542
+ return playbackURL
3476
3543
  } else {
3477
3544
  playbackURL = await this.getEventURL(eventName)
3478
3545
  }
@@ -4014,7 +4081,7 @@ class sessionClass {
4014
4081
 
4015
4082
  if ( !this.temp_cache.gamechanger[id].games ) this.temp_cache.gamechanger[id].games = []
4016
4083
  this.temp_cache.gamechanger[id].games.push(best_games)
4017
- let maxlength = (this.gamechanger_delay + 10) / 10
4084
+ let maxlength = (this.gamechanger_delay + 10 + MLB_GAMECHANGER_PADDING) / 10
4018
4085
  while ( this.temp_cache.gamechanger[id].games.length > maxlength ) {
4019
4086
  this.temp_cache.gamechanger[id].games.shift()
4020
4087
  }