mlbserver 2026.3.15 → 2026.4.1-2

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 +212 -49
  2. package/package.json +1 -1
  3. package/session.js +114 -84
package/index.js CHANGED
@@ -262,6 +262,7 @@ app.get('/clearcache', async function(req, res) {
262
262
  session.log('Clearing session...')
263
263
  session.clear_session_data()
264
264
  session = new sessionClass(argv)
265
+ session.setPorts(port, multiview_port)
265
266
 
266
267
  let server = (req.headers['x-forwarded-proto'] ? req.headers['x-forwarded-proto'] : 'http') + '://' + req.headers.host + http_root
267
268
  res.redirect(server)
@@ -431,7 +432,7 @@ var getKey = function(url, headers, cb) {
431
432
  requestRetry(url, headers, function(err, response) {
432
433
  if (err) return cb(err)
433
434
  let key = response.body
434
- session.debuglog('key returned ' + key)
435
+ session.debuglog('key returned ' + Buffer.from(key, 'binary').toString('base64'))
435
436
  session.temp_cache.prevKeys[url] = key
436
437
  cb(null, key)
437
438
  })
@@ -1262,15 +1263,28 @@ app.get('/gamechangerplaylist.m3u8', async function(req, res) {
1262
1263
  let new_segments_complete = false
1263
1264
  let segment_count = 0
1264
1265
  for (var i=(body.length-1); i>=0; i--) {
1265
- if ( body[i].startsWith('#EXTINF:') ) {
1266
- let line = url.resolve(u, body[i+1])
1267
- if ( !new_segments_complete ) {
1268
- session.debuglog(game_changer_title + 'found segment ' + line)
1266
+ if ( body[i].startsWith('#EXT-X-KEY') ) {
1267
+ let key = url.resolve(u, body[i].match('URI="([^"]+)"')[1])
1268
+ let iv = body[i].match('IV=0x(.*)$')[1]
1269
+ let ts
1270
+ let extinf
1271
+ for (var j=1; j<=4; j++) {
1272
+ if ( body[i+j] ) {
1273
+ if ( !extinf && body[i+j].startsWith('#EXTINF') ) {
1274
+ extinf = body[i+j]
1275
+ } else if ( !ts && !body[i+j].startsWith('#') ) {
1276
+ ts = url.resolve(u, body[i+j])
1277
+ }
1278
+ if ( extinf && ts ) break;
1279
+ }
1280
+ }
1281
+ if ( key && iv && extinf && ts && !new_segments_complete ) {
1282
+ session.debuglog(game_changer_title + 'found segment ' + ts)
1269
1283
  if ( discontinuity ) {
1270
1284
  session.debuglog(game_changer_title + 'only getting newest segment after stream change')
1271
- new_segments.unshift({'extinf':body[i], 'ts':line, 'streamURLToken':streamURLToken})
1285
+ new_segments.unshift({'key':key, 'iv':iv, 'extinf':extinf, 'ts':ts, 'streamURLToken':streamURLToken})
1272
1286
  new_segments_complete = true
1273
- } else if ( !discontinuity && (session.temp_cache.gamechanger[id].segments.length > 0) && (line == session.temp_cache.gamechanger[id].segments[session.temp_cache.gamechanger[id].segments.length-1].ts) ) {
1287
+ } else if ( !discontinuity && (session.temp_cache.gamechanger[id].segments.length > 0) && (ts == session.temp_cache.gamechanger[id].segments[session.temp_cache.gamechanger[id].segments.length-1].ts) ) {
1274
1288
  session.debuglog(game_changer_title + 'found previous last segment')
1275
1289
  new_segments_complete = true
1276
1290
  } else if ( segment_count == GAMECHANGER_LIST_SIZE ) {
@@ -1281,7 +1295,7 @@ app.get('/gamechangerplaylist.m3u8', async function(req, res) {
1281
1295
  }
1282
1296
  new_segments_complete = true
1283
1297
  } else {
1284
- new_segments.unshift({'extinf':body[i], 'ts':line, 'streamURLToken':streamURLToken})
1298
+ new_segments.unshift({'key':key, 'iv':iv, 'extinf':extinf, 'ts':ts, 'streamURLToken':streamURLToken})
1285
1299
  }
1286
1300
  }
1287
1301
  segment_count++
@@ -1314,7 +1328,7 @@ app.get('/gamechangerplaylist.m3u8', async function(req, res) {
1314
1328
  if ( session.temp_cache.gamechanger[id].segments[i].discontinuity ) {
1315
1329
  session.temp_cache.gamechanger[id].playlist[resolution] += '#EXT-X-DISCONTINUITY' + '\n'
1316
1330
  }
1317
- session.temp_cache.gamechanger[id].playlist[resolution] += session.temp_cache.gamechanger[id].segments[i].extinf + '\n' + http_root + '/segment.ts?url=' + encodeURIComponent(session.temp_cache.gamechanger[id].segments[i].ts) + '&streamURLToken='+encodeURIComponent(session.temp_cache.gamechanger[id].segments[i].streamURLToken) + content_protect + '\n'
1331
+ session.temp_cache.gamechanger[id].playlist[resolution] += session.temp_cache.gamechanger[id].segments[i].extinf + '\n' + http_root + '/segment.ts?url=' + encodeURIComponent(session.temp_cache.gamechanger[id].segments[i].ts) + '&streamURLToken='+encodeURIComponent(session.temp_cache.gamechanger[id].segments[i].streamURLToken) + '&key='+encodeURIComponent(session.temp_cache.gamechanger[id].segments[i].key) + '&iv='+encodeURIComponent(session.temp_cache.gamechanger[id].segments[i].iv) + content_protect + '\n'
1318
1332
  }
1319
1333
 
1320
1334
  session.debuglog(game_changer_title + 'playlist ' + session.temp_cache.gamechanger[id].playlist[resolution])
@@ -1529,8 +1543,7 @@ app.get('/', async function(req, res) {
1529
1543
  var body = '<!DOCTYPE html><html><head><meta charset="UTF-8"><meta http-equiv="Content-type" content="text/html;charset=UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><title>' + appname + '</title><link rel="icon" href="favicon.svg' + content_protect_a + '"><style type="text/css">input[type=text],input[type=button]{-webkit-appearance:none;-webkit-border-radius:0}body{width:480px;color:lightgray;background-color:black;font-family:Arial,Helvetica,sans-serif;-webkit-text-size-adjust:none}a{color:darkgray}button{color:lightgray;background-color:black}button.default{color:black;background-color:lightgray}table{width:100%;pad}table,th,td{border:1px solid darkgray;border-collapse:collapse}th,td{padding:5px}.tinytext,textarea,input[type="number"]{font-size:.8em}textarea{width:380px}.freegame,.freegame a{color:green}.blackout,.blackout a{text-decoration:line-through}'
1530
1544
 
1531
1545
  // Highlights CSS
1532
- //max-height:calc(100vh-110px);
1533
- body += '.modal{display:none;position:fixed;z-index:1;padding-top:100px;left:0;top:0;width:100%;height:100%;overflow:auto;-webkit-overflow-scrolling:touch;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}.modal-content{background-color:#fefefe;margin:auto;padding:10px;border:1px solid #888;width:360px;color:black}#highlights a{color:black}.close{color:black;float:right;font-size:28px;font-weight:bold;}#highlights a:hover,#highlights a:focus,.close:hover,.close:focus{color:gray;text-decoration:none;cursor:pointer;}'
1546
+ body += '.modal{display:none;position:fixed;z-index:1;left:0;top:0;width:100%;height:100%;overflow:hidden;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}.modal-content{position:absolute;top:100px;bottom:20px;left:50%;transform:translateX(-50%);background-color:#fefefe;padding:10px;border:1px solid #888;width:360px;overflow-y:auto;color:black}#highlights{overflow-y:auto;}#highlights a{color:black}.close{color:black;float:right;font-size:28px;font-weight:bold;}#highlights a:hover,#highlights a:focus,.close:hover,.close:focus{color:gray;text-decoration:none;cursor:pointer;}'
1534
1547
 
1535
1548
  // Tooltip CSS
1536
1549
  body += '.tooltip{position:relative;display:inline-block;border-bottom: 1px dotted gray;}.tooltip .tooltiptext{font-size:.8em;visibility:hidden;width:360px;background-color:gray;color:white;text-align:left;padding:5px;border-radius:6px;position:absolute;z-index:1;top:100%;left:75%;margin-left:-30px;}.tooltip:hover .tooltiptext{visibility:visible;}'
@@ -1816,37 +1829,38 @@ app.get('/', async function(req, res) {
1816
1829
  if ( (entitlements.length > 0) && cache_data.dates && cache_data.dates[0] && (cache_data.dates[0].date >= today) && cache_data.dates[0].games && (cache_data.dates[0].games.length > 1) && cache_data.dates[0].games[0] && (cache_data.dates[0].games[0].seriesDescription == 'Regular Season') ) {
1817
1830
  // Scraped Big Inning schedule
1818
1831
  big_inning = await session.getBigInningSchedule(gameDate)
1819
-
1820
- // Generated Big Inning schedule (disabled)
1821
- //big_inning = await session.generateBigInningSchedule(gameDate)
1822
- }
1823
- if ( big_inning && big_inning.start ) {
1824
- 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>'
1825
- let compareStart = new Date(big_inning.start)
1826
- compareStart.setMinutes(compareStart.getMinutes()-10)
1827
- let compareEnd = new Date(big_inning.end)
1828
- compareEnd.setHours(compareEnd.getHours()+1)
1829
- if ( (currentDate >= compareStart) && (currentDate < compareEnd) ) {
1830
- let querystring = '?event=biginning'
1831
- let multiviewquerystring = querystring + '&resolution=' + DEFAULT_MULTIVIEW_RESOLUTION
1832
- if ( linkType == VALID_LINK_TYPES[0] ) {
1833
- if ( startFrom != VALID_START_FROM[0] ) querystring += '&startFrom=' + startFrom
1834
- if ( controls != VALID_CONTROLS[0] ) querystring += '&controls=' + controls
1835
- }
1836
- if ( resolution != VALID_RESOLUTIONS[0] ) querystring += '&resolution=' + resolution
1837
- if ( linkType == VALID_LINK_TYPES[1] ) {
1838
- if ( force_vod != VALID_FORCE_VOD[0] ) querystring += '&force_vod=' + force_vod
1839
- } else if ( linkType == VALID_LINK_TYPES[4] ) {
1840
- querystring += '&filename=' + gameDate + ' Big Inning'
1832
+ }
1833
+ if ( big_inning ) {
1834
+ for (var i = 0; i < big_inning.length; i++) {
1835
+ if ( big_inning[i].start ) {
1836
+ body += '<tr><td><span class="tooltip">' + new Date(big_inning[i].start).toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + ' - ' + new Date(big_inning[i].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>'
1837
+ let compareStart = new Date(big_inning[i].start)
1838
+ compareStart.setMinutes(compareStart.getMinutes()-10)
1839
+ let compareEnd = new Date(big_inning[i].end)
1840
+ compareEnd.setHours(compareEnd.getHours()+1)
1841
+ if ( (currentDate >= compareStart) && (currentDate < compareEnd) ) {
1842
+ let querystring = '?event=biginning'
1843
+ let multiviewquerystring = querystring + '&resolution=' + DEFAULT_MULTIVIEW_RESOLUTION
1844
+ if ( linkType == VALID_LINK_TYPES[0] ) {
1845
+ if ( startFrom != VALID_START_FROM[0] ) querystring += '&startFrom=' + startFrom
1846
+ if ( controls != VALID_CONTROLS[0] ) querystring += '&controls=' + controls
1847
+ }
1848
+ if ( resolution != VALID_RESOLUTIONS[0] ) querystring += '&resolution=' + resolution
1849
+ if ( linkType == VALID_LINK_TYPES[1] ) {
1850
+ if ( force_vod != VALID_FORCE_VOD[0] ) querystring += '&force_vod=' + force_vod
1851
+ } else if ( linkType == VALID_LINK_TYPES[4] ) {
1852
+ querystring += '&filename=' + gameDate + ' Big Inning'
1853
+ }
1854
+ querystring += content_protect_b
1855
+ multiviewquerystring += content_protect_b
1856
+ body += '<a href="' + thislink + querystring + '">Big Inning</a>'
1857
+ body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)">'
1858
+ } else {
1859
+ body += 'Big Inning'
1860
+ }
1861
+ body += '</td></tr>' + "\n"
1841
1862
  }
1842
- querystring += content_protect_b
1843
- multiviewquerystring += content_protect_b
1844
- body += '<a href="' + thislink + querystring + '">Big Inning</a>'
1845
- body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)">'
1846
- } else {
1847
- body += 'Big Inning'
1848
1863
  }
1849
- body += '</td></tr>' + "\n"
1850
1864
  }
1851
1865
 
1852
1866
  // Game Changer and Stream Finder
@@ -3307,11 +3321,13 @@ function start_multiview_stream(streams, sync, dvr, faster, reencode, park_audio
3307
3321
  //let audio_input = audio_present[i] + ':a:m:language:en?'
3308
3322
  let audio_input = audio_present[i] + ':a:'
3309
3323
  let video_url = streams[audio_present[i]]
3310
- if ( !video_url || video_url.includes('audio_track=English') || !video_url.includes('audio_track=') ) {
3324
+ // disabled code below because we can now assume
3325
+ // the first audio track is the one we want
3326
+ //if ( !video_url || video_url.includes('audio_track=English') || !video_url.includes('audio_track=') ) {
3311
3327
  audio_input += '0'
3312
- } else {
3328
+ /*} else {
3313
3329
  audio_input += '1'
3314
- }
3330
+ }*/
3315
3331
  let filter = ''
3316
3332
  // Optionally apply sync adjustments
3317
3333
  if ( sync[audio_present[i]] ) {
@@ -3518,11 +3534,13 @@ app.get('/download.ts', async function(req, res) {
3518
3534
  if ( ! (await protect(req, res)) ) return
3519
3535
 
3520
3536
  try {
3537
+ let ffmpeg_timeout = 432000
3521
3538
  // we'll know it's an actual download request if it include a filename parameter
3522
3539
  if ( req.query.filename ) {
3523
3540
  session.requestlog('download.ts', req)
3524
3541
  } else {
3525
3542
  session.debuglog('force alternate audio', req)
3543
+ ffmpeg_timeout = 20
3526
3544
  }
3527
3545
 
3528
3546
  let server = 'http://127.0.0.1:' + session.data.port + http_root
@@ -3548,7 +3566,7 @@ app.get('/download.ts', async function(req, res) {
3548
3566
  }
3549
3567
  }
3550
3568
 
3551
- ffmpeg_command = ffmpeg({ timeout: 432000 })
3569
+ ffmpeg_command = ffmpeg({ timeout: ffmpeg_timeout })
3552
3570
 
3553
3571
  // Set input stream and minimize ffmpeg startup latency
3554
3572
  ffmpeg_command.input(video_url)
@@ -3606,15 +3624,21 @@ app.get('/download.ts', async function(req, res) {
3606
3624
  ffmpeg_command.addOutputOption('-f', 'mpegts')
3607
3625
  .output(res)
3608
3626
  .on('start', function(commandLine) {
3609
- session.debuglog('download.ts command started')
3627
+ session.debuglog('download.ts command started for ' + video_url)
3610
3628
  if ( argv.debug || argv.ffmpeg_logging ) {
3611
- session.debuglog('download.ts command: ' + commandLine)
3629
+ session.log('download.ts command: ' + commandLine)
3612
3630
  }
3613
3631
  })
3614
3632
  .on('error', function(err, stdout, stderr) {
3615
- session.debuglog('download.ts command stopped: ' + err.message)
3616
- if ( stdout ) session.log(stdout)
3617
- if ( stderr ) session.log(stderr)
3633
+ if (err.message.includes('timeout')) {
3634
+ session.debuglog('download.ts command timeout: ' + err.message)
3635
+ } else {
3636
+ session.debuglog('download.ts command error: ' + err.message)
3637
+ }
3638
+ if ( stdout ) session.debuglog(stdout)
3639
+ if ( stderr ) session.debuglog(stderr)
3640
+ ffmpeg_command.kill('SIGKILL')
3641
+ session.debuglog('killed ffmpeg process due to error processing ' + video_url)
3618
3642
  })
3619
3643
  .on('end', function() {
3620
3644
  session.debuglog('download.ts command ended')
@@ -3782,3 +3806,142 @@ app.get('/comskip.txt', async function(req, res) {
3782
3806
  res.end('comskip.txt request error, check log')
3783
3807
  }
3784
3808
  })
3809
+
3810
+ // Listen for embedded MPEGTS stream requests
3811
+ // embedded player not fully tested
3812
+ app.get('/mpegts.html', async function(req, res) {
3813
+ if ( ! (await protect(req, res)) ) return
3814
+
3815
+ try {
3816
+ let server = 'http://127.0.0.1:' + session.data.port + http_root
3817
+
3818
+ let video_url = '/stream.ts'
3819
+ if ( req.query.src ) {
3820
+ video_url = req.query.src
3821
+ } else {
3822
+ let urlArray = req.url.split('?')
3823
+ if ( (urlArray.length == 2) ) {
3824
+ video_url += '?' + urlArray[1]
3825
+ }
3826
+ video_url = server + video_url
3827
+ }
3828
+ session.debuglog('mpegts.html src : ' + video_url)
3829
+
3830
+ var body = '<html><script src="https://xqq.im/mpegts.js/dist/mpegts.js"></script><style type"text/css">body{background-color:black}video{width:100% !important;height:auto !important;max-width:1280px}</style><body><video id="videoElement" controls autoplay playsinline></video><script>if (mpegts.getFeatureList().mseLivePlayback) { var videoElement = document.getElementById("videoElement"); var player = mpegts.createPlayer({ type: "mpegts", isLive: true, url: "' + video_url + '" }); player.attachMediaElement(videoElement); player.load(); player.play(); }</script></body></html>'
3831
+
3832
+ res.end(body)
3833
+ } catch (e) {
3834
+ session.log('mpegts.html request error : ' + e.message)
3835
+ res.end('')
3836
+ }
3837
+ })
3838
+
3839
+
3840
+ // Listen for MPEGTS stream requests
3841
+ app.get('/stream.ts', async function(req, res) {
3842
+ if ( ! (await protect(req, res)) ) return
3843
+
3844
+ try {
3845
+ let server = 'http://127.0.0.1:' + session.data.port + http_root
3846
+
3847
+ let video_url = '/stream.m3u8'
3848
+ if ( req.query.src ) {
3849
+ video_url = req.query.src
3850
+ } else {
3851
+ let urlArray = req.url.split('?')
3852
+ if ( (urlArray.length == 2) ) {
3853
+ video_url += '?' + urlArray[1]
3854
+ }
3855
+ video_url = server + video_url
3856
+ }
3857
+ session.debuglog('stream.ts src : ' + video_url)
3858
+
3859
+ // force adaptive streams to just use a single video resolution/track
3860
+ if ( !video_url.includes('resolution=') ) {
3861
+ video_url += '&resolution=best'
3862
+ } else if ( video_url.includes('resolution=adaptive') ) {
3863
+ video_url = video_url.replace('resolution=adaptive', 'resolution=best')
3864
+ }
3865
+
3866
+ // force streams to just use a single audio track, if they aren't already
3867
+ if ( !video_url.includes('audio_track=') ) {
3868
+ video_url += '&audio_track=English'
3869
+ } else if ( video_url.includes('audio_track=all') ) {
3870
+ video_url = video_url.replace('audio_track=all', 'audio_track=English')
3871
+ }
3872
+
3873
+ ffmpeg_command = ffmpeg({ timeout: 432000 })
3874
+
3875
+ // Set input live stream and minimize ffmpeg startup latency
3876
+ ffmpeg_command.input(video_url)
3877
+ .addInputOption('-thread_queue_size', '4096')
3878
+ .addInputOption('-fflags', 'nobuffer')
3879
+ .addInputOption('-probesize', '1000000')
3880
+ .addInputOption('-analyzeduration', '0')
3881
+
3882
+ // We'll limit our processing to real-time
3883
+ ffmpeg_command.native()
3884
+
3885
+ let video_input = 0
3886
+ let audio_input = 0
3887
+
3888
+ // Adjust audio sync, if specified
3889
+ if ( req.query.sync ) {
3890
+ if ( req.query.sync > 0 ) {
3891
+ session.log('stream.ts delaying video by ' + req.query.sync + ' seconds')
3892
+ video_input = 1
3893
+ ffmpeg_command.addInputOption('-itsoffset', req.query.sync)
3894
+ } else {
3895
+ session.log('stream.ts delaying audio by ' + (req.query.sync * -1) + ' seconds')
3896
+ audio_input = 1
3897
+ ffmpeg_command.addInputOption('-itsoffset', (req.query.sync * -1))
3898
+ }
3899
+ ffmpeg_command.input(video_url)
3900
+ .addInputOption('-thread_queue_size', '4096')
3901
+
3902
+ // We'll limit our processing to real-time
3903
+ ffmpeg_command.native()
3904
+ }
3905
+
3906
+ // video
3907
+ ffmpeg_command.addOutputOption('-map', video_input + ':v:0')
3908
+ .addOutputOption('-c:v', 'copy')
3909
+
3910
+ // audio
3911
+ ffmpeg_command.addOutputOption('-map', audio_input + ':a')
3912
+ .addOutputOption('-c:a', 'copy')
3913
+
3914
+ // output mpegts to response stream
3915
+ ffmpeg_command.addOutputOption('-f', 'mpegts')
3916
+ .output(res)
3917
+ .on('start', function(commandLine) {
3918
+ session.debuglog('stream.ts command started')
3919
+ if ( argv.debug || argv.ffmpeg_logging ) {
3920
+ session.log('stream.ts command: ' + commandLine)
3921
+ }
3922
+ })
3923
+ .on('error', function(err, stdout, stderr) {
3924
+ session.debuglog('stream.ts command stopped: ' + err.message)
3925
+ if ( stdout ) session.debuglog(stdout)
3926
+ if ( stderr ) session.debuglog(stderr)
3927
+ })
3928
+ .on('end', function() {
3929
+ session.debuglog('stream.ts command ended')
3930
+ })
3931
+
3932
+ if ( argv.ffmpeg_logging ) {
3933
+ session.log('ffmpeg output logging enabled')
3934
+ ffmpeg_command.on('stderr', function(stderrLine) {
3935
+ session.log(stderrLine);
3936
+ })
3937
+ }
3938
+
3939
+ var headers = {'Content-Type': 'video/mp2t',"access-control-allow-origin":"*"}
3940
+ res.writeHead(200, headers)
3941
+
3942
+ ffmpeg_command.run()
3943
+ } catch (e) {
3944
+ session.log('stream.ts request error : ' + e.message)
3945
+ res.end('')
3946
+ }
3947
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mlbserver",
3
- "version": "2026.3.15",
3
+ "version": "2026.4.1-2",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
package/session.js CHANGED
@@ -28,7 +28,7 @@ const LIDOM_TEAM_IDS = { 'AGU': '667', 'TOR': '668', 'EST': '669', 'GIG': '670',
28
28
 
29
29
  const LMP_TEAM_IDS = { 'MXC': '673', 'JAL': '674', 'MOC': '675', 'HER': '677', 'CUL': '678', 'MAZ': '679', 'OBR': '680', 'GSV': '5482', 'NAY': '6483', 'TBC': '6484' }
30
30
 
31
- const AFFILIATE_TEAM_IDS = { 'ATH': '237,400,499,524', 'ATL': '431,432,478,6325', 'AZ': '419,516,2310,5368', 'BAL': '418,488,548,568', 'BOS': '414,428,533,546', 'CHC': '451,521,550,553', 'CIN': '416,450,459,498', 'CLE': '402,437,445,481', 'COL': '259,342,486,538', 'CWS': '247,487,494,580', 'DET': '106,512,570,582', 'HOU': '482,573,3712,5434', 'KC': '541,565,1350,3705', 'LAA': '401,460,559,561', 'LAD': '238,260,456,526', 'MIA': '479,554,564,4124', 'MIL': '249,556,572,5015', 'MIN': '492,509,1960,3898', 'NYM': '453,505,507,552', 'NYY': '531,537,587,1956', 'PHI': '427,522,566,1410', 'PIT': '452,477,484,3390', 'SD': '103,510,584,4904', 'SEA': '403,515,529,574', 'SF': '105,461,476,3410', 'STL': '235,279,440,443', 'TB': '233,234,421,2498', 'TEX': '102,448,540,6324', 'TOR': '422,424,435,463', 'WSH': '426,436,534,547' }
31
+ const AFFILIATE_TEAM_IDS = {"ATH":"237,400,499,524","ATL":"431,432,478,6325","AZ":"419,516,2310,5368","BAL":"418,493,548,568","BOS":"414,428,533,546","CHC":"451,521,550,553","CIN":"416,450,459,498","CLE":"402,437,445,481","COL":"259,342,486,538","CWS":"247,487,494,580","DET":"106,512,570,582","HOU":"482,573,3712,5434","KC":"541,565,1350,3705","LAA":"460,526,559,561","LAD":"238,260,456,6482","MIA":"479,554,564,4124","MIL":"249,556,572,5015","MIN":"492,509,1960,3898","NYM":"453,505,507,552","NYY":"531,537,587,1956","PHI":"427,522,566,1410","PIT":"452,477,484,3390","SD":"103,510,584,4904","SEA":"401,403,529,574","SF":"105,461,476,3410","STL":"235,279,440,443","TB":"233,234,421,2498","TEX":"102,448,540,6324","TOR":"422,424,435,463","WSH":"426,436,534,547"}
32
32
 
33
33
  // First is default level, last should be All (also used as default org)
34
34
  const LEVELS = { 'MLB': '1', 'AAA': '11', 'AA': '12', 'A+': '13', 'A': '14', 'WINTER': '17', 'All': '1,11,12,13,14,17' }
@@ -2368,8 +2368,8 @@ class sessionClass {
2368
2368
  stream = server + '/stream.m3u8?event=' + encodeURIComponent(cache_data.dates[i].games[j].teams['home'].team.shortName.toUpperCase())
2369
2369
  }
2370
2370
  stream += '&league_id=' + league_id
2371
+ stream += '&mediaType=' + streamMediaType
2371
2372
  }
2372
- stream += '&mediaType=' + streamMediaType
2373
2373
  stream += '&level=' + encodeURIComponent(this.getLevelNameFromSportId(sportId))
2374
2374
  stream += '&resolution=' + resolution
2375
2375
  if ( this.protection.content_protect ) stream += '&content_protect=' + this.protection.content_protect
@@ -2919,25 +2919,28 @@ class sessionClass {
2919
2919
  let gameDate = cache_data.dates[i].date
2920
2920
  if ( (gameDate >= 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') && this.cache.bigInningSchedule[gameDate] ) {
2921
2921
  this.debuglog('getTVData Big Inning active for date ' + cache_data.dates[i].date)
2922
- // Scraped Big Inning schedule
2923
- let start = this.convertDateToXMLTV(new Date(this.cache.bigInningSchedule[gameDate].start))
2924
- let stop = this.convertDateToXMLTV(new Date(this.cache.bigInningSchedule[gameDate].end))
2925
-
2926
- // Big Inning calendar ICS
2927
- let prefix = 'Watch'
2928
- let location = server + '/embed.html?event=biginning&mediaType=Video&resolution=' + resolution
2929
- if ( this.protection.content_protect ) location += '&content_protect=' + this.protection.content_protect
2930
- calendar += await this.generate_ics_event(prefix, new Date(this.cache.bigInningSchedule[gameDate].start), new Date(this.cache.bigInningSchedule[gameDate].end), title, description, location)
2931
2922
 
2932
- // Off Air if necessary
2933
- let off_air_event = await this.generate_off_air_event(offAir, channelid, gameDate, channels[channelid].stop, this.cache.bigInningSchedule[gameDate].start, title)
2934
- if ( off_air_event ) {
2935
- programs += off_air_event
2936
- channels[channelid].stop = stop
2937
- }
2923
+ for (var j = 0; j < this.cache.bigInningSchedule[gameDate].length; j++) {
2924
+ // Scraped Big Inning schedule
2925
+ let start = this.convertDateToXMLTV(new Date(this.cache.bigInningSchedule[gameDate][j].start))
2926
+ let stop = this.convertDateToXMLTV(new Date(this.cache.bigInningSchedule[gameDate][j].end))
2927
+
2928
+ // Big Inning calendar ICS
2929
+ let prefix = 'Watch'
2930
+ let location = server + '/embed.html?event=biginning&mediaType=Video&resolution=' + resolution
2931
+ if ( this.protection.content_protect ) location += '&content_protect=' + this.protection.content_protect
2932
+ calendar += await this.generate_ics_event(prefix, new Date(this.cache.bigInningSchedule[gameDate][j].start), new Date(this.cache.bigInningSchedule[gameDate][j].end), title, description, location)
2933
+
2934
+ // Off Air if necessary
2935
+ let off_air_event = await this.generate_off_air_event(offAir, channelid, gameDate, channels[channelid].stop, this.cache.bigInningSchedule[gameDate][j].start, title)
2936
+ if ( off_air_event ) {
2937
+ programs += off_air_event
2938
+ channels[channelid].stop = stop
2939
+ }
2938
2940
 
2939
- // Big Inning guide XML
2940
- programs += await this.generate_xml_program(channelid, start, stop, title, description, logo, this.convertDateToAirDate(new Date(this.cache.bigInningSchedule[gameDate].start)))
2941
+ // Big Inning guide XML
2942
+ programs += await this.generate_xml_program(channelid, start, stop, title, description, logo, this.convertDateToAirDate(new Date(this.cache.bigInningSchedule[gameDate][j].start)))
2943
+ }
2941
2944
  }
2942
2945
  this.debuglog('getTVData completed Big Inning for date ' + cache_data.dates[i].date)
2943
2946
  }
@@ -3319,6 +3322,7 @@ class sessionClass {
3319
3322
  async getSkipMarkers(gamePk, skip_type, start_inning, start_inning_half, streamURL, streamURLToken, skip_adjust, broadcast_start_timestamp=false) {
3320
3323
  try {
3321
3324
  this.debuglog('getSkipMarkers')
3325
+ let variantPlaylist;
3322
3326
 
3323
3327
  if ( skip_adjust != 0 ) this.log('manual adjustment of ' + skip_adjust + ' seconds being applied')
3324
3328
 
@@ -3342,7 +3346,7 @@ class sessionClass {
3342
3346
 
3343
3347
  // Get the broadcast start time first, if necessary -- event times will be relative to this
3344
3348
  if ( !broadcast_start_timestamp ) {
3345
- let variantPlaylist = await this.getVariantPlaylist(streamURL, streamURLToken)
3349
+ variantPlaylist = await this.getVariantPlaylist(streamURL, streamURLToken)
3346
3350
  broadcast_start_timestamp = await this.getBroadcastStart(variantPlaylist)
3347
3351
  }
3348
3352
 
@@ -3488,6 +3492,10 @@ class sessionClass {
3488
3492
  // if skipping commercials, look at the variant playlist to detect insertions
3489
3493
  if ( skip_type == 4 ) {
3490
3494
  this.debuglog('detecting commercial breaks')
3495
+ if (!variantPlaylist) {
3496
+ this.debuglog('variantPlaylist missing, fetching...')
3497
+ variantPlaylist = await this.getVariantPlaylist(streamURL, streamURLToken)
3498
+ }
3491
3499
  let body = variantPlaylist
3492
3500
  let break_active = false
3493
3501
  let break_end = 0
@@ -3534,54 +3542,54 @@ class sessionClass {
3534
3542
  // temporarily disable Big Inning schedule checking until a new source URL is available
3535
3543
  /*this.cache.bigInningSchedule = {}
3536
3544
  return*/
3537
-
3545
+
3546
+ // reset for new format as of 2026-04-01
3547
+ try {
3548
+ if ( !this.cache.bigInningSchedule[Object.keys(this.cache.bigInningSchedule)[0]].length ) {
3549
+ this.log('getBigInningSchedule cache reset')
3550
+ delete this.cache.bigInningScheduleCacheExpiry
3551
+ delete this.cache.bigInningSchedule
3552
+ }
3553
+ } catch (e) {
3554
+ //this.debuglog('getBigInningSchedule cache reset error : ' + e.message)
3555
+ }
3556
+
3538
3557
  let currentDate = new Date()
3539
3558
  if ( !this.cache || !this.cache.bigInningScheduleCacheExpiry || (currentDate > new Date(this.cache.bigInningScheduleCacheExpiry)) ) {
3540
3559
  if ( !this.cache.bigInningSchedule ) this.cache.bigInningSchedule = {}
3541
3560
  let reqObj = {
3542
- url: 'https://api.fubo.tv/gg/series/123881219/live-programs?limit=14&languages=en&countrySlugs=USA',
3561
+ url: 'https://www.fubo.tv/welcome/channel/mlb-big-inning',
3543
3562
  headers: {
3544
- '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',
3545
- 'accept-language': 'en-US,en;q=0.9',
3546
- 'cache-control': 'no-cache',
3547
- 'dnt': '1',
3548
- 'pragma': 'no-cache',
3549
- 'sec-ch-ua': '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"',
3550
- 'sec-ch-ua-mobile': '?0',
3551
- 'sec-ch-ua-platform': '"macOS"',
3552
- 'sec-fetch-dest': 'document',
3553
- 'sec-fetch-mode': 'navigate',
3554
- 'sec-fetch-site': 'none',
3555
- 'sec-fetch-user': '?1',
3556
- 'sec-gpc': '1',
3557
- 'upgrade-insecure-requests': '1',
3563
+ 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
3564
+ 'accept-encoding': 'gzip, deflate, br, zstd',
3565
+ 'referer': 'https://www.fubo.tv',
3558
3566
  'user-agent': USER_AGENT
3559
3567
  },
3560
- json: true,
3561
3568
  gzip: true
3562
3569
  }
3563
3570
  var response = await this.httpGet(reqObj, false)
3564
3571
  if ( response ) {
3565
- this.debuglog(JSON.stringify(response))
3572
+ // disabled because it's big
3573
+ //this.debuglog(response)
3566
3574
 
3567
- if ( response.data ) {
3568
- for (var i=0; i < response.data.length; i++) {
3569
- if ( response.data[i].airings && (response.data[i].airings.length > 0) ) {
3570
- for (var j=0; j < response.data[i].airings.length; j++) {
3571
- if ( response.data[i].airings[j].station && response.data[i].airings[j].station.name && (response.data[i].airings[j].station.name == 'MLB Big Inning') && response.data[i].airings[j].accessRightsV2 && response.data[i].airings[j].accessRightsV2.live ) {
3572
- let est_date = new Date(response.data[i].airings[j].accessRightsV2.live.startTime).toLocaleString("en-US", {timeZone: 'America/New_York'})
3573
- let date_array = est_date.split(',')[0].split('/')
3574
- let this_datestring = date_array[2] + '-' + date_array[0].padStart(2, '0') + '-' + date_array[1].padStart(2, '0')
3575
- this.cache.bigInningSchedule[this_datestring] = {
3576
- start: response.data[i].airings[j].accessRightsV2.live.startTime,
3577
- end: response.data[i].airings[j].accessRightsV2.live.endTime
3578
- }
3579
- break
3580
- }
3581
- }
3575
+ let nextdatastring = response.match(/<script id=\"__NEXT_DATA__\" type=\"application\/json\">(.*?)<\/script>/)
3576
+ let nextdata = JSON.parse(nextdatastring[1])
3577
+ let initialState = JSON.parse(nextdata.props.pageProps.initialState.replace(/\\"/g, '"'))
3578
+
3579
+ initialState.channel.channelPrograms.live.data.forEach((program) => {
3580
+ program.airings.forEach((airing) => {
3581
+ let est_date = new Date(airing.accessRightsV2.live.startTime).toLocaleString("en-US", {timeZone: 'America/New_York'})
3582
+ let date_array = est_date.split(',')[0].split('/')
3583
+ let this_datestring = date_array[2] + '-' + date_array[0].padStart(2, '0') + '-' + date_array[1].padStart(2, '0')
3584
+ if ( !this.cache.bigInningSchedule[this_datestring] || !this.cache.bigInningSchedule[this_datestring].length ) {
3585
+ this.cache.bigInningSchedule[this_datestring] = []
3582
3586
  }
3583
- }
3584
- }
3587
+ this.cache.bigInningSchedule[this_datestring].push({
3588
+ start: airing.accessRightsV2.live.startTime,
3589
+ end: airing.accessRightsV2.live.endTime
3590
+ })
3591
+ });
3592
+ });
3585
3593
  this.debuglog(JSON.stringify(this.cache.bigInningSchedule))
3586
3594
 
3587
3595
  // Default cache period is 1 day from now
@@ -3608,35 +3616,6 @@ class sessionClass {
3608
3616
  }
3609
3617
  }
3610
3618
 
3611
- // Generate generic Big Inning schedule for specified date
3612
- // times in UTC (and DST) according to https://www.mlb.com/live-stream-games/help-center/subscription-access-big-inning
3613
- async generateBigInningSchedule(dateString) {
3614
- try {
3615
- this.debuglog('generateBigInningSchedule')
3616
-
3617
- let utc_start_string = '01:00'
3618
- let utc_end_string = '03:30'
3619
- let add_date = 1
3620
- // Different Sunday schedule
3621
- let weekday_index = new Date(dateString + ' 00:00:00').getDay()
3622
- if ( weekday_index == 0 ) {
3623
- utc_start_string = '19:00'
3624
- utc_end_string = '21:30'
3625
- add_date = 0
3626
- }
3627
- let d = new Date(dateString + 'T' + utc_start_string + ':00.000+00:00')
3628
- d.setDate(d.getDate()+add_date)
3629
- let start = d
3630
- d = new Date(dateString + 'T' + utc_end_string + ':00.000+00:00')
3631
- d.setDate(d.getDate()+add_date)
3632
- let end = d
3633
-
3634
- return {start: start, end: end}
3635
- } catch(e) {
3636
- this.log('generateBigInningSchedule error : ' + e.message)
3637
- }
3638
- }
3639
-
3640
3619
  // Get event data
3641
3620
  async getEventData(url) {
3642
3621
  try {
@@ -4349,6 +4328,7 @@ class sessionClass {
4349
4328
  if ( cache_data ) {
4350
4329
  if ( cache_data.dates && cache_data.dates[0] && cache_data.dates[0].games && (cache_data.dates[0].games.length > 0) ) {
4351
4330
  let team_data = this.temp_cache.gamechanger[id].streamFinderData.team_data
4331
+ let games_CLI = this.temp_cache.gamechanger[id].streamFinderData.games_CLI
4352
4332
 
4353
4333
  var games = []
4354
4334
 
@@ -5615,6 +5595,56 @@ class sessionClass {
5615
5595
  this.log('getComskipMarkers error : ' + e.message)
5616
5596
  }
5617
5597
  }
5598
+
5599
+ // generates AFFILIATE_TEAM_IDS, should be done each season
5600
+ async getAffiliates() {
5601
+ try {
5602
+ this.debuglog('getAffiliates')
5603
+
5604
+ let affiliates_data = {}
5605
+ let reqObj = {
5606
+ url: 'https://statsapi.mlb.com/api/v1/teams?sportIds=1,11,12,13,14&activeStatus=true&season=2026',
5607
+ headers: {
5608
+ 'User-agent': USER_AGENT,
5609
+ 'Origin': 'https://www.mlb.com',
5610
+ 'Accept-Encoding': 'gzip, deflate, br',
5611
+ 'Content-type': 'application/json'
5612
+ },
5613
+ gzip: true
5614
+ }
5615
+ var response = await this.httpGet(reqObj, false)
5616
+ if ( response && this.isValidJson(response) ) {
5617
+ //this.debuglog(response)
5618
+ let teams_data = JSON.parse(response)
5619
+
5620
+ let parent_orgs = {}
5621
+ if ( teams_data && teams_data.teams ) {
5622
+ for (var i=0; i<teams_data.teams.length; i++) {
5623
+ if (teams_data.teams[i].sport.id == 1) {
5624
+ parent_orgs[teams_data.teams[i].id] = teams_data.teams[i].abbreviation
5625
+ affiliates_data[teams_data.teams[i].abbreviation] = []
5626
+ }
5627
+ }
5628
+ for (var i=0; i<teams_data.teams.length; i++) {
5629
+ if (teams_data.teams[i].sport.id != 1) {
5630
+ teams_data.teams[i].abbreviation
5631
+ affiliates_data[parent_orgs[teams_data.teams[i].parentOrgId]].push(teams_data.teams[i].id)
5632
+ affiliates_data[parent_orgs[teams_data.teams[i].parentOrgId]].sort((a, b) => a - b)
5633
+ }
5634
+ }
5635
+ for (const [key, value] of Object.entries(affiliates_data)) {
5636
+ affiliates_data[key] = value.join(',')
5637
+ }
5638
+
5639
+ console.log(JSON.stringify(this.sortObj(affiliates_data)))
5640
+ }
5641
+ } else {
5642
+ this.log('error : invalid json from url ' + reqObj.url)
5643
+ }
5644
+ } catch(e) {
5645
+ this.log('getAffiliates error : ' + e.message)
5646
+ }
5647
+ }
5618
5648
  }
5619
5649
 
5620
5650
  module.exports = sessionClass