mlbserver 2026.2.23 → 2026.3.27-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 +40 -5
  2. package/index.js +55 -11
  3. package/package.json +2 -1
  4. package/session.js +204 -54
package/Dockerfile CHANGED
@@ -1,13 +1,24 @@
1
- FROM node:16-alpine
1
+ # --- Build Stage ---
2
+ FROM node:18-alpine AS build
2
3
 
3
- RUN apk update && apk add tzdata
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
4
18
 
5
19
  # Create app directory
6
20
  WORKDIR /mlbserver
7
21
 
8
- # Add data directory
9
- VOLUME /mlbserver/data_directory
10
-
11
22
  # Install app dependencies
12
23
  # A wildcard is used to ensure both package.json AND package-lock.json are copied
13
24
  # where available (npm@5+)
@@ -20,5 +31,29 @@ RUN npm install
20
31
  # Bundle app source
21
32
  COPY . .
22
33
 
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
+
23
58
  EXPOSE 9999 10000
24
59
  CMD [ "node", "index.js", "--env", "--port", "9999", "--multiview_port", "10000", "--data_directory", "/mlbserver/data_directory" ]
package/index.js CHANGED
@@ -431,7 +431,7 @@ var getKey = function(url, headers, cb) {
431
431
  requestRetry(url, headers, function(err, response) {
432
432
  if (err) return cb(err)
433
433
  let key = response.body
434
- session.debuglog('key returned ' + key)
434
+ session.debuglog('key returned ' + Buffer.from(key, 'binary').toString('base64'))
435
435
  session.temp_cache.prevKeys[url] = key
436
436
  cb(null, key)
437
437
  })
@@ -1262,15 +1262,28 @@ app.get('/gamechangerplaylist.m3u8', async function(req, res) {
1262
1262
  let new_segments_complete = false
1263
1263
  let segment_count = 0
1264
1264
  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)
1265
+ if ( body[i].startsWith('#EXT-X-KEY') ) {
1266
+ let key = url.resolve(u, body[i].match('URI="([^"]+)"')[1])
1267
+ let iv = body[i].match('IV=0x(.*)$')[1]
1268
+ let ts
1269
+ let extinf
1270
+ for (var j=1; j<=4; j++) {
1271
+ if ( body[i+j] ) {
1272
+ if ( !extinf && body[i+j].startsWith('#EXTINF') ) {
1273
+ extinf = body[i+j]
1274
+ } else if ( !ts && !body[i+j].startsWith('#') ) {
1275
+ ts = url.resolve(u, body[i+j])
1276
+ }
1277
+ if ( extinf && ts ) break;
1278
+ }
1279
+ }
1280
+ if ( key && iv && extinf && ts && !new_segments_complete ) {
1281
+ session.debuglog(game_changer_title + 'found segment ' + ts)
1269
1282
  if ( discontinuity ) {
1270
1283
  session.debuglog(game_changer_title + 'only getting newest segment after stream change')
1271
- new_segments.unshift({'extinf':body[i], 'ts':line, 'streamURLToken':streamURLToken})
1284
+ new_segments.unshift({'key':key, 'iv':iv, 'extinf':extinf, 'ts':ts, 'streamURLToken':streamURLToken})
1272
1285
  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) ) {
1286
+ } 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
1287
  session.debuglog(game_changer_title + 'found previous last segment')
1275
1288
  new_segments_complete = true
1276
1289
  } else if ( segment_count == GAMECHANGER_LIST_SIZE ) {
@@ -1281,7 +1294,7 @@ app.get('/gamechangerplaylist.m3u8', async function(req, res) {
1281
1294
  }
1282
1295
  new_segments_complete = true
1283
1296
  } else {
1284
- new_segments.unshift({'extinf':body[i], 'ts':line, 'streamURLToken':streamURLToken})
1297
+ new_segments.unshift({'key':key, 'iv':iv, 'extinf':extinf, 'ts':ts, 'streamURLToken':streamURLToken})
1285
1298
  }
1286
1299
  }
1287
1300
  segment_count++
@@ -1314,7 +1327,7 @@ app.get('/gamechangerplaylist.m3u8', async function(req, res) {
1314
1327
  if ( session.temp_cache.gamechanger[id].segments[i].discontinuity ) {
1315
1328
  session.temp_cache.gamechanger[id].playlist[resolution] += '#EXT-X-DISCONTINUITY' + '\n'
1316
1329
  }
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'
1330
+ 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
1331
  }
1319
1332
 
1320
1333
  session.debuglog(game_changer_title + 'playlist ' + session.temp_cache.gamechanger[id].playlist[resolution])
@@ -1684,6 +1697,33 @@ app.get('/', async function(req, res) {
1684
1697
  let currentDate = new Date()
1685
1698
 
1686
1699
  let entitlements = await session.getEntitlements()
1700
+
1701
+ // MASN live stream for entitled subscribers
1702
+ try {
1703
+ if ( entitlements.includes('MASN_110') ) {
1704
+ body += '<tr><td><span class="tooltip">MASN<span class="tooltiptext">MASN live stream for entitled subscribers. <a href="https://support.mlb.com/s/article/MASN-In-Market-Offering">See here for more information</a>.</span></span></td><td>'
1705
+ let querystring = '?event=MASN'
1706
+ let multiviewquerystring = querystring + '&resolution=' + DEFAULT_MULTIVIEW_RESOLUTION
1707
+ if ( linkType == VALID_LINK_TYPES[0] ) {
1708
+ if ( startFrom != VALID_START_FROM[0] ) querystring += '&startFrom=' + startFrom
1709
+ if ( controls != VALID_CONTROLS[0] ) querystring += '&controls=' + controls
1710
+ }
1711
+ if ( resolution != VALID_RESOLUTIONS[0] ) querystring += '&resolution=' + resolution
1712
+ if ( linkType == VALID_LINK_TYPES[1] ) {
1713
+ if ( force_vod != VALID_FORCE_VOD[0] ) querystring += '&force_vod=' + force_vod
1714
+ } else if ( linkType == VALID_LINK_TYPES[4] ) {
1715
+ querystring += '&filename=' + gameDate + ' MASN'
1716
+ }
1717
+ querystring += content_protect_b
1718
+ multiviewquerystring += content_protect_b
1719
+ body += '<a href="' + thislink + querystring + '">MASN</a>'
1720
+ body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)">'
1721
+ body += '</td></tr>' + "\n"
1722
+ } // end entitlements check
1723
+ } catch (e) {
1724
+ session.debuglog('MASN detect error : ' + e.message)
1725
+ }
1726
+
1687
1727
  // MLB Network live stream for eligible USA subscribers
1688
1728
  try {
1689
1729
  if ( entitlements.includes('MLBN') || entitlements.includes('EXECMLB') || entitlements.includes('MLBTVMLBNADOBEPASS') ) {
@@ -2438,7 +2478,7 @@ app.get('/', async function(req, res) {
2438
2478
  resolution = 'best'
2439
2479
  }
2440
2480
 
2441
- 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"
2481
+ 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 MASN, 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"
2442
2482
 
2443
2483
  let include_teams = 'ath,atl'
2444
2484
  if ( (session.credentials.fav_teams.length > 0) && (session.credentials.fav_teams[0].length > 0) ) {
@@ -2457,6 +2497,10 @@ app.get('/', async function(req, res) {
2457
2497
 
2458
2498
  body += '<p><span class="tooltip">Include (or exclude) Winter Leagues<span class="tooltiptext">Winter leagues include the Arizona Fall League, Dominican Winter League aka Liga de Beisbol Dominicano, and Mexican Winter League aka Liga Mexicana del Pacífico. 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=winter' + content_protect_b + '">m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=winter' + content_protect_b + '">xml</a> and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=winter' + content_protect_b + '">ics</a></p>' + "\n"
2459
2499
 
2500
+ if ( entitlements.includes('MASN_110') ) {
2501
+ body += '<p><span class="tooltip">Include (or exclude) MASN<span class="tooltiptext">MASN live stream for entitled subscribers. <a href="https://support.mlb.com/s/article/MASN-In-Market-Offering">See here for more information</a>.</span></span>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=masn' + content_protect_b + '">m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=masn' + content_protect_b + '">xml</a></p>' + "\n"
2502
+ }
2503
+
2460
2504
  if ( entitlements.includes('MLBN') || entitlements.includes('EXECMLB') || entitlements.includes('MLBTVMLBNADOBEPASS') ) {
2461
2505
  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"
2462
2506
  }
@@ -2466,7 +2510,7 @@ app.get('/', async function(req, res) {
2466
2510
  }
2467
2511
 
2468
2512
  if ( entitlements.includes('SNY_121') ) {
2469
- 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"
2513
+ 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/SNY-In-Market-Offering">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"
2470
2514
  }
2471
2515
 
2472
2516
  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"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mlbserver",
3
- "version": "2026.2.23",
3
+ "version": "2026.3.27-2",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,6 +13,7 @@
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",
16
17
  "readline-sync": "^1.4.10",
17
18
  "request": "^2.88.2",
18
19
  "request-promise": "^4.2.6",
package/session.js CHANGED
@@ -8,6 +8,7 @@ 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')
11
12
 
12
13
  const MULTIVIEW_DIRECTORY_NAME = 'multiview'
13
14
 
@@ -28,7 +29,7 @@ const LIDOM_TEAM_IDS = { 'AGU': '667', 'TOR': '668', 'EST': '669', 'GIG': '670',
28
29
 
29
30
  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
31
 
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' }
32
+ 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
33
 
33
34
  // First is default level, last should be All (also used as default org)
34
35
  const LEVELS = { 'MLB': '1', 'AAA': '11', 'AA': '12', 'A+': '13', 'A': '14', 'WINTER': '17', 'All': '1,11,12,13,14,17' }
@@ -864,6 +865,8 @@ class sessionClass {
864
865
  constructor(argv = {}) {
865
866
  this.debug = argv.debug
866
867
 
868
+ this.executablePath = argv.PUPPETEER_EXECUTABLE_PATH
869
+
867
870
  let dirname = __dirname
868
871
  if ( argv.data_directory ) {
869
872
  dirname = argv.data_directory
@@ -2368,8 +2371,8 @@ class sessionClass {
2368
2371
  stream = server + '/stream.m3u8?event=' + encodeURIComponent(cache_data.dates[i].games[j].teams['home'].team.shortName.toUpperCase())
2369
2372
  }
2370
2373
  stream += '&league_id=' + league_id
2374
+ stream += '&mediaType=' + streamMediaType
2371
2375
  }
2372
- stream += '&mediaType=' + streamMediaType
2373
2376
  stream += '&level=' + encodeURIComponent(this.getLevelNameFromSportId(sportId))
2374
2377
  stream += '&resolution=' + resolution
2375
2378
  if ( this.protection.content_protect ) stream += '&content_protect=' + this.protection.content_protect
@@ -2764,6 +2767,39 @@ class sessionClass {
2764
2767
  channels = this.sortObj(channels)
2765
2768
 
2766
2769
  let entitlements = await this.getEntitlements()
2770
+
2771
+ // MASN live stream for entitled subscribers
2772
+ try {
2773
+ if ( (entitlements.includes('MASN_110')) ) {
2774
+ if ( (mediaType == 'MLBTV') && ((includeLevels.length == 0) || includeLevels.includes('MLB') || includeLevels.includes('ALL')) ) {
2775
+ if ( (excludeTeams.length > 0) && excludeTeams.includes('MASN') ) {
2776
+ // do nothing
2777
+ } else if ( (includeTeams.length == 0) || includeTeams.includes('MASN') ) {
2778
+ this.debuglog('getTVData processing MASN')
2779
+ let logo = 'https://img.mlbstatic.com/mlb-images/image/upload/t_16x9/t_w640/v1745242435/mlb/jov4fxbzmqikc8umj5kr.png'
2780
+ let channelid = mediaType + '.MASN'
2781
+ //if ( this.protection.content_protect ) logo += '&amp;content_protect=' + this.protection.content_protect
2782
+ let stream = server + '/stream.m3u8?event=masn&mediaType=Video&resolution=' + resolution
2783
+ if ( this.protection.content_protect ) stream += '&content_protect=' + this.protection.content_protect
2784
+ if ( pipe == 'true' ) stream = await this.convert_stream_to_pipe(stream, channelid)
2785
+ channels[channelid] = await this.create_channel_object(channelid, logo, stream, mediaType)
2786
+
2787
+ let title = 'MASN'
2788
+ let description = 'Live stream of MASN (Mid-Atlantic Sports Network)'
2789
+
2790
+ let start = this.convertDateToXMLTV(new Date(cache_data.dates[0].date + ' 00:00:00'))
2791
+ let stop = this.convertDateToXMLTV(new Date(cache_data.dates[cache_data.dates.length-1].date + ' 00:00:00'))
2792
+
2793
+ // MASN guide XML
2794
+ programs += await this.generate_xml_program(channelid, start, stop, title, description, logo, this.convertStringToAirDate(cache_data.dates[0].date))
2795
+ this.debuglog('getTVData completed MASN')
2796
+ } // end includeTeams check
2797
+ } // end mediaType check
2798
+ } // end entitlements check
2799
+ } catch (e) {
2800
+ this.debuglog('getTVData MASN detect error : ' + e.message)
2801
+ }
2802
+
2767
2803
  // MLB Network live stream for eligible USA subscribers
2768
2804
  try {
2769
2805
  if ( (entitlements.includes('MLBN') || entitlements.includes('EXECMLB') || entitlements.includes('MLBTVMLBNADOBEPASS')) ) {
@@ -2850,7 +2886,7 @@ class sessionClass {
2850
2886
  let start = this.convertDateToXMLTV(new Date(cache_data.dates[0].date + ' 00:00:00'))
2851
2887
  let stop = this.convertDateToXMLTV(new Date(cache_data.dates[cache_data.dates.length-1].date + ' 00:00:00'))
2852
2888
 
2853
- // SNLA guide XML
2889
+ // SNY guide XML
2854
2890
  programs += await this.generate_xml_program(channelid, start, stop, title, description, logo, this.convertStringToAirDate(cache_data.dates[0].date))
2855
2891
  this.debuglog('getTVData completed SNY')
2856
2892
  } // end includeTeams check
@@ -3286,6 +3322,7 @@ class sessionClass {
3286
3322
  async getSkipMarkers(gamePk, skip_type, start_inning, start_inning_half, streamURL, streamURLToken, skip_adjust, broadcast_start_timestamp=false) {
3287
3323
  try {
3288
3324
  this.debuglog('getSkipMarkers')
3325
+ let variantPlaylist;
3289
3326
 
3290
3327
  if ( skip_adjust != 0 ) this.log('manual adjustment of ' + skip_adjust + ' seconds being applied')
3291
3328
 
@@ -3309,7 +3346,7 @@ class sessionClass {
3309
3346
 
3310
3347
  // Get the broadcast start time first, if necessary -- event times will be relative to this
3311
3348
  if ( !broadcast_start_timestamp ) {
3312
- let variantPlaylist = await this.getVariantPlaylist(streamURL, streamURLToken)
3349
+ variantPlaylist = await this.getVariantPlaylist(streamURL, streamURLToken)
3313
3350
  broadcast_start_timestamp = await this.getBroadcastStart(variantPlaylist)
3314
3351
  }
3315
3352
 
@@ -3455,6 +3492,10 @@ class sessionClass {
3455
3492
  // if skipping commercials, look at the variant playlist to detect insertions
3456
3493
  if ( skip_type == 4 ) {
3457
3494
  this.debuglog('detecting commercial breaks')
3495
+ if (!variantPlaylist) {
3496
+ this.debuglog('variantPlaylist missing, fetching...')
3497
+ variantPlaylist = await this.getVariantPlaylist(streamURL, streamURLToken)
3498
+ }
3458
3499
  let body = variantPlaylist
3459
3500
  let break_active = false
3460
3501
  let break_end = 0
@@ -3505,62 +3546,118 @@ class sessionClass {
3505
3546
  let currentDate = new Date()
3506
3547
  if ( !this.cache || !this.cache.bigInningScheduleCacheExpiry || (currentDate > new Date(this.cache.bigInningScheduleCacheExpiry)) ) {
3507
3548
  if ( !this.cache.bigInningSchedule ) this.cache.bigInningSchedule = {}
3508
- let reqObj = {
3509
- url: 'https://api.fubo.tv/gg/series/123881219/live-programs?limit=14&languages=en&countrySlugs=USA',
3510
- headers: {
3511
- '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',
3512
- 'accept-language': 'en-US,en;q=0.9',
3513
- 'cache-control': 'no-cache',
3514
- 'dnt': '1',
3515
- 'pragma': 'no-cache',
3516
- 'sec-ch-ua': '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"',
3517
- 'sec-ch-ua-mobile': '?0',
3518
- 'sec-ch-ua-platform': '"macOS"',
3519
- 'sec-fetch-dest': 'document',
3520
- 'sec-fetch-mode': 'navigate',
3521
- 'sec-fetch-site': 'none',
3522
- 'sec-fetch-user': '?1',
3523
- 'sec-gpc': '1',
3524
- 'upgrade-insecure-requests': '1',
3525
- 'user-agent': USER_AGENT
3526
- },
3527
- json: true,
3528
- gzip: true
3529
- }
3530
- var response = await this.httpGet(reqObj, false)
3531
- if ( response ) {
3532
- this.debuglog(JSON.stringify(response))
3533
-
3534
- if ( response.data ) {
3535
- for (var i=0; i < response.data.length; i++) {
3536
- if ( response.data[i].airings && (response.data[i].airings.length > 0) ) {
3537
- for (var j=0; j < response.data[i].airings.length; j++) {
3538
- 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 ) {
3539
- let est_date = new Date(response.data[i].airings[j].accessRightsV2.live.startTime).toLocaleString("en-US", {timeZone: 'America/New_York'})
3540
- let date_array = est_date.split(',')[0].split('/')
3541
- let this_datestring = date_array[2] + '-' + date_array[0].padStart(2, '0') + '-' + date_array[1].padStart(2, '0')
3542
- this.cache.bigInningSchedule[this_datestring] = {
3543
- start: response.data[i].airings[j].accessRightsV2.live.startTime,
3544
- end: response.data[i].airings[j].accessRightsV2.live.endTime
3545
- }
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
3546
3646
  break
3547
- }
3548
3647
  }
3549
- }
3648
+ break
3550
3649
  }
3551
3650
  }
3552
- this.debuglog(JSON.stringify(this.cache.bigInningSchedule))
3651
+ }
3652
+ this.debuglog(JSON.stringify(this.cache.bigInningSchedule))
3553
3653
 
3554
- // Default cache period is 1 day from now
3555
- let oneDayFromNow = new Date()
3556
- oneDayFromNow.setDate(oneDayFromNow.getDate()+1)
3557
- let cacheExpiry = oneDayFromNow
3558
- this.cache.bigInningScheduleCacheExpiry = cacheExpiry
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
3559
3659
 
3560
- this.save_cache_data()
3561
- } else {
3562
- this.log('error : invalid response from url ' + reqObj.url)
3563
- }
3660
+ this.save_cache_data()
3564
3661
  } else {
3565
3662
  this.debuglog('using cached big inning schedule')
3566
3663
  }
@@ -3848,6 +3945,8 @@ class sessionClass {
3848
3945
  let dateString = eventName.substring(12)
3849
3946
  this.debuglog('getEventStreamURL RecapRundown for ' + dateString)
3850
3947
  playbackURL = await this.getRecapRundownURL(dateString)
3948
+ } else if ( eventName.toUpperCase() == 'MASN' ) {
3949
+ playbackURL = await this.getLinearStreamURL('MASN_ONE_LIVE')
3851
3950
  } else if ( eventName.toUpperCase() == 'MLBN' ) {
3852
3951
  playbackURL = 'https://falcon.mlbinfra.com/api/v1/linear/mlbn'
3853
3952
  } else if ( eventName.toUpperCase() == 'SNLA' ) {
@@ -4314,6 +4413,7 @@ class sessionClass {
4314
4413
  if ( cache_data ) {
4315
4414
  if ( cache_data.dates && cache_data.dates[0] && cache_data.dates[0].games && (cache_data.dates[0].games.length > 0) ) {
4316
4415
  let team_data = this.temp_cache.gamechanger[id].streamFinderData.team_data
4416
+ let games_CLI = this.temp_cache.gamechanger[id].streamFinderData.games_CLI
4317
4417
 
4318
4418
  var games = []
4319
4419
 
@@ -5580,6 +5680,56 @@ class sessionClass {
5580
5680
  this.log('getComskipMarkers error : ' + e.message)
5581
5681
  }
5582
5682
  }
5683
+
5684
+ // generates AFFILIATE_TEAM_IDS, should be done each season
5685
+ async getAffiliates() {
5686
+ try {
5687
+ this.debuglog('getAffiliates')
5688
+
5689
+ let affiliates_data = {}
5690
+ let reqObj = {
5691
+ url: 'https://statsapi.mlb.com/api/v1/teams?sportIds=1,11,12,13,14&activeStatus=true&season=2026',
5692
+ headers: {
5693
+ 'User-agent': USER_AGENT,
5694
+ 'Origin': 'https://www.mlb.com',
5695
+ 'Accept-Encoding': 'gzip, deflate, br',
5696
+ 'Content-type': 'application/json'
5697
+ },
5698
+ gzip: true
5699
+ }
5700
+ var response = await this.httpGet(reqObj, false)
5701
+ if ( response && this.isValidJson(response) ) {
5702
+ //this.debuglog(response)
5703
+ let teams_data = JSON.parse(response)
5704
+
5705
+ let parent_orgs = {}
5706
+ if ( teams_data && teams_data.teams ) {
5707
+ for (var i=0; i<teams_data.teams.length; i++) {
5708
+ if (teams_data.teams[i].sport.id == 1) {
5709
+ parent_orgs[teams_data.teams[i].id] = teams_data.teams[i].abbreviation
5710
+ affiliates_data[teams_data.teams[i].abbreviation] = []
5711
+ }
5712
+ }
5713
+ for (var i=0; i<teams_data.teams.length; i++) {
5714
+ if (teams_data.teams[i].sport.id != 1) {
5715
+ teams_data.teams[i].abbreviation
5716
+ affiliates_data[parent_orgs[teams_data.teams[i].parentOrgId]].push(teams_data.teams[i].id)
5717
+ affiliates_data[parent_orgs[teams_data.teams[i].parentOrgId]].sort((a, b) => a - b)
5718
+ }
5719
+ }
5720
+ for (const [key, value] of Object.entries(affiliates_data)) {
5721
+ affiliates_data[key] = value.join(',')
5722
+ }
5723
+
5724
+ console.log(JSON.stringify(this.sortObj(affiliates_data)))
5725
+ }
5726
+ } else {
5727
+ this.log('error : invalid json from url ' + reqObj.url)
5728
+ }
5729
+ } catch(e) {
5730
+ this.log('getAffiliates error : ' + e.message)
5731
+ }
5732
+ }
5583
5733
  }
5584
5734
 
5585
5735
  module.exports = sessionClass