mlbserver 2025.2.27 → 2025.3.2-9.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 +86 -52
  2. package/package.json +1 -1
  3. package/session.js +42 -18
package/index.js CHANGED
@@ -29,10 +29,10 @@ const VALID_CONTROLS = [ 'Show', 'Hide' ]
29
29
  const VALID_INNING_HALF = [ '', 'top', 'bottom' ]
30
30
  const VALID_INNING_NUMBER = [ '', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12' ]
31
31
  const VALID_SCORES = [ 'Hide', 'Show' ]
32
- const VALID_RESOLUTIONS = [ 'adaptive', '1080p60', '720p60', '720p', '540p', '504p', '360p', '288p', '214p', 'none' ]
32
+ const VALID_RESOLUTIONS = [ 'adaptive', '1080p60', '720p60', '720p', '540p', '504p', '360p', '288p', '216p', '180p', 'none' ]
33
33
  const DEFAULT_MULTIVIEW_RESOLUTION = '504p'
34
34
  // Corresponding andwidths to display for above resolutions
35
- const DISPLAY_BANDWIDTHS = [ '', '9600k', '6600k', '4160k', '2950k', '2120k', '1400k', '1000k', '600k', '' ]
35
+ const DISPLAY_BANDWIDTHS = [ '', '9600k', '6600k', '4160k', '2950k', '2120k', '1400k', '1000k', '600k', '300k', '' ]
36
36
  const VALID_AUDIO_TRACKS = [ 'all', 'English', 'Home Radio', 'Casa Radio', 'Away Radio', 'Visita Radio', 'Park', 'none' ]
37
37
  const DISPLAY_AUDIO_TRACKS = [ 'all', 'TV', 'Radio', 'Spa.', 'Away Rad.', 'Away Sp.', 'Park', 'none' ]
38
38
  const DEFAULT_MULTIVIEW_AUDIO_TRACK = 'English'
@@ -56,21 +56,42 @@ const GAMECHANGER_RESOLUTIONS = {
56
56
  'frame_rate': '29.97',
57
57
  'url_bandwidth': '1800',
58
58
  'bandwidth': '2120',
59
- 'codec': '4d001f'
59
+ 'codec': '4d401f'
60
+ },
61
+ '180p': {
62
+ 'resolution': '320x180',
63
+ 'frame_rate': '29.97',
64
+ 'url_bandwidth': '192',
65
+ 'bandwidth': '600',
66
+ 'codec': '4d401f'
67
+ },
68
+ '216p': {
69
+ 'resolution': '384x216',
70
+ 'frame_rate': '29.97',
71
+ 'url_bandwidth': '450',
72
+ 'bandwidth': '600',
73
+ 'codec': '4d401f'
74
+ },
75
+ '288p': {
76
+ 'resolution': '512x288',
77
+ 'frame_rate': '29.97',
78
+ 'url_bandwidth': '800',
79
+ 'bandwidth': '1000',
80
+ 'codec': '4d401f'
60
81
  },
61
82
  '360p': {
62
83
  'resolution': '640x360',
63
84
  'frame_rate': '29.97',
64
85
  'url_bandwidth': '1200',
65
86
  'bandwidth': '1400',
66
- 'codec': '4d001f'
87
+ 'codec': '4d401f'
67
88
  },
68
89
  '540p': {
69
90
  'resolution': '960x540',
70
91
  'frame_rate': '29.97',
71
92
  'url_bandwidth': '2500',
72
93
  'bandwidth': '2950',
73
- 'codec': '4d001f'
94
+ 'codec': '4d401f'
74
95
  },
75
96
  '720p': {
76
97
  'resolution': '1280x720',
@@ -84,7 +105,14 @@ const GAMECHANGER_RESOLUTIONS = {
84
105
  'frame_rate': '59.94',
85
106
  'url_bandwidth': '5600',
86
107
  'bandwidth': '6600',
87
- 'codec': '640028'
108
+ 'codec': '640029'
109
+ },
110
+ '1080p60': {
111
+ 'resolution': '1920x1080',
112
+ 'frame_rate': '59.94',
113
+ 'url_bandwidth': '7500',
114
+ 'bandwidth': '9600',
115
+ 'codec': '64002a'
88
116
  }
89
117
  }
90
118
  const GAMECHANGER_LIST_SIZE = 6
@@ -505,7 +533,7 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
505
533
  return line
506
534
  } else if ( segment_found ) {
507
535
  segment_found = false
508
- return http_root + '/ts?url='+encodeURIComponent(url.resolve(streamURL, line.trim())) + content_protect + referer_parameter + token_parameter
536
+ return http_root + '/segment.ts?url='+encodeURIComponent(url.resolve(streamURL, line.trim())) + content_protect + referer_parameter + token_parameter
509
537
  }
510
538
 
511
539
  // Omit keyframe tracks
@@ -567,7 +595,7 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
567
595
  //var parsed = line.match(/URI="([^"]+)"?$/)
568
596
  var parsed = line.match(',URI="([^"]+)"')
569
597
  if ( parsed[1] ) {
570
- newurl = http_root + '/playlist?url='+encodeURIComponent(url.resolve(streamURL, parsed[1].trim()))
598
+ newurl = http_root + '/playlist.m3u8?url='+encodeURIComponent(url.resolve(streamURL, parsed[1].trim()))
571
599
  if ( force_vod != VALID_FORCE_VOD[0] ) newurl += '&force_vod=on'
572
600
  if ( inning_half != VALID_INNING_HALF[0] ) newurl += '&inning_half=' + inning_half
573
601
  if ( inning_number != VALID_INNING_NUMBER[0] ) newurl += '&inning_number=' + inning_number
@@ -600,7 +628,7 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
600
628
  if ( resolution == VALID_RESOLUTIONS[VALID_RESOLUTIONS.length-1] ) {
601
629
  return
602
630
  } else if ( resolution === VALID_RESOLUTIONS[0] ) {
603
- return line
631
+ return line
604
632
  } else {
605
633
  if ( (video_track_found == false) && (line.indexOf(resolution+',FRAME-RATE='+frame_rate) > 0) ) {
606
634
  video_track_matched = true
@@ -621,7 +649,7 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
621
649
  if ( line.startsWith('#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="eng",URI="') ) {
622
650
  var parsed = line.match(',URI="([^"]+)"')
623
651
  if ( parsed[1] ) {
624
- newurl = http_root + '/playlist?url='+encodeURIComponent(url.resolve(streamURL, parsed[1].trim())) + content_protect + referer_parameter + token_parameter
652
+ newurl = http_root + '/playlist.m3u8?url='+encodeURIComponent(url.resolve(streamURL, parsed[1].trim())) + content_protect + referer_parameter + token_parameter
625
653
  return '#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="eng",URI="' + newurl + '"'
626
654
  }
627
655
  return
@@ -643,7 +671,7 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
643
671
  if ( gamePk ) newurl += '&gamePk=' + gamePk
644
672
  if ( audio_track != VALID_AUDIO_TRACKS[0] ) newurl += '&audio_track=' + encodeURIComponent(audio_track)
645
673
  newurl += content_protect + referer_parameter + token_parameter
646
- return http_root + '/playlist?url='+newurl
674
+ return http_root + '/playlist.m3u8?url='+newurl
647
675
  }
648
676
  })
649
677
  .filter(function(line) {
@@ -666,21 +694,21 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
666
694
 
667
695
 
668
696
  // Listen for playlist requests
669
- app.get('/playlist', async function(req, res) {
697
+ app.get('/playlist.m3u8', async function(req, res) {
670
698
  if ( ! (await protect(req, res)) ) return
671
699
 
672
- session.requestlog('playlist', req, true)
700
+ session.requestlog('playlist.m3u8', req, true)
673
701
 
674
702
  delete req.headers.host
675
703
 
676
704
  var u = req.query.url
677
- session.debuglog('playlist url : ' + u)
705
+ session.debuglog('playlist.m3u8 url : ' + u)
678
706
 
679
707
  var referer = false
680
708
  var referer_parameter = ''
681
709
  if ( req.query.referer ) {
682
710
  referer = decodeURIComponent(req.query.referer)
683
- session.debuglog('found playlist referer : ' + referer)
711
+ session.debuglog('found playlist.m3u8 referer : ' + referer)
684
712
  referer_parameter = '&referer=' + encodeURIComponent(req.query.referer)
685
713
  }
686
714
 
@@ -815,9 +843,9 @@ app.get('/playlist', async function(req, res) {
815
843
 
816
844
  if (line[0] === '#') return line
817
845
 
818
- let newline = http_root + '/ts'
846
+ let newline = http_root + '/segment.ts'
819
847
  if ( line.includes('.vtt') ) {
820
- newline = http_root + '/vtt'
848
+ newline = http_root + '/subtitles.vtt'
821
849
  }
822
850
 
823
851
  newline += '?url='+encodeURIComponent(url.resolve(u, line.trim())) + content_protect + referer_parameter + token_parameter
@@ -825,7 +853,7 @@ app.get('/playlist', async function(req, res) {
825
853
 
826
854
  // if an alternate audio track is specified, force removal of embedded audio
827
855
  if ( (audio_track != VALID_AUDIO_TRACKS[0]) && (audio_track != VALID_AUDIO_TRACKS[1]) ) {
828
- newline = 'download.html?audio_track=' + encodeURIComponent(audio_track) + '&src=' + encodeURIComponent('http://127.0.0.1:' + session.data.port + newline) + content_protect
856
+ newline = http_root + '/download.ts?audio_track=' + encodeURIComponent(audio_track) + '&src=' + encodeURIComponent('http://127.0.0.1:' + session.data.port + newline) + content_protect
829
857
  }
830
858
 
831
859
  return newline
@@ -869,20 +897,20 @@ app.get('/playlist', async function(req, res) {
869
897
  })
870
898
 
871
899
  // Listen for ts requests (video segments) and decode them
872
- app.get('/ts', async function(req, res) {
900
+ app.get('/segment.ts', async function(req, res) {
873
901
  if ( ! (await protect(req, res)) ) return
874
902
 
875
- session.requestlog('ts', req, true)
903
+ session.requestlog('segment.ts', req, true)
876
904
 
877
905
  delete req.headers.host
878
906
 
879
907
  var u = req.query.url
880
- session.debuglog('ts url : ' + u)
908
+ session.debuglog('segment.ts url : ' + u)
881
909
 
882
910
  var headers = {encoding:null}
883
911
 
884
912
  if ( req.query.referer ) {
885
- session.debuglog('found segment referer : ' + req.query.referer)
913
+ session.debuglog('found segment.ts referer : ' + req.query.referer)
886
914
  referer = decodeURIComponent(req.query.referer)
887
915
  headers.referer = referer
888
916
  headers.origin = getOriginFromURL(referer)
@@ -931,20 +959,20 @@ app.get('/ts', async function(req, res) {
931
959
 
932
960
 
933
961
  // Listen for WebVTT subtitle requests
934
- app.get('/vtt', async function(req, res) {
962
+ app.get('/subtitles.vtt', async function(req, res) {
935
963
  if ( ! (await protect(req, res)) ) return
936
964
 
937
- session.requestlog('vtt', req, true)
965
+ session.requestlog('subtitles.vtt', req, true)
938
966
 
939
967
  delete req.headers.host
940
968
 
941
969
  var u = req.query.url
942
- session.debuglog('vtt url : ' + u)
970
+ session.debuglog('subtitles.vtt url : ' + u)
943
971
 
944
972
  var referer = false
945
973
  if ( req.query.referer ) {
946
974
  referer = decodeURIComponent(req.query.referer)
947
- session.debuglog('found vtt referer : ' + referer)
975
+ session.debuglog('found subtitles.vtt referer : ' + referer)
948
976
  }
949
977
 
950
978
  var req = function () {
@@ -1173,7 +1201,7 @@ app.get('/gamechangerplaylist', async function(req, res) {
1173
1201
  if ( session.temp_cache.gamechanger[id].segments[i].discontinuity ) {
1174
1202
  session.temp_cache.gamechanger[id].playlist[resolution] += '#EXT-X-DISCONTINUITY' + '\n'
1175
1203
  }
1176
- session.temp_cache.gamechanger[id].playlist[resolution] += session.temp_cache.gamechanger[id].segments[i].extinf + '\n' + '/ts?url=' + encodeURIComponent(session.temp_cache.gamechanger[id].segments[i].ts) + '&streamURLToken='+encodeURIComponent(session.temp_cache.gamechanger[id].segments[i].streamURLToken) + content_protect + '\n'
1204
+ session.temp_cache.gamechanger[id].playlist[resolution] += session.temp_cache.gamechanger[id].segments[i].extinf + '\n' + '/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'
1177
1205
  }
1178
1206
 
1179
1207
  session.debuglog(game_changer_title + 'playlist ' + session.temp_cache.gamechanger[id].playlist[resolution])
@@ -1523,6 +1551,8 @@ app.get('/', async function(req, res) {
1523
1551
  let link = linkType.toLowerCase() + '.html'
1524
1552
  if ( linkType == VALID_LINK_TYPES[1] ) {
1525
1553
  link = linkType.toLowerCase() + '.m3u8'
1554
+ } else if ( linkType == VALID_LINK_TYPES[4] ) {
1555
+ link = linkType.toLowerCase() + '.ts'
1526
1556
  } else {
1527
1557
  force_vod = VALID_FORCE_VOD[0]
1528
1558
  }
@@ -1574,7 +1604,7 @@ app.get('/', async function(req, res) {
1574
1604
  body += '</td></tr>' + "\n"
1575
1605
  }*/
1576
1606
 
1577
- if ( (gameDate >= today) && cache_data.dates && cache_data.dates[0] && cache_data.dates[0].games && (cache_data.dates[0].games.length > 0) ) {
1607
+ if ( cache_data.dates && cache_data.dates[0] && cache_data.dates[0].games && (cache_data.dates[0].games.length > 0) ) {
1578
1608
  blackouts = await session.get_blackout_games(cache_data.dates[0].date, true)
1579
1609
  }
1580
1610
 
@@ -1686,6 +1716,7 @@ app.get('/', async function(req, res) {
1686
1716
  } else {
1687
1717
  teams += hometeam
1688
1718
  }
1719
+ let filename_teams = awayteam + " @ " + hometeam
1689
1720
  let pitchers = ""
1690
1721
  let state = "<br/>"
1691
1722
 
@@ -1764,7 +1795,7 @@ app.get('/', async function(req, res) {
1764
1795
  state += "<br/>" + detailedState
1765
1796
  }
1766
1797
 
1767
- var filename = gameDate + ' ' + teams + ' '
1798
+ var filename = gameDate + ' ' + filename_teams + ' '
1768
1799
 
1769
1800
  if ( cache_data.dates[0].games[j].doubleHeader != 'N' ) {
1770
1801
  state += "<br/>Game " + cache_data.dates[0].games[j].gameNumber
@@ -1844,7 +1875,7 @@ app.get('/', async function(req, res) {
1844
1875
  body += '><td>' + description + teams + pitchers + state + '</td>'
1845
1876
 
1846
1877
  // Check if Winter League / MiLB game first
1847
- if ( (cache_data.dates[0].games[j].teams['home'].team.sport.id != levels['MLB']) && (mediaType == 'MLBTV') ) {
1878
+ if ( (cache_data.dates[0].games[j].teams['away'].team.sport.id != levels['MLB']) && (cache_data.dates[0].games[j].teams['home'].team.sport.id != levels['MLB']) && (mediaType == 'MLBTV') ) {
1848
1879
  body += "<td>"
1849
1880
  if ( cache_data.dates[0].games[j].broadcasts ) {
1850
1881
  let broadcastName = 'N/A'
@@ -1940,9 +1971,12 @@ app.get('/', async function(req, res) {
1940
1971
 
1941
1972
  // display blackout tooltip, if necessary
1942
1973
  if ( blackouts[gamePk] ) {
1943
- body += '<span class="tooltip"><span class="blackout">' + teamabbr + '</span><span class="tooltiptext">' + blackouts[gamePk].blackout_type + ' video blackout until approx. 2.5 hours after the game'
1944
- if ( blackouts[gamePk].blackoutExpiry ) {
1945
- body += ' (~' + blackouts[gamePk].blackoutExpiry.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + ')'
1974
+ body += '<span class="tooltip"><span class="blackout">' + teamabbr + '</span><span class="tooltiptext">' + blackouts[gamePk].blackout_type
1975
+ if ( blackouts[gamePk].blackout_type != 'Not entitled' ) {
1976
+ body += ' video blackout until approx. 2.5 hours after the game'
1977
+ if ( blackouts[gamePk].blackoutExpiry ) {
1978
+ body += ' (~' + blackouts[gamePk].blackoutExpiry.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + ')'
1979
+ }
1946
1980
  }
1947
1981
  body += '</span></span>'
1948
1982
  } else {
@@ -2059,7 +2093,7 @@ app.get('/', async function(req, res) {
2059
2093
  body += "</table>" + "\n"
2060
2094
 
2061
2095
  if ( (Object.keys(blackouts).length > 0) ) {
2062
- body += '<span class="tooltip tinytext"><span class="blackout">strikethrough</span> indicates a live blackout<span class="tooltiptext">Tap or hover over the team abbreviation to see an estimate of when the blackout will be lifted (officially ~90 minutes, but more likely ~150 minutes or ~2.5 hours after the game ends).</span></span>' + "\n"
2096
+ body += '<span class="tooltip tinytext"><span class="blackout">strikethrough</span> indicates a live blackout or non-entitled video<span class="tooltiptext">Tap or hover over the team abbreviation to see an estimate of when the blackout will be lifted (officially ~90 minutes, but more likely ~150 minutes or ~2.5 hours after the game ends).</span></span>' + "\n"
2063
2097
  if ( argv.free ) {
2064
2098
  body += '<br/>'
2065
2099
  }
@@ -2132,7 +2166,7 @@ app.get('/', async function(req, res) {
2132
2166
  body += '<input type="checkbox" id="reencode"/> <span class="tooltip">Re-encode all audio<span class="tooltiptext">Uses more CPU. Generally only necessary if you need the multiview stream to continue after one of the individual streams has ended. (Any streams with sync adjustments above will automatically be re-encoded, regardless of this setting.)</span></span><br/>' + "\n"
2133
2167
  body += '<input type="checkbox" id="park_audio"/> <span class="tooltip">Park audio: filter out announcers<span class="tooltiptext">Implies re-encoding all audio. If this is enabled, an extra audio filter is applied to remove the announcer voices.</span></span><br/>' + "\n"
2134
2168
  body += '<hr><span class="tooltip">Alternate audio URL and sync<span class="tooltiptext">Optional: you can also include a separate audio-only URL as an additional alternate audio track. Archive games will likely require a very large negative sync value, as the radio broadcasts may not be trimmed like the video archives.</span></span>:<br/><textarea id="audio_url" rows=2 cols=60 oninput="this.value=stream_substitution(this.value)"></textarea><input id="audio_url_seek" type="number" value="0" style="vertical-align:top;font-size:.8em;width:4em"/>'
2135
- body += '<hr>Watch: <a href="' + http_root + '/embed.html?msrc=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Embed</a> | <a href="' + http_root + '/stream.m3u8?src=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Stream</a> | <a href="' + http_root + '/chromecast.html?msrc=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Chromecast</a> | <a href="' + http_root + '/advanced.html?msrc=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Advanced</a> | <a href="' + http_root + '/download.html?src=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '&filename=' + gameDate + ' Multiview">Download</a><br/><span class="tinytext">Kodi STRM files: <a href="' + http_root + '/kodi.strm?src=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Matrix/19+</a> (<a href="' + http_root + '/kodi.strm?version=18&src=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Leia/18</a>)</span>'
2169
+ body += '<hr>Watch: <a href="' + http_root + '/embed.html?msrc=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Embed</a> | <a href="' + http_root + '/stream.m3u8?src=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Stream</a> | <a href="' + http_root + '/chromecast.html?msrc=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Chromecast</a> | <a href="' + http_root + '/advanced.html?msrc=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Advanced</a> | <a href="' + http_root + '/download.ts?src=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '&filename=' + gameDate + ' Multiview">Download</a><br/><span class="tinytext">Kodi STRM files: <a href="' + http_root + '/kodi.strm?src=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Matrix/19+</a> (<a href="' + http_root + '/kodi.strm?version=18&src=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Leia/18</a>)</span>'
2136
2170
  body += '</td></tr></table><br/>' + "\n"
2137
2171
  }
2138
2172
 
@@ -2988,13 +3022,13 @@ app.get('/kodi.strm', async function(req, res) {
2988
3022
  })
2989
3023
 
2990
3024
  // Listen for download requests (either for actual downloads or just proxying stream through ffmpeg)
2991
- app.get('/download.html', async function(req, res) {
3025
+ app.get('/download.ts', async function(req, res) {
2992
3026
  if ( ! (await protect(req, res)) ) return
2993
3027
 
2994
3028
  try {
2995
3029
  // we'll know it's an actual download request if it include a filename parameter
2996
3030
  if ( req.query.filename ) {
2997
- session.requestlog('download', req)
3031
+ session.requestlog('download.ts', req)
2998
3032
  } else {
2999
3033
  session.debuglog('force alternate audio', req)
3000
3034
  }
@@ -3011,7 +3045,7 @@ app.get('/download.html', async function(req, res) {
3011
3045
  }
3012
3046
  video_url = server + video_url
3013
3047
  }
3014
- session.debuglog('download src : ' + video_url)
3048
+ session.debuglog('download.ts src : ' + video_url)
3015
3049
 
3016
3050
  // force adaptive streams to just use a single video resolution/track
3017
3051
  if ( req.query.filename ) {
@@ -3033,7 +3067,7 @@ app.get('/download.html', async function(req, res) {
3033
3067
  // video
3034
3068
  if ( !req.query.resolution || (req.query.resolution != VALID_RESOLUTIONS[VALID_RESOLUTIONS.length-1]) ) {
3035
3069
  // copy first video track if available
3036
- ffmpeg_command.addOutputOption('-map', '0:v:0?')
3070
+ ffmpeg_command.addOutputOption('-map', '0:v:0')
3037
3071
  .addOutputOption('-c:v', 'copy')
3038
3072
  } else {
3039
3073
  // suppress video is "none" resolution was specified
@@ -3058,13 +3092,6 @@ app.get('/download.html', async function(req, res) {
3058
3092
  ffmpeg_command.addOutputOption('-map', '[out0]')
3059
3093
  .addOutputOption('-c:a', 'aac')
3060
3094
  .addOutputOption('-ac:a:0', '1')
3061
-
3062
- // if not downloading to a file, also copy source PTS values for continuous playback
3063
- if ( !req.query.filename ) {
3064
- ffmpeg_command.addOutputOption('-copyts')
3065
- .addOutputOption('-muxpreload', '0')
3066
- .addOutputOption('-muxdelay', '0')
3067
- }
3068
3095
  } else if ( (!req.query.filename && (req.query.audio_track != VALID_AUDIO_TRACKS[1])) || (req.query.audio_track == VALID_AUDIO_TRACKS[7]) ) {
3069
3096
  // if we're not downloading a file, and we requested an alternate audio track, then we want to suppress the embedded TV audio
3070
3097
  // or if the user requested no audio tracks in their download, we will suppress all
@@ -3076,22 +3103,29 @@ app.get('/download.html', async function(req, res) {
3076
3103
  }
3077
3104
  }
3078
3105
 
3106
+ // if not downloading to a file, also copy source PTS values for continuous playback
3107
+ if ( !req.query.filename ) {
3108
+ ffmpeg_command.addOutputOption('-copyts')
3109
+ .addOutputOption('-muxpreload', '0')
3110
+ .addOutputOption('-muxdelay', '0')
3111
+ }
3112
+
3079
3113
  // output mpegts to response stream
3080
3114
  ffmpeg_command.addOutputOption('-f', 'mpegts')
3081
3115
  .output(res)
3082
3116
  .on('start', function(commandLine) {
3083
- session.debuglog('download command started')
3117
+ session.debuglog('download.ts command started')
3084
3118
  if ( argv.debug || argv.ffmpeg_logging ) {
3085
- session.debuglog('download command: ' + commandLine)
3119
+ session.debuglog('download.ts command: ' + commandLine)
3086
3120
  }
3087
3121
  })
3088
3122
  .on('error', function(err, stdout, stderr) {
3089
- session.debuglog('download command stopped: ' + err.message)
3123
+ session.debuglog('download.ts command stopped: ' + err.message)
3090
3124
  if ( stdout ) session.log(stdout)
3091
3125
  if ( stderr ) session.log(stderr)
3092
3126
  })
3093
3127
  .on('end', function() {
3094
- session.debuglog('download command ended')
3128
+ session.debuglog('download.ts command ended')
3095
3129
  })
3096
3130
 
3097
3131
  if ( argv.ffmpeg_logging ) {
@@ -3109,7 +3143,7 @@ app.get('/download.html', async function(req, res) {
3109
3143
 
3110
3144
  ffmpeg_command.run()
3111
3145
  } catch (e) {
3112
- session.log('download request error : ' + e.message)
3146
+ session.log('download.ts request error : ' + e.message)
3113
3147
  res.end('')
3114
3148
  }
3115
- })
3149
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mlbserver",
3
- "version": "2025.02.27",
3
+ "version": "2025.03.29.2",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
package/session.js CHANGED
@@ -38,10 +38,14 @@ const WINTER_LEAGUES = [AFL_ID, LIDOM_ID]
38
38
  const BREAK_TYPES = ['Game Advisory', 'Pitching Substitution', 'Offensive Substitution', 'Defensive Sub', 'Defensive Switch', 'Runner Placed On Base']
39
39
  // These are the events to keep, in addition to the last event of each at-bat, if we're skipping pitches
40
40
  const ACTION_TYPES = ['Wild Pitch', 'Passed Ball', 'Stolen Base', 'Caught Stealing', 'Pickoff', 'Error', 'Out', 'Balk', 'Defensive Indiff', 'Other Advance']
41
+ // These are some idle events to skip
42
+ const IDLE_TYPES = ['Mound Visit', 'Batter Timeout', 'Pitcher Step Off']
41
43
  const EVENT_START_PADDING = -3
42
- const PITCH_END_PADDING = -4
44
+ const PITCH_END_PADDING = 2
43
45
  const ACTION_END_PADDING = 7
44
46
  const MINIMUM_BREAK_DURATION = 5
47
+ // extra padding for MLB events (2025)
48
+ const MLB_PADDING = 39
45
49
 
46
50
  const LI_TABLE = {
47
51
  1: {
@@ -1776,7 +1780,7 @@ class sessionClass {
1776
1780
  if ( ((team.toUpperCase() == home_team) && (LEVELS[level.toUpperCase()] == home_level)) || ((team.toUpperCase() == away_team) && (LEVELS[level.toUpperCase()] == away_level)) || ((team.toUpperCase().indexOf('NATIONAL.') == 0) && (home_level == LEVELS['MLB'])) || ((team.toUpperCase().startsWith('FREE.') && cache_data.dates[0].games[j].broadcasts && cache_data.dates[0].games[j].broadcasts[0] && (cache_data.dates[0].games[j].broadcasts[0].freeGame == true))) ) {
1777
1781
 
1778
1782
  // Check if Winter League / MiLB game first
1779
- if ( (home_level != LEVELS['MLB']) && (mediaType == 'MLBTV') ) {
1783
+ if ( (away_level != LEVELS['MLB']) && (home_level != LEVELS['MLB']) && (mediaType == 'MLBTV') ) {
1780
1784
  this.debuglog('matched non-MLB team for ' + cache_data.dates[0].games[j].teams['home'].team.abbreviation + '@' + cache_data.dates[0].games[j].teams['away'].team.abbreviation)
1781
1785
  if ( cache_data.dates[0].games[j].broadcasts ) {
1782
1786
  let broadcastName = 'N/A'
@@ -2892,8 +2896,14 @@ class sessionClass {
2892
2896
  async getBroadcastStart(streamURL, streamURLToken) {
2893
2897
  try {
2894
2898
  this.debuglog('getBroadcastStart')
2899
+
2900
+ // MLB version
2901
+ let variant = '_5600K'
2902
+ if ( streamURL.includes('milb.com') ) {
2903
+ variant = '_1280x720_59_5472K'
2904
+ }
2895
2905
 
2896
- let variant_url = 'http://localhost:' + this.data.port + '/playlist?url=' + encodeURIComponent(streamURL.substr(0,streamURL.length-5) + '_5600K.m3u8')
2906
+ let variant_url = 'http://localhost:' + this.data.port + '/playlist.m3u8?url=' + encodeURIComponent(streamURL.substr(0,streamURL.length-5) + variant + '.m3u8')
2897
2907
  if ( streamURLToken ) {
2898
2908
  variant_url += '&streamURLToken=' + encodeURIComponent(streamURLToken)
2899
2909
  }
@@ -2929,6 +2939,15 @@ class sessionClass {
2929
2939
  this.debuglog('getSkipMarkers')
2930
2940
 
2931
2941
  if ( skip_adjust != 0 ) this.log('manual adjustment of ' + skip_adjust + ' seconds being applied')
2942
+
2943
+ let event_start_padding = EVENT_START_PADDING
2944
+ let pitch_end_padding = PITCH_END_PADDING
2945
+ let action_end_padding = ACTION_END_PADDING
2946
+ if ( !streamURL.includes('milb.com') ) {
2947
+ event_start_padding += MLB_PADDING
2948
+ pitch_end_padding += MLB_PADDING
2949
+ action_end_padding += MLB_PADDING
2950
+ }
2932
2951
 
2933
2952
  if ( !this.temp_cache[gamePk] ) {
2934
2953
  this.temp_cache[gamePk] = {}
@@ -2993,7 +3012,7 @@ class sessionClass {
2993
3012
  if ((current_inning > start_inning) || ((current_inning == start_inning) && ((current_inning_half == start_inning_half) || (current_inning_half == 'bottom')))) {
2994
3013
  // loop through events within each play
2995
3014
  for (var j=0; j < cache_data.liveData.plays.allPlays[i].playEvents.length; j++) {
2996
- let event_end_padding = ACTION_END_PADDING
3015
+ let event_end_padding = action_end_padding
2997
3016
  // always exclude break types
2998
3017
  if (cache_data.liveData.plays.allPlays[i].playEvents[j].details && cache_data.liveData.plays.allPlays[i].playEvents[j].details.event && BREAK_TYPES.includes(cache_data.liveData.plays.allPlays[i].playEvents[j].details.event)) {
2999
3018
  // if we're in the process of skipping inning breaks, treat the first break type we find as another inning break
@@ -3004,15 +3023,18 @@ class sessionClass {
3004
3023
  continue
3005
3024
  } else {
3006
3025
  if ( (j < (cache_data.liveData.plays.allPlays[i].playEvents.length - 1)) && (!cache_data.liveData.plays.allPlays[i].playEvents[j].details || !cache_data.liveData.plays.allPlays[i].playEvents[j].details.event || !ACTION_TYPES.some(v => cache_data.liveData.plays.allPlays[i].playEvents[j].details.event.includes(v))) ) {
3007
- event_end_padding = PITCH_END_PADDING
3026
+ event_end_padding = pitch_end_padding
3008
3027
  }
3009
3028
  let action_index
3010
- // skip type 1 (breaks) && 2 (idle time) will look at all plays with an endTime
3011
- if ((skip_type <= 2) && cache_data.liveData.plays.allPlays[i].playEvents[j].endTime) {
3029
+ // skip type 1 (breaks) will look at all plays with an endTime
3030
+ if ((skip_type == 1) && cache_data.liveData.plays.allPlays[i].playEvents[j].endTime) {
3031
+ action_index = j
3032
+ // skip type 2 (idle time) will look at all non-idle plays with an endTime
3033
+ } else if ((skip_type == 2) && cache_data.liveData.plays.allPlays[i].playEvents[j].endTime && !IDLE_TYPES.some(v => cache_data.liveData.plays.allPlays[i].playEvents[j].details.description.includes(v))) {
3012
3034
  action_index = j
3013
3035
  } else if (skip_type == 3) {
3014
3036
  // skip type 3 excludes non-action pitches (events that aren't last in the at-bat and don't fall under action types)
3015
- if ( event_end_padding == PITCH_END_PADDING ) {
3037
+ if ( event_end_padding == pitch_end_padding ) {
3016
3038
  continue
3017
3039
  } else {
3018
3040
  // if the action is associated with another play or the event doesn't have an end time, use the previous event instead
@@ -3026,7 +3048,7 @@ class sessionClass {
3026
3048
  if (typeof action_index === 'undefined') {
3027
3049
  continue
3028
3050
  } else {
3029
- let break_end = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[action_index].startTime) - broadcast_start_timestamp) / 1000) + EVENT_START_PADDING
3051
+ let break_end = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[action_index].startTime) - broadcast_start_timestamp) / 1000) + event_start_padding
3030
3052
 
3031
3053
  // attempt to fix erroneous timestamps, like NYY-SEA 2022-08-09, bottom 11
3032
3054
  if ( break_end < break_start ) {
@@ -3583,25 +3605,27 @@ class sessionClass {
3583
3605
  let game = cache_data.results[j]
3584
3606
  let game_pk = game.gamePk
3585
3607
  this.debuglog('get_blackout_games checking game ' + game_pk)
3586
- if ( game.blackedOutVideo ) {
3587
- this.debuglog('get_blackout_games found blackout')
3608
+ if ( game.blackedOutVideo || !game.entitledVideo ) {
3609
+ this.debuglog('get_blackout_games found blackout or non-entitled video')
3588
3610
  let blackout_type = ''
3589
- // local/national blackout label disabled, as all were returning local
3590
- /*if ( game.videoStatusCodes.includes('2') ) {
3611
+ if ( game.videoStatusCodes.includes(2) ) {
3591
3612
  this.debuglog('get_blackout_games found national blackout')
3592
3613
  blackout_type = 'National/International'
3593
- } else {
3614
+ } else if ( game.videoStatusCodes.includes(1) ) {
3594
3615
  this.debuglog('get_blackout_games found local blackout')
3595
3616
  blackout_type = 'Local'
3596
- }*/
3617
+ } else {
3618
+ this.debuglog('get_blackout_games found non-entitled video')
3619
+ blackout_type = 'Not entitled'
3620
+ }
3597
3621
  blackouts[game_pk] = { blackout_type: blackout_type }
3598
- } else if ( !game.entitledVideo && (game.videoStatusCodes[0] == '3') ) {
3622
+ } /*else if ( !game.entitledVideo && (game.videoStatusCodes[0] == '3') ) {
3599
3623
  this.debuglog('get_blackout_games found non-entitled MVPD required blackout')
3600
3624
  blackouts[game_pk] = { blackout_type: '' }
3601
- }
3625
+ }*/
3602
3626
 
3603
3627
  // add blackout expiry, if requested
3604
- if ( blackouts[game_pk] && calculate_expiries && await this.check_game_time(game.gameData) ) {
3628
+ if ( blackouts[game_pk] && (blackouts[game_pk].blackout_type != 'Not entitled') && calculate_expiries && await this.check_game_time(game.gameData) ) {
3605
3629
  this.debuglog('get_blackout_games calculating blackout expiry')
3606
3630
  let date_cache_data = await this.getDayData(gameDate)
3607
3631
  if ( date_cache_data.dates && date_cache_data.dates[0] && date_cache_data.dates[0].games && (date_cache_data.dates[0].games.length > 0) ) {