mlbserver 2026.3.27-2 → 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 (4) hide show
  1. package/Dockerfile +5 -40
  2. package/index.js +190 -40
  3. package/package.json +1 -2
  4. package/session.js +74 -159
package/Dockerfile CHANGED
@@ -1,24 +1,13 @@
1
- # --- Build Stage ---
2
- FROM node:18-alpine AS build
1
+ FROM node:16-alpine
3
2
 
4
- # Set environment variable to skip the automatic Chromium download by Puppeteer
5
- ENV PUPPETEER_SKIP_DOWNLOAD=true
6
- ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
7
-
8
- # Install system-level dependencies for Chromium on Alpine
9
- RUN apk update && apk add --no-cache \
10
- tzdata \
11
- udev \
12
- ttf-freefont \
13
- chromium \
14
- nss \
15
- freetype \
16
- harfbuzz \
17
- ca-certificates
3
+ RUN apk update && apk add tzdata
18
4
 
19
5
  # Create app directory
20
6
  WORKDIR /mlbserver
21
7
 
8
+ # Add data directory
9
+ VOLUME /mlbserver/data_directory
10
+
22
11
  # Install app dependencies
23
12
  # A wildcard is used to ensure both package.json AND package-lock.json are copied
24
13
  # where available (npm@5+)
@@ -31,29 +20,5 @@ RUN npm install
31
20
  # Bundle app source
32
21
  COPY . .
33
22
 
34
- # --- Runtime Stage ---
35
- FROM node:20-alpine AS runtime
36
-
37
- # Install only the necessary runtime dependencies again
38
- RUN apk add --no-cache \
39
- tzdata \
40
- udev \
41
- ttf-freefont \
42
- chromium \
43
- nss \
44
- freetype \
45
- harfbuzz \
46
- ca-certificates
47
-
48
- # Set the executable path for Puppeteer
49
- ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
50
-
51
- WORKDIR /mlbserver
52
- # Copy built application from the build stage
53
- COPY --from=build /mlbserver .
54
-
55
- # Add data directory
56
- VOLUME /mlbserver/data_directory
57
-
58
23
  EXPOSE 9999 10000
59
24
  CMD [ "node", "index.js", "--env", "--port", "9999", "--multiview_port", "10000", "--data_directory", "/mlbserver/data_directory" ]
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)
@@ -1542,8 +1543,7 @@ app.get('/', async function(req, res) {
1542
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}'
1543
1544
 
1544
1545
  // Highlights CSS
1545
- //max-height:calc(100vh-110px);
1546
- 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;}'
1547
1547
 
1548
1548
  // Tooltip CSS
1549
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;}'
@@ -1829,37 +1829,38 @@ app.get('/', async function(req, res) {
1829
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') ) {
1830
1830
  // Scraped Big Inning schedule
1831
1831
  big_inning = await session.getBigInningSchedule(gameDate)
1832
-
1833
- // Generated Big Inning schedule (disabled)
1834
- //big_inning = await session.generateBigInningSchedule(gameDate)
1835
- }
1836
- if ( big_inning && big_inning.start ) {
1837
- 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>'
1838
- let compareStart = new Date(big_inning.start)
1839
- compareStart.setMinutes(compareStart.getMinutes()-10)
1840
- let compareEnd = new Date(big_inning.end)
1841
- compareEnd.setHours(compareEnd.getHours()+1)
1842
- if ( (currentDate >= compareStart) && (currentDate < compareEnd) ) {
1843
- let querystring = '?event=biginning'
1844
- let multiviewquerystring = querystring + '&resolution=' + DEFAULT_MULTIVIEW_RESOLUTION
1845
- if ( linkType == VALID_LINK_TYPES[0] ) {
1846
- if ( startFrom != VALID_START_FROM[0] ) querystring += '&startFrom=' + startFrom
1847
- if ( controls != VALID_CONTROLS[0] ) querystring += '&controls=' + controls
1848
- }
1849
- if ( resolution != VALID_RESOLUTIONS[0] ) querystring += '&resolution=' + resolution
1850
- if ( linkType == VALID_LINK_TYPES[1] ) {
1851
- if ( force_vod != VALID_FORCE_VOD[0] ) querystring += '&force_vod=' + force_vod
1852
- } else if ( linkType == VALID_LINK_TYPES[4] ) {
1853
- 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"
1854
1862
  }
1855
- querystring += content_protect_b
1856
- multiviewquerystring += content_protect_b
1857
- body += '<a href="' + thislink + querystring + '">Big Inning</a>'
1858
- body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)">'
1859
- } else {
1860
- body += 'Big Inning'
1861
1863
  }
1862
- body += '</td></tr>' + "\n"
1863
1864
  }
1864
1865
 
1865
1866
  // Game Changer and Stream Finder
@@ -3320,11 +3321,13 @@ function start_multiview_stream(streams, sync, dvr, faster, reencode, park_audio
3320
3321
  //let audio_input = audio_present[i] + ':a:m:language:en?'
3321
3322
  let audio_input = audio_present[i] + ':a:'
3322
3323
  let video_url = streams[audio_present[i]]
3323
- 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=') ) {
3324
3327
  audio_input += '0'
3325
- } else {
3328
+ /*} else {
3326
3329
  audio_input += '1'
3327
- }
3330
+ }*/
3328
3331
  let filter = ''
3329
3332
  // Optionally apply sync adjustments
3330
3333
  if ( sync[audio_present[i]] ) {
@@ -3531,11 +3534,13 @@ app.get('/download.ts', async function(req, res) {
3531
3534
  if ( ! (await protect(req, res)) ) return
3532
3535
 
3533
3536
  try {
3537
+ let ffmpeg_timeout = 432000
3534
3538
  // we'll know it's an actual download request if it include a filename parameter
3535
3539
  if ( req.query.filename ) {
3536
3540
  session.requestlog('download.ts', req)
3537
3541
  } else {
3538
3542
  session.debuglog('force alternate audio', req)
3543
+ ffmpeg_timeout = 20
3539
3544
  }
3540
3545
 
3541
3546
  let server = 'http://127.0.0.1:' + session.data.port + http_root
@@ -3561,7 +3566,7 @@ app.get('/download.ts', async function(req, res) {
3561
3566
  }
3562
3567
  }
3563
3568
 
3564
- ffmpeg_command = ffmpeg({ timeout: 432000 })
3569
+ ffmpeg_command = ffmpeg({ timeout: ffmpeg_timeout })
3565
3570
 
3566
3571
  // Set input stream and minimize ffmpeg startup latency
3567
3572
  ffmpeg_command.input(video_url)
@@ -3619,15 +3624,21 @@ app.get('/download.ts', async function(req, res) {
3619
3624
  ffmpeg_command.addOutputOption('-f', 'mpegts')
3620
3625
  .output(res)
3621
3626
  .on('start', function(commandLine) {
3622
- session.debuglog('download.ts command started')
3627
+ session.debuglog('download.ts command started for ' + video_url)
3623
3628
  if ( argv.debug || argv.ffmpeg_logging ) {
3624
- session.debuglog('download.ts command: ' + commandLine)
3629
+ session.log('download.ts command: ' + commandLine)
3625
3630
  }
3626
3631
  })
3627
3632
  .on('error', function(err, stdout, stderr) {
3628
- session.debuglog('download.ts command stopped: ' + err.message)
3629
- if ( stdout ) session.log(stdout)
3630
- 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)
3631
3642
  })
3632
3643
  .on('end', function() {
3633
3644
  session.debuglog('download.ts command ended')
@@ -3795,3 +3806,142 @@ app.get('/comskip.txt', async function(req, res) {
3795
3806
  res.end('comskip.txt request error, check log')
3796
3807
  }
3797
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.27-2",
3
+ "version": "2026.4.1-2",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,7 +13,6 @@
13
13
  "http": "0.0.1-security",
14
14
  "http-attach": "^1.0.0",
15
15
  "minimist": "^1.2.8",
16
- "puppeteer": "^24.40.0",
17
16
  "readline-sync": "^1.4.10",
18
17
  "request": "^2.88.2",
19
18
  "request-promise": "^4.2.6",
package/session.js CHANGED
@@ -8,7 +8,6 @@ const path = require('path')
8
8
  const readlineSync = require('readline-sync')
9
9
  const FileCookieStore = require('tough-cookie-filestore')
10
10
  const parseString = require('xml2js').parseString
11
- const puppeteer = require('puppeteer')
12
11
 
13
12
  const MULTIVIEW_DIRECTORY_NAME = 'multiview'
14
13
 
@@ -865,8 +864,6 @@ class sessionClass {
865
864
  constructor(argv = {}) {
866
865
  this.debug = argv.debug
867
866
 
868
- this.executablePath = argv.PUPPETEER_EXECUTABLE_PATH
869
-
870
867
  let dirname = __dirname
871
868
  if ( argv.data_directory ) {
872
869
  dirname = argv.data_directory
@@ -2922,25 +2919,28 @@ class sessionClass {
2922
2919
  let gameDate = cache_data.dates[i].date
2923
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] ) {
2924
2921
  this.debuglog('getTVData Big Inning active for date ' + cache_data.dates[i].date)
2925
- // Scraped Big Inning schedule
2926
- let start = this.convertDateToXMLTV(new Date(this.cache.bigInningSchedule[gameDate].start))
2927
- let stop = this.convertDateToXMLTV(new Date(this.cache.bigInningSchedule[gameDate].end))
2928
-
2929
- // Big Inning calendar ICS
2930
- let prefix = 'Watch'
2931
- let location = server + '/embed.html?event=biginning&mediaType=Video&resolution=' + resolution
2932
- if ( this.protection.content_protect ) location += '&content_protect=' + this.protection.content_protect
2933
- calendar += await this.generate_ics_event(prefix, new Date(this.cache.bigInningSchedule[gameDate].start), new Date(this.cache.bigInningSchedule[gameDate].end), title, description, location)
2934
2922
 
2935
- // Off Air if necessary
2936
- let off_air_event = await this.generate_off_air_event(offAir, channelid, gameDate, channels[channelid].stop, this.cache.bigInningSchedule[gameDate].start, title)
2937
- if ( off_air_event ) {
2938
- programs += off_air_event
2939
- channels[channelid].stop = stop
2940
- }
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
+ }
2941
2940
 
2942
- // Big Inning guide XML
2943
- 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
+ }
2944
2944
  }
2945
2945
  this.debuglog('getTVData completed Big Inning for date ' + cache_data.dates[i].date)
2946
2946
  }
@@ -3542,122 +3542,66 @@ class sessionClass {
3542
3542
  // temporarily disable Big Inning schedule checking until a new source URL is available
3543
3543
  /*this.cache.bigInningSchedule = {}
3544
3544
  return*/
3545
-
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
+
3546
3557
  let currentDate = new Date()
3547
3558
  if ( !this.cache || !this.cache.bigInningScheduleCacheExpiry || (currentDate > new Date(this.cache.bigInningScheduleCacheExpiry)) ) {
3548
3559
  if ( !this.cache.bigInningSchedule ) this.cache.bigInningSchedule = {}
3549
-
3550
- const browser = await puppeteer.launch({
3551
- headless: 'new',
3552
- executablePath: this.executablePath,
3553
- args: [
3554
- '--no-sandbox',
3555
- '--disable-gpu',
3556
- '--disable-setuid-sandbox',
3557
- '--disable-dev-shm-usage'
3558
- ],
3559
- })
3560
- const page = await browser.newPage()
3561
- await page.setUserAgent(USER_AGENT)
3562
- await page.goto('https://support.mlb.com/s/article/What-Is-MLB-Big-Inning?language=en_US', { waitUntil: 'networkidle0' })
3563
- const response = await page.content()
3564
- await browser.close()
3565
-
3566
- // break HTML into array based on table rows
3567
- var rows = response.split('<tr ')
3568
- // start iterating at 2 (after header row)
3569
- for (var i=2; i<rows.length; i++) {
3570
- // split HTML row into array with columns
3571
- let cols = rows[i].split('<td ')
3572
-
3573
- // define some variables that persist for each row
3574
- let parts
3575
- let year
3576
- let month
3577
- let day
3578
- let this_datestring
3579
- let add_date = 0
3580
- let d
3581
-
3582
- // start iterating at 2 (after DOW column)
3583
- for (var j=2; j<cols.length; j++) {
3584
- // split on brackets to get column text at resulting array index 0
3585
- let col = cols[j].split('>')[1].split('<')
3586
- switch(j){
3587
- // first column is date
3588
- case 2:
3589
- // split date into array
3590
- // old date format (January 1, 1970) (disabled)
3591
- /*parts = col[0].split(' ')
3592
- year = parts[2]
3593
- // get month index, zero-based
3594
- month = new Date(Date.parse(parts[0] +" 1, 2021")).getMonth()
3595
- day = parts[1].substring(0,parts[1].length-3)*/
3596
- // new date format (01/01/70)
3597
- parts = col[0].split('/')
3598
- year = parts[2]
3599
- if ( year.length == 2 ) {
3600
- year = '20' + parts[2]
3601
- }
3602
- // get month index, zero-based
3603
- month = parseInt(parts[0]) - 1
3604
- day = parts[1]
3605
- this_datestring = new Date(year, month, day).toISOString().substring(0,10)
3606
- this.cache.bigInningSchedule[this_datestring] = {}
3607
- // increment month index (not zero-based)
3608
- month += 1
3609
- break
3610
- // remaining columns are times
3611
- default:
3612
- let hour
3613
- let minute = '00'
3614
- let ampm
3615
- // if time has colon, split into array on that to get hour and minute parts
3616
- if ( col[0].indexOf(':') > 0 ) {
3617
- parts = col[0].split(':')
3618
- hour = parseInt(parts[0])
3619
- minute = parts[1].substring(0,2)
3620
- } else {
3621
- hour = parseInt(col[0].substring(0,col[0].length-2))
3622
- }
3623
- ampm = col[0].substring(col[0].length-2,col[0].length)
3624
- // convert hour to 24-hour format
3625
- if ( (ampm == 'PM') || ((hour == 12) && (ampm == 'AM')) ) {
3626
- hour += 12
3627
- }
3628
- // these times are EDT so add 4 for UTC
3629
- hour += 4
3630
- // if hour is beyond 23, note we will have to add 1 day
3631
- if ( hour > 23 ) {
3632
- add_date = 1
3633
- hour -= 24
3634
- }
3635
-
3636
- d = new Date(this_datestring + 'T' + hour.toString().padStart(2, '0') + ':' + minute.toString().padStart(2, '0') + ':00.000+00:00')
3637
- d.setDate(d.getDate()+add_date)
3638
- switch(j){
3639
- // 3rd column is start time
3640
- case 3:
3641
- this.cache.bigInningSchedule[this_datestring].start = d
3642
- break
3643
- // 3rd column is end time
3644
- case 4:
3645
- this.cache.bigInningSchedule[this_datestring].end = d
3646
- break
3647
- }
3648
- break
3649
- }
3650
- }
3560
+ let reqObj = {
3561
+ url: 'https://www.fubo.tv/welcome/channel/mlb-big-inning',
3562
+ headers: {
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',
3566
+ 'user-agent': USER_AGENT
3567
+ },
3568
+ gzip: true
3651
3569
  }
3652
- this.debuglog(JSON.stringify(this.cache.bigInningSchedule))
3570
+ var response = await this.httpGet(reqObj, false)
3571
+ if ( response ) {
3572
+ // disabled because it's big
3573
+ //this.debuglog(response)
3574
+
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] = []
3586
+ }
3587
+ this.cache.bigInningSchedule[this_datestring].push({
3588
+ start: airing.accessRightsV2.live.startTime,
3589
+ end: airing.accessRightsV2.live.endTime
3590
+ })
3591
+ });
3592
+ });
3593
+ this.debuglog(JSON.stringify(this.cache.bigInningSchedule))
3653
3594
 
3654
- // Default cache period is 1 day from now
3655
- let oneDayFromNow = new Date()
3656
- oneDayFromNow.setDate(oneDayFromNow.getDate()+1)
3657
- let cacheExpiry = oneDayFromNow
3658
- this.cache.bigInningScheduleCacheExpiry = cacheExpiry
3595
+ // Default cache period is 1 day from now
3596
+ let oneDayFromNow = new Date()
3597
+ oneDayFromNow.setDate(oneDayFromNow.getDate()+1)
3598
+ let cacheExpiry = oneDayFromNow
3599
+ this.cache.bigInningScheduleCacheExpiry = cacheExpiry
3659
3600
 
3660
- this.save_cache_data()
3601
+ this.save_cache_data()
3602
+ } else {
3603
+ this.log('error : invalid response from url ' + reqObj.url)
3604
+ }
3661
3605
  } else {
3662
3606
  this.debuglog('using cached big inning schedule')
3663
3607
  }
@@ -3672,35 +3616,6 @@ class sessionClass {
3672
3616
  }
3673
3617
  }
3674
3618
 
3675
- // Generate generic Big Inning schedule for specified date
3676
- // times in UTC (and DST) according to https://www.mlb.com/live-stream-games/help-center/subscription-access-big-inning
3677
- async generateBigInningSchedule(dateString) {
3678
- try {
3679
- this.debuglog('generateBigInningSchedule')
3680
-
3681
- let utc_start_string = '01:00'
3682
- let utc_end_string = '03:30'
3683
- let add_date = 1
3684
- // Different Sunday schedule
3685
- let weekday_index = new Date(dateString + ' 00:00:00').getDay()
3686
- if ( weekday_index == 0 ) {
3687
- utc_start_string = '19:00'
3688
- utc_end_string = '21:30'
3689
- add_date = 0
3690
- }
3691
- let d = new Date(dateString + 'T' + utc_start_string + ':00.000+00:00')
3692
- d.setDate(d.getDate()+add_date)
3693
- let start = d
3694
- d = new Date(dateString + 'T' + utc_end_string + ':00.000+00:00')
3695
- d.setDate(d.getDate()+add_date)
3696
- let end = d
3697
-
3698
- return {start: start, end: end}
3699
- } catch(e) {
3700
- this.log('generateBigInningSchedule error : ' + e.message)
3701
- }
3702
- }
3703
-
3704
3619
  // Get event data
3705
3620
  async getEventData(url) {
3706
3621
  try {