mlbserver 2025.6.12 → 2025.6.26

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 +27 -40
  2. package/package.json +1 -1
  3. package/session.js +54 -14
package/index.js CHANGED
@@ -365,10 +365,8 @@ app.get('/stream.m3u8', async function(req, res) {
365
365
  let skip_adjust = parseInt(req.query.skip_adjust) || DEFAULT_SKIP_ADJUST
366
366
 
367
367
  let skip_type = VALID_SKIP.indexOf(options.skip)
368
- // for skip other than commercial skip, look up markers
369
- if ( skip_type != 4 ) {
370
- await session.getSkipMarkers(gamePk, skip_type, options.inning_number, options.inning_half, streamURL, streamURLToken, skip_adjust)
371
- }
368
+ // look up markers
369
+ await session.getSkipMarkers(gamePk, skip_type, options.inning_number, options.inning_half, streamURL, streamURLToken, skip_adjust)
372
370
  }
373
371
  }
374
372
 
@@ -549,11 +547,6 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
549
547
  return
550
548
  }
551
549
 
552
- // Omit subtitles when skipping
553
- if ( line.startsWith('#EXT-X-MEDIA:TYPE=SUBTITLES') && (skip != 'none') ) {
554
- return
555
- }
556
-
557
550
  // Parse audio tracks to only include matching one, if specified
558
551
  if ( line.startsWith('#EXT-X-MEDIA:TYPE=AUDIO') ) {
559
552
  // if we've already returned our desired audio track, we can skip subsequent ones
@@ -649,11 +642,19 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
649
642
  }
650
643
 
651
644
  // Pass through any remaining caption tracks
652
- if ( line.startsWith('#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="eng",URI="') ) {
645
+ if ( line.startsWith('#EXT-X-MEDIA:TYPE=SUBTITLES,') ) {
653
646
  var parsed = line.match(',URI="([^"]+)"')
654
647
  if ( parsed[1] ) {
655
- newurl = http_root + '/playlist.m3u8?url='+encodeURIComponent(url.resolve(streamURL, parsed[1].trim())) + content_protect + referer_parameter + token_parameter
656
- return '#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="eng",URI="' + newurl + '"'
648
+ newurl = http_root + '/playlist.m3u8?url='+encodeURIComponent(url.resolve(streamURL, parsed[1].trim()))
649
+ if ( force_vod != VALID_FORCE_VOD[0] ) newurl += '&force_vod=on'
650
+ if ( inning_half != VALID_INNING_HALF[0] ) newurl += '&inning_half=' + inning_half
651
+ if ( inning_number != VALID_INNING_NUMBER[0] ) newurl += '&inning_number=' + inning_number
652
+ if ( skip != VALID_SKIP[0] ) newurl += '&skip=' + skip
653
+ if ( skip_adjust != DEFAULT_SKIP_ADJUST ) newurl += '&skip_adjust=' + skip_adjust
654
+ if ( pad != VALID_PAD[0] ) newurl += '&pad=' + pad
655
+ if ( gamePk ) newurl += '&gamePk=' + gamePk
656
+ newurl += content_protect + referer_parameter + token_parameter
657
+ return line.replace(parsed[1], newurl)
657
658
  }
658
659
  return
659
660
  }
@@ -768,23 +769,7 @@ app.get('/playlist.m3u8', async function(req, res) {
768
769
  content_protect = '&content_protect=' + session.protection.content_protect
769
770
  }
770
771
 
771
- // if skipping commercials, filter the playlist to remove ad insertion domains
772
- if ( skip == 'commercials' ) {
773
- session.debuglog('filtering commercial breaks')
774
- let new_body = []
775
- let break_active = false
776
- for (var i=0; i<body.length; i++) {
777
- if ( (break_active == false) && body[i].startsWith('#EXT-OATCLS-SCTE35:') ) {
778
- break_active = true
779
- new_body.push('#EXT-X-DISCONTINUITY')
780
- } else if ( (break_active == true) && body[i].startsWith('#EXT-X-CUE-IN') ) {
781
- break_active = false
782
- } else if ( break_active == false ) {
783
- new_body.push(body[i])
784
- }
785
- }
786
- body = new_body
787
- } else if ( (gamePk) && ((inning_half != VALID_INNING_HALF[0]) || (inning_number != VALID_INNING_NUMBER[0]) || (skip != VALID_SKIP[0])) && (typeof session.temp_cache[gamePk] !== 'undefined') && (typeof session.temp_cache[gamePk].skip_markers !== 'undefined') ) {
772
+ if ( (gamePk) && ((inning_half != VALID_INNING_HALF[0]) || (inning_number != VALID_INNING_NUMBER[0]) || (skip != VALID_SKIP[0])) && (typeof session.temp_cache[gamePk] !== 'undefined') && (typeof session.temp_cache[gamePk].skip_markers !== 'undefined') ) {
788
773
  session.debuglog('pulling skip markers from temporary cache')
789
774
  skip_markers = session.temp_cache[gamePk].skip_markers
790
775
  } else {
@@ -978,19 +963,22 @@ app.get('/subtitles.vtt', async function(req, res) {
978
963
  var u = req.query.url
979
964
  session.debuglog('subtitles.vtt url : ' + u)
980
965
 
966
+ var headers = {}
967
+
981
968
  var referer = false
982
969
  if ( req.query.referer ) {
970
+ session.debuglog('found subtitles.vtt referer : ' + req.query.referer)
983
971
  referer = decodeURIComponent(req.query.referer)
984
- session.debuglog('found subtitles.vtt referer : ' + referer)
972
+ headers.referer = referer
973
+ headers.origin = getOriginFromURL(referer)
985
974
  }
986
975
 
987
- var req = function () {
988
- var headers = {}
989
- if ( referer ) {
990
- headers.referer = referer
991
- headers.origin = getOriginFromURL(referer)
992
- }
976
+ if ( req.query.streamURLToken ) {
977
+ token = decodeURIComponent(req.query.streamURLToken)
978
+ headers['x-cdn-token'] = token
979
+ }
993
980
 
981
+ var req = function () {
994
982
  requestRetry(u, headers, function(err, response) {
995
983
  if (err) return res.error(err)
996
984
 
@@ -1689,7 +1677,6 @@ app.get('/', async function(req, res) {
1689
1677
  let compareEnd = new Date(big_inning.end)
1690
1678
  compareEnd.setHours(compareEnd.getHours()+1)
1691
1679
  if ( (currentDate >= compareStart) && (currentDate < compareEnd) ) {
1692
- body += '<tr><td><span class="tooltip">Big Inning<span class="tooltiptext">Big Inning is the live look-in and highlights show. <a href="https://support.mlb.com/s/article/What-Is-MLB-Big-Inning">See here for more information</a>.</span></span></td><td>'
1693
1680
  let querystring = '?event=biginning'
1694
1681
  let multiviewquerystring = querystring + '&resolution=' + DEFAULT_MULTIVIEW_RESOLUTION
1695
1682
  if ( linkType == VALID_LINK_TYPES[0] ) {
@@ -1725,8 +1712,8 @@ app.get('/', async function(req, res) {
1725
1712
  compareEnd.setHours(compareEnd.getHours()+4)
1726
1713
  body += '<tr><td><span class="tooltip">' + compareStart.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + ' - ' + compareEnd.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + '<span class="tooltiptext">The game changer stream will automatically switch between the highest leverage active live non-blackout games, and should be available whenever there are such games available. Does not support adaptive bitrate switching, will default to 720p60 resolution if not specified.</span></span></td><td>'
1727
1714
  if ( (currentDate >= compareStart) && (currentDate < compareEnd) ) {
1728
- let streamURL = server + '/gamechanger.m3u8?'
1729
- let multiviewquerystring = streamURL + 'resolution=' + DEFAULT_MULTIVIEW_RESOLUTION + content_protect_b
1715
+ let streamURL = server + '/gamechanger.m3u8'
1716
+ let multiviewquerystring = '/gamechanger.m3u8?resolution=' + DEFAULT_MULTIVIEW_RESOLUTION + content_protect_b
1730
1717
  streamURL += content_protect_a
1731
1718
  if ( resolution != VALID_RESOLUTIONS[0] ) streamURL += 'resolution=' + resolution + '&'
1732
1719
  if ( linkType != VALID_LINK_TYPES[1] ) {
@@ -2214,7 +2201,7 @@ app.get('/', async function(req, res) {
2214
2201
  }
2215
2202
  body += '</p>' + "\n"
2216
2203
 
2217
- body += '<p><span class="tooltip">Skip<span class="tooltiptext">For video streams only (use the video "none" option above to apply it to audio streams): you can remove all breaks, idle time, non-action pitches, or only commercial breaks from the stream (useful to make your own "condensed games").<br/><br/>NOTE: skip timings are only generated when the stream is loaded -- so for live games, it will only skip up to the time you loaded the stream. Also, commercial break skipping will ignore inning start options (it will always start from the beginning).</span></span>: '
2204
+ body += '<p><span class="tooltip">Skip<span class="tooltiptext">For video streams only (use the video "none" option above to apply it to audio streams): you can remove all breaks, idle time, non-action pitches, or only commercial breaks from the stream (useful to make your own "condensed games").<br/><br/>NOTE: skip timings are only generated when the stream is loaded -- so for live games, it will only skip up to the time you loaded the stream.</span></span>: '
2218
2205
  for (var i = 0; i < VALID_SKIP.length; i++) {
2219
2206
  body += '<button '
2220
2207
  if ( skip == VALID_SKIP[i] ) body += 'class="default" '
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mlbserver",
3
- "version": "2025.06.12",
3
+ "version": "2025.06.26",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
package/session.js CHANGED
@@ -3036,10 +3036,10 @@ class sessionClass {
3036
3036
  }
3037
3037
  }
3038
3038
 
3039
- // Get broadcast start timestamp
3040
- async getBroadcastStart(streamURL, streamURLToken) {
3039
+ // Get variant playlist
3040
+ async getVariantPlaylist(streamURL, streamURLToken) {
3041
3041
  try {
3042
- this.debuglog('getBroadcastStart')
3042
+ this.debuglog('getVariantPlaylist')
3043
3043
 
3044
3044
  // MLB version
3045
3045
  let variant = '_5600K'
@@ -3059,6 +3059,19 @@ class sessionClass {
3059
3059
  }
3060
3060
  var response = await this.httpGet(reqObj, false)
3061
3061
  var body = response.replace(/^\s+|\s+$/g, '').split('\n')
3062
+
3063
+ return body
3064
+ } catch(e) {
3065
+ this.log('getVariantPlaylist error : ' + e.message)
3066
+ }
3067
+ }
3068
+
3069
+ // Get broadcast start timestamp
3070
+ async getBroadcastStart(variantPlaylist) {
3071
+ try {
3072
+ this.debuglog('getBroadcastStart')
3073
+
3074
+ var body = variantPlaylist
3062
3075
 
3063
3076
  // check if HLS
3064
3077
  if ( body[0] != '#EXTM3U' ) {
@@ -3103,7 +3116,8 @@ class sessionClass {
3103
3116
  let break_start = 0
3104
3117
 
3105
3118
  // Get the broadcast start time first -- event times will be relative to this
3106
- let broadcast_start_timestamp = await this.getBroadcastStart(streamURL, streamURLToken)
3119
+ let variantPlaylist = await this.getVariantPlaylist(streamURL, streamURLToken)
3120
+ let broadcast_start_timestamp = await this.getBroadcastStart(variantPlaylist)
3107
3121
 
3108
3122
  if ( broadcast_start_timestamp ) {
3109
3123
  this.debuglog('getSkipMarkers broadcast start detected as ' + broadcast_start_timestamp)
@@ -3145,8 +3159,8 @@ class sessionClass {
3145
3159
  // Loop through all plays
3146
3160
  for (var i=0; i < cache_data.liveData.plays.allPlays.length; i++) {
3147
3161
 
3148
- // exit loop after found inning, if not skipping any breaks
3149
- if ((skip_type == 0) && (skip_markers.length == 1)) {
3162
+ // exit loop after found inning, if not skipping any play-defined breaks
3163
+ if ( ((skip_type == 0) || (skip_type == 4)) && (skip_markers.length == 1) ) {
3150
3164
  break
3151
3165
  }
3152
3166
 
@@ -3170,8 +3184,8 @@ class sessionClass {
3170
3184
  event_end_padding = pitch_end_padding
3171
3185
  }
3172
3186
  let action_index
3173
- // skip type 0 (none, inning start) and 1 (breaks) will look at all plays with an endTime
3174
- if ((skip_type <= 1) && cache_data.liveData.plays.allPlays[i].playEvents[j].endTime) {
3187
+ // skip type 0 (none, inning start), 1 (breaks), and 4 (commercials) will look at all plays with an endTime
3188
+ if ( ((skip_type <= 1) || (skip_type == 4)) && cache_data.liveData.plays.allPlays[i].playEvents[j].endTime ) {
3175
3189
  action_index = j
3176
3190
  // skip type 2 (idle time) will look at all non-idle plays with an endTime
3177
3191
  } else if ((skip_type == 2) && cache_data.liveData.plays.allPlays[i].playEvents[j].endTime && (!cache_data.liveData.plays.allPlays[i].playEvents[j].details || !cache_data.liveData.plays.allPlays[i].playEvents[j].details.description || !IDLE_TYPES.some(v => cache_data.liveData.plays.allPlays[i].playEvents[j].details.description.includes(v)))) {
@@ -3217,8 +3231,8 @@ class sessionClass {
3217
3231
  total_skip_time += break_end - break_start
3218
3232
  previous_inning = current_inning
3219
3233
  previous_inning_half = current_inning_half
3220
- // exit loop after found inning, if not skipping breaks
3221
- if (skip_type == 0) {
3234
+ // exit loop after found inning, if not skipping play-defined breaks
3235
+ if ( (skip_type == 0) || (skip_type == 4)) {
3222
3236
  break
3223
3237
  }
3224
3238
  }
@@ -3243,6 +3257,32 @@ class sessionClass {
3243
3257
  }
3244
3258
  }
3245
3259
  }
3260
+
3261
+ // if skipping commercials, look at the variant playlist to detect insertions
3262
+ if ( skip_type == 4 ) {
3263
+ this.debuglog('detecting commercial breaks')
3264
+ let body = variantPlaylist
3265
+ let break_active = false
3266
+ let break_end = 0
3267
+ let time_counter = 0
3268
+ if ( skip_markers.length > 0 ) {
3269
+ break_end = skip_markers[skip_markers.length-1].break_end
3270
+ }
3271
+ for (var i=0; i<body.length; i++) {
3272
+ if ( body[i].startsWith('#EXTINF:') ) {
3273
+ time_counter += parseFloat(body[i].substring(8, body[i].length-1))
3274
+ }
3275
+ if ( (time_counter > break_end) && (break_active == false) && body[i].startsWith('#EXT-OATCLS-SCTE35:') ) {
3276
+ break_active = true
3277
+ break_start = time_counter
3278
+ } else if ( (break_active == true) && body[i].startsWith('#EXT-X-CUE-IN') ) {
3279
+ break_end = time_counter
3280
+ break_active = false
3281
+ skip_markers.push({'break_start': break_start, 'break_end': break_end})
3282
+ total_skip_time += break_end - break_start
3283
+ }
3284
+ }
3285
+ }
3246
3286
 
3247
3287
  this.debuglog('getSkipMarkers found ' + new Date(total_skip_time * 1000).toISOString().substr(11, 8) + ' total skip time')
3248
3288
  }
@@ -3299,13 +3339,13 @@ class sessionClass {
3299
3339
 
3300
3340
  if ( response.data ) {
3301
3341
  for (var i=0; i < response.data.length; i++) {
3302
- if ( response.data[i].airings && (response.data[i].airings.length > 0) && response.data[i].airings[0] && response.data[i].airings[0].accessRights && response.data[i].airings[0].accessRights.live ) {
3303
- let est_date = new Date(response.data[i].airings[0].accessRights.live.startTime).toLocaleString("en-US", {timeZone: 'America/New_York'})
3342
+ if ( response.data[i].airings && (response.data[i].airings.length > 0) && response.data[i].airings[0] && response.data[i].airings[0].accessRightsV2 && response.data[i].airings[0].accessRightsV2.live ) {
3343
+ let est_date = new Date(response.data[i].airings[0].accessRightsV2.live.startTime).toLocaleString("en-US", {timeZone: 'America/New_York'})
3304
3344
  let date_array = est_date.split(',')[0].split('/')
3305
3345
  let this_datestring = date_array[2] + '-' + date_array[0].padStart(2, '0') + '-' + date_array[1].padStart(2, '0')
3306
3346
  this.cache.bigInningSchedule[this_datestring] = {
3307
- start: response.data[i].airings[0].accessRights.live.startTime,
3308
- end: response.data[i].airings[0].accessRights.live.endTime
3347
+ start: response.data[i].airings[0].accessRightsV2.live.startTime,
3348
+ end: response.data[i].airings[0].accessRightsV2.live.endTime
3309
3349
  }
3310
3350
  }
3311
3351
  }