mlbserver 2024.8.2 → 2024.10.0-8.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 +184 -36
  2. package/package.json +1 -1
  3. package/session.js +229 -79
package/index.js CHANGED
@@ -23,20 +23,21 @@ const sessionClass = require('./session.js')
23
23
  const VALID_DATES = [ 'today', 'yesterday' ]
24
24
  const YESTERDAY_UTC_HOURS = 14 // UTC hours (EST + 4) to change home page default date from yesterday to today
25
25
  const VALID_MEDIA_TYPES = [ 'Video', 'Audio', 'Spanish' ]
26
- const VALID_LINK_TYPES = [ 'Embed', 'Stream', 'Chromecast', 'Advanced' ]
26
+ const VALID_LINK_TYPES = [ 'Embed', 'Stream', 'Chromecast', 'Advanced', 'Download' ]
27
27
  const VALID_START_FROM = [ 'Beginning', 'Live' ]
28
28
  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', '720p60', '720p', '540p', '504p', '360p', 'none' ]
32
+ const VALID_RESOLUTIONS = [ 'adaptive', '720p60', '720p', '540p', '504p', '360p', 'none' ]
33
33
  const DEFAULT_MULTIVIEW_RESOLUTION = '504p'
34
34
  // Corresponding andwidths to display for above resolutions
35
35
  const DISPLAY_BANDWIDTHS = [ '', '6600k', '4160k', '2950k', '2120k', '1400k', '' ]
36
- const VALID_AUDIO_TRACKS = [ 'all', 'English', 'English Radio', 'Radio Española', 'Alternate English', 'Alternate Spanish', 'none' ]
37
- const DISPLAY_AUDIO_TRACKS = [ 'all', 'TV', 'Radio', 'Spanish', 'Alt.', 'Alt. Spanish', 'none' ]
36
+ const VALID_AUDIO_TRACKS = [ 'all', 'English', 'Home Radio', 'Casa Radio', 'Away Radio', 'Visita Radio', 'Park', 'none' ]
37
+ const DISPLAY_AUDIO_TRACKS = [ 'all', 'TV', 'Radio', 'Spa.', 'Away Rad.', 'Away Sp.', 'Park', 'none' ]
38
38
  const DEFAULT_MULTIVIEW_AUDIO_TRACK = 'English'
39
39
  const VALID_SKIP = [ 'off', 'breaks', 'idle time', 'pitches', 'commercials' ]
40
+ const DEFAULT_SKIP_ADJUST = 0
40
41
  const VALID_PAD = [ 'off', 'on' ]
41
42
  const VALID_FORCE_VOD = [ 'off', 'on' ]
42
43
  const VALID_SCAN_MODES = [ 'off', 'on' ]
@@ -221,6 +222,8 @@ app.get('/clearcache', async function(req, res) {
221
222
 
222
223
  session.log('Clearing cache...')
223
224
  session.clear_cache()
225
+ session.log('Clearing session...')
226
+ session.clear_session_data()
224
227
  session = new sessionClass(argv)
225
228
 
226
229
  let server = 'http://' + req.headers.host
@@ -338,10 +341,12 @@ app.get('/stream.m3u8', async function(req, res) {
338
341
  if ( gamePk ) {
339
342
  options.gamePk = gamePk
340
343
 
344
+ let skip_adjust = parseInt(req.query.skip_adjust) || DEFAULT_SKIP_ADJUST
345
+
341
346
  let skip_type = VALID_SKIP.indexOf(options.skip)
342
347
  // for skip other than commercial skip, look up markers
343
348
  if ( skip_type != 4 ) {
344
- await session.getSkipMarkers(gamePk, skip_type, options.inning_number, options.inning_half, streamURL, streamURLToken)
349
+ await session.getSkipMarkers(gamePk, skip_type, options.inning_number, options.inning_half, streamURL, streamURLToken, skip_adjust)
345
350
  }
346
351
  }
347
352
  }
@@ -455,6 +460,7 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
455
460
  let inning_half = options.inning_half || VALID_INNING_HALF[0]
456
461
  let inning_number = options.inning_number || VALID_INNING_NUMBER[0]
457
462
  let skip = options.skip || VALID_SKIP[0]
463
+ let skip_adjust = options.skip_adjust || DEFAULT_SKIP_ADJUST
458
464
  let pad = options.pad || VALID_PAD[0]
459
465
  let gamePk = options.gamePk || false
460
466
 
@@ -530,19 +536,20 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
530
536
  // we'll append to this and output track(s) at the end of this code block
531
537
  let audio_output = ''
532
538
 
533
- // default TV audio
539
+ // default TV audio, or park sounds
534
540
  if ( !line.includes(',URI=') ) {
535
- if ( audio_track == VALID_AUDIO_TRACKS[1] ) {
541
+ if ( (audio_track == VALID_AUDIO_TRACKS[1]) || (audio_track == VALID_AUDIO_TRACKS[6]) ) {
536
542
  return line
537
543
  } else if ( audio_track == VALID_AUDIO_TRACKS[0] ) {
538
544
  audio_output += line
539
545
  }
540
546
  } else {
541
- if ( (audio_track == VALID_AUDIO_TRACKS[0]) || (audio_track == VALID_AUDIO_TRACKS[2]) || (audio_track == VALID_AUDIO_TRACKS[3]) ) {
542
- // if user specified home radio or home Spanish audio track, check if this one matches
543
- if ( (audio_track == VALID_AUDIO_TRACKS[2]) || (audio_track == VALID_AUDIO_TRACKS[3]) ) {
547
+ if ( (audio_track == VALID_AUDIO_TRACKS[0]) || (audio_track == VALID_AUDIO_TRACKS[2]) || (audio_track == VALID_AUDIO_TRACKS[3]) || (audio_track == VALID_AUDIO_TRACKS[4]) || (audio_track == VALID_AUDIO_TRACKS[5]) ) {
548
+ // if user specified home/away radio or home/away Spanish audio track, check if this one matches
549
+ if ( (audio_track == VALID_AUDIO_TRACKS[2]) || (audio_track == VALID_AUDIO_TRACKS[3]) || (audio_track == VALID_AUDIO_TRACKS[4]) || (audio_track == VALID_AUDIO_TRACKS[5]) ) {
544
550
  if ( line.includes('NAME="'+audio_track+'"') || line.includes('NAME="'+audio_track.substring(0,audio_track.length-1)+'"') ) {
545
551
  audio_track_matched = true
552
+ line = line.replace('DEFAULT=NO','DEFAULT=YES')
546
553
  line = line.replace('AUTOSELECT=NO','AUTOSELECT=YES')
547
554
  if ( !line.includes(',DEFAULT=YES') ) line = line.replace('AUTOSELECT=YES','AUTOSELECT=YES,DEFAULT=YES')
548
555
  } else {
@@ -559,6 +566,7 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
559
566
  if ( inning_half != VALID_INNING_HALF[0] ) newurl += '&inning_half=' + inning_half
560
567
  if ( inning_number != VALID_INNING_NUMBER[0] ) newurl += '&inning_number=' + inning_number
561
568
  if ( skip != VALID_SKIP[0] ) newurl += '&skip=' + skip
569
+ if ( skip_adjust != DEFAULT_SKIP_ADJUST ) newurl += '&skip_adjust=' + skip_adjust
562
570
  if ( pad != VALID_PAD[0] ) newurl += '&pad=' + pad
563
571
  if ( gamePk ) newurl += '&gamePk=' + gamePk
564
572
  newurl += content_protect + referer_parameter + token_parameter
@@ -626,8 +634,10 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
626
634
  if ( inning_half != VALID_INNING_HALF[0] ) newurl += '&inning_half=' + inning_half
627
635
  if ( inning_number != VALID_INNING_NUMBER[0] ) newurl += '&inning_number=' + inning_number
628
636
  if ( skip != VALID_SKIP[0] ) newurl += '&skip=' + skip
637
+ if ( skip_adjust != DEFAULT_SKIP_ADJUST ) newurl += '&skip_adjust=' + skip_adjust
629
638
  if ( pad != VALID_PAD[0] ) newurl += '&pad=' + pad
630
639
  if ( gamePk ) newurl += '&gamePk=' + gamePk
640
+ if ( audio_track == VALID_AUDIO_TRACKS[6] ) newurl += '&park_audio=true'
631
641
  newurl += content_protect + referer_parameter + token_parameter
632
642
  return '/playlist?url='+newurl
633
643
  }
@@ -681,8 +691,10 @@ app.get('/playlist', async function(req, res) {
681
691
  var inning_half = req.query.inning_half || VALID_INNING_HALF[0]
682
692
  var inning_number = req.query.inning_number || VALID_INNING_NUMBER[0]
683
693
  var skip = req.query.skip || VALID_SKIP[0]
694
+ var skip_adjust = req.query.skip_adjust || DEFAULT_SKIP_ADJUST
684
695
  var pad = req.query.pad || VALID_PAD[0]
685
696
  var gamePk = req.query.gamePk || false
697
+ var park_audio = req.query.park_audio || false
686
698
 
687
699
  var req = function () {
688
700
  var headers = {}
@@ -807,6 +819,11 @@ app.get('/playlist', async function(req, res) {
807
819
  newline += '?url='+encodeURIComponent(url.resolve(u, line.trim())) + content_protect + referer_parameter + token_parameter
808
820
  if ( key ) newline += '&key='+encodeURIComponent(key) + '&iv='+encodeURIComponent(iv)
809
821
 
822
+ // park audio
823
+ if ( park_audio ) {
824
+ newline = 'download.html?park_audio=true&src=' + encodeURIComponent('http://127.0.0.1:' + session.data.port + newline) + content_protect
825
+ }
826
+
810
827
  return newline
811
828
  })
812
829
  .filter(function(line) {
@@ -1045,10 +1062,10 @@ app.get('/gamechangerplaylist', async function(req, res) {
1045
1062
  }
1046
1063
  session.temp_cache.gamechanger[id].lastAccess = gamechangerAccess
1047
1064
 
1048
- if ( !session.temp_cache.gamechanger.start || (gamechangerAccess < session.temp_cache.gamechanger.start) || !session.temp_cache.gamechanger.end || (gamechangerAccess > session.temp_cache.gamechanger.end) ) {
1065
+ /*if ( !session.temp_cache.gamechanger.start || (gamechangerAccess < session.temp_cache.gamechanger.start) || !session.temp_cache.gamechanger.end || (gamechangerAccess > session.temp_cache.gamechanger.end) ) {
1049
1066
  session.log(game_changer_title + 'outside of games starting/ending, skipping')
1050
1067
  respond(GAMECHANGER_RESPONSE_HEADERS, res, Buffer.from(''))
1051
- } else {
1068
+ } else {*/
1052
1069
  let streamURL
1053
1070
  let streamURLToken
1054
1071
  let discontinuity = false
@@ -1164,7 +1181,7 @@ app.get('/gamechangerplaylist', async function(req, res) {
1164
1181
  respond(GAMECHANGER_RESPONSE_HEADERS, res, Buffer.from(session.temp_cache.gamechanger[id].playlist[resolution]))
1165
1182
  }
1166
1183
  }
1167
- }
1184
+ //}
1168
1185
  }
1169
1186
 
1170
1187
  return req()
@@ -1329,6 +1346,10 @@ app.get('/', async function(req, res) {
1329
1346
  if ( req.query.skip ) {
1330
1347
  skip = req.query.skip
1331
1348
  }
1349
+ var skip_adjust = DEFAULT_SKIP_ADJUST
1350
+ if ( req.query.skip_adjust ) {
1351
+ skip_adjust = req.query.skip_adjust
1352
+ }
1332
1353
  var pad = VALID_PAD[0]
1333
1354
  if ( req.query.pad ) {
1334
1355
  pad = req.query.pad
@@ -1361,16 +1382,16 @@ app.get('/', async function(req, res) {
1361
1382
  body += '</style><script type="text/javascript">' + "\n";
1362
1383
 
1363
1384
  // Define option variables in page
1364
- body += 'var date="' + gameDate + '";var level="' + level + '";var org="' + org + '";var mediaType="' + mediaType + '";var resolution="' + resolution + '";var audio_track="' + audio_track + '";var force_vod="' + force_vod + '";var inning_half="' + inning_half + '";var inning_number="' + inning_number + '";var skip="' + skip + '";var pad="' + pad + '";var linkType="' + linkType + '";var startFrom="' + startFrom + '";var scores="' + scores + '";var controls="' + controls + '";var scan_mode="' + scan_mode + '";var content_protect="' + content_protect + '";' + "\n"
1385
+ body += 'var date="' + gameDate + '";var level="' + level + '";var org="' + org + '";var mediaType="' + mediaType + '";var resolution="' + resolution + '";var audio_track="' + audio_track + '";var force_vod="' + force_vod + '";var inning_half="' + inning_half + '";var inning_number="' + inning_number + '";var skip="' + skip + '";var skip_adjust="' + skip_adjust + '";var pad="' + pad + '";var linkType="' + linkType + '";var startFrom="' + startFrom + '";var scores="' + scores + '";var controls="' + controls + '";var scan_mode="' + scan_mode + '";var content_protect="' + content_protect + '";' + "\n"
1365
1386
 
1366
1387
  // Reload function, called after options change
1367
- body += 'var defaultDate="' + today + '";var curDate=new Date();var utcHours=curDate.getUTCHours();if ((utcHours >= ' + todayUTCHours + ') && (utcHours < ' + YESTERDAY_UTC_HOURS + ')){defaultDate="' + yesterday + '"}function reload(){var newurl="/?";if (date != defaultDate){var urldate=date;if (date == "' + today + '"){urldate="today"}else if (date == "' + yesterday + '"){urldate="yesterday"}newurl+="date="+urldate+"&"}if (level != "' + default_level + '"){newurl+="level="+encodeURIComponent(level)+"&"}if (org != "All"){newurl+="org="+encodeURIComponent(org)+"&"}if (mediaType != "' + VALID_MEDIA_TYPES[0] + '"){newurl+="mediaType="+mediaType+"&"}if (mediaType=="Video"){if (resolution != "' + VALID_RESOLUTIONS[0] + '"){newurl+="resolution="+resolution+"&"}if (audio_track != "' + VALID_AUDIO_TRACKS[0] + '"){newurl+="audio_track="+encodeURIComponent(audio_track)+"&"}else if (resolution == "none"){newurl+="audio_track="+encodeURIComponent("' + VALID_AUDIO_TRACKS[2] + '")+"&"}if (inning_half != "' + VALID_INNING_HALF[0] + '"){newurl+="inning_half="+inning_half+"&"}if (inning_number != "' + VALID_INNING_NUMBER[0] + '"){newurl+="inning_number="+inning_number+"&"}if (skip != "' + VALID_SKIP[0] + '"){newurl+="skip="+skip+"&";}}if (pad != "' + VALID_PAD[0] + '"){newurl+="pad="+pad+"&";}if (linkType != "' + VALID_LINK_TYPES[0] + '"){newurl+="linkType="+linkType+"&"}if (linkType=="' + VALID_LINK_TYPES[0] + '"){if (startFrom != "' + VALID_START_FROM[0] + '"){newurl+="startFrom="+startFrom+"&"}if (controls != "' + VALID_CONTROLS[0] + '"){newurl+="controls="+controls+"&"}}if (linkType=="Stream"){if (force_vod != "' + VALID_FORCE_VOD[0] + '"){newurl+="force_vod="+force_vod+"&"}}if (scores != "' + VALID_SCORES[0] + '"){newurl+="scores="+scores+"&"}if (scan_mode != "' + session.data.scan_mode + '"){newurl+="scan_mode="+scan_mode+"&"}if (content_protect != ""){newurl+="content_protect="+content_protect+"&"}window.location=newurl.substring(0,newurl.length-1)}' + "\n"
1388
+ body += 'var defaultDate="' + today + '";var curDate=new Date();var utcHours=curDate.getUTCHours();if ((utcHours >= ' + todayUTCHours + ') && (utcHours < ' + YESTERDAY_UTC_HOURS + ')){defaultDate="' + yesterday + '"}function reload(){var newurl="/?";if (date != defaultDate){var urldate=date;if (date == "' + today + '"){urldate="today"}else if (date == "' + yesterday + '"){urldate="yesterday"}newurl+="date="+urldate+"&"}if (level != "' + default_level + '"){newurl+="level="+encodeURIComponent(level)+"&"}if (org != "All"){newurl+="org="+encodeURIComponent(org)+"&"}if (mediaType != "' + VALID_MEDIA_TYPES[0] + '"){newurl+="mediaType="+mediaType+"&"}if (mediaType=="Video"){if (resolution != "' + VALID_RESOLUTIONS[0] + '"){newurl+="resolution="+resolution+"&"}if (audio_track != "' + VALID_AUDIO_TRACKS[0] + '"){newurl+="audio_track="+encodeURIComponent(audio_track)+"&"}else if (resolution == "none"){newurl+="audio_track="+encodeURIComponent("' + VALID_AUDIO_TRACKS[2] + '")+"&"}if (inning_half != "' + VALID_INNING_HALF[0] + '"){newurl+="inning_half="+inning_half+"&"}if (inning_number != "' + VALID_INNING_NUMBER[0] + '"){newurl+="inning_number="+inning_number+"&"}if (skip != "' + VALID_SKIP[0] + '"){newurl+="skip="+skip+"&";if (skip_adjust != "' + DEFAULT_SKIP_ADJUST + '"){newurl+="skip_adjust="+skip_adjust+"&"}}}if (pad != "' + VALID_PAD[0] + '"){newurl+="pad="+pad+"&";}if (linkType != "' + VALID_LINK_TYPES[0] + '"){newurl+="linkType="+linkType+"&"}if (linkType=="' + VALID_LINK_TYPES[0] + '"){if (startFrom != "' + VALID_START_FROM[0] + '"){newurl+="startFrom="+startFrom+"&"}if (controls != "' + VALID_CONTROLS[0] + '"){newurl+="controls="+controls+"&"}}if (linkType=="Stream"){if (force_vod != "' + VALID_FORCE_VOD[0] + '"){newurl+="force_vod="+force_vod+"&"}}if (scores != "' + VALID_SCORES[0] + '"){newurl+="scores="+scores+"&"}if (scan_mode != "' + session.data.scan_mode + '"){newurl+="scan_mode="+scan_mode+"&"}if (content_protect != ""){newurl+="content_protect="+content_protect+"&"}window.location=newurl.substring(0,newurl.length-1)}' + "\n"
1368
1389
 
1369
1390
  // Ajax function for multiview and highlights
1370
1391
  body += 'function makeGETRequest(url, callback){var request=new XMLHttpRequest();request.onreadystatechange=function(){if (request.readyState==4 && request.status==200){callback(request.responseText)}};request.open("GET", url);request.send();}' + "\n"
1371
1392
 
1372
1393
  // Multiview functions
1373
- body += 'var excludeTeams=[];function parsemultiviewresponse(responsetext){if (responsetext == "started"){setTimeout(function(){document.getElementById("startmultiview").innerHTML="Restart";document.getElementById("stopmultiview").innerHTML="Stop"},15000)}else if (responsetext == "stopped"){setTimeout(function(){document.getElementById("stopmultiview").innerHTML="Stopped";document.getElementById("startmultiview").innerHTML="Start"},3000)}else{alert(responsetext)}}function addmultiview(e, teams=[], excludes=[]){var newvalue=e.value;for(var i=1;i<=4;i++){var valuefound = false;var oldvalue="";if(!e.checked){oldvalue=e.value;newvalue=""}if ((document.getElementById("multiview" + i).value == oldvalue) || ((oldvalue != "") && (document.getElementById("multiview" + i).value.startsWith(oldvalue)))){if ((newvalue != "") && (excludes.length > 0)){newvalue+="&excludeTeams="+excludeTeams.toString()}document.getElementById("multiview" + i).value=newvalue;valuefound=true;break}}if(e.checked && !valuefound){e.checked=false}for(var i=0;i<teams.length;i++){if(e.checked){excludeTeams.push(teams[i])}else{var index=excludeTeams.indexOf(teams[i]);if (index !== -1){excludeTeams.splice(index,1)}}}}function startmultiview(e){var count=0;var getstr="";for(var i=1;i<=4;i++){if (document.getElementById("multiview"+i).value != ""){count++;getstr+="streams="+encodeURIComponent(document.getElementById("multiview"+i).value)+"&sync="+encodeURIComponent(document.getElementById("sync"+i).value)+"' + content_protect_b + '"+"&"}}if((count >= 1) && (count <= 4)){if (document.getElementById("faster").checked){getstr+="faster=true&dvr=true&"}else if (document.getElementById("dvr").checked){getstr+="dvr=true&"}if (document.getElementById("reencode").checked){getstr+="reencode=true&"}if (document.getElementById("park_audio").checked){getstr+="park_audio=true&"}if (document.getElementById("audio_url").value != ""){getstr+="audio_url="+encodeURIComponent(document.getElementById("audio_url").value)+"&";if (document.getElementById("audio_url_seek").value != "0"){getstr+="audio_url_seek="+encodeURIComponent(document.getElementById("audio_url_seek").value)}}e.innerHTML="starting...";makeGETRequest("/multiview?"+getstr, parsemultiviewresponse)}else{alert("Multiview requires between 1-4 streams to be selected")}return false}function stopmultiview(e){e.innerHTML="stopping...";makeGETRequest("/multiview", parsemultiviewresponse);return false}' + "\n"
1394
+ body += 'var excludeTeams=[];function parsemultiviewresponse(responsetext){if (responsetext == "started"){setTimeout(function(){document.getElementById("startmultiview").innerHTML="Restart";document.getElementById("stopmultiview").innerHTML="Stop"},15000)}else if (responsetext == "stopped"){setTimeout(function(){document.getElementById("stopmultiview").innerHTML="Stopped";document.getElementById("startmultiview").innerHTML="Start"},3000)}else{alert(responsetext)}}function addmultiview(e, teams=[], excludes=[]){var newvalue=e.value;for(var i=1;i<=4;i++){var valuefound = false;var oldvalue="";if(!e.checked){oldvalue=e.value;newvalue=""}if ((document.getElementById("multiview" + i).value == oldvalue) || ((oldvalue != "") && (document.getElementById("multiview" + i).value.startsWith(oldvalue)))){if ((newvalue != "") && (excludes.length > 0)){newvalue+="&excludeTeams="+excludeTeams.toString()}document.getElementById("multiview" + i).value=newvalue;valuefound=true;break}}if(e.checked && !valuefound){e.checked=false}for(var i=0;i<teams.length;i++){if(e.checked){excludeTeams.push(teams[i])}else{var index=excludeTeams.indexOf(teams[i]);if (index !== -1){excludeTeams.splice(index,1)}}}}function startmultiview(e){var count=0;var getstr="";for(var i=1;i<=4;i++){if (document.getElementById("multiview"+i).value != ""){count++;getstr+="streams="+encodeURIComponent(document.getElementById("multiview"+i).value)+"&sync="+encodeURIComponent(document.getElementById("sync"+i).value)+"' + content_protect_b + '"+"&"}}if((count >= 1) && (count <= 4)){if (document.getElementById("faster").checked){getstr+="faster=true&dvr=true&"}else if (document.getElementById("dvr").checked){getstr+="dvr=true&"}if (document.getElementById("reencode").checked){getstr+="reencode=true&"}if (document.getElementById("park_audio").checked){getstr+="park_audio=true&"}if (document.getElementById("audio_url").value != ""){getstr+="audio_url="+encodeURIComponent(document.getElementById("audio_url").value)+"&";if (document.getElementById("audio_url_seek").value != "0"){getstr+="audio_url_seek="+encodeURIComponent(document.getElementById("audio_url_seek").value)}}e.innerHTML="starting...";makeGETRequest("/multiview?"+getstr, parsemultiviewresponse)}else{alert("Multiview requires between 1-4 streams to be selected")}return false}function stopmultiview(e){e.innerHTML="stopping...";makeGETRequest("/multiview' + content_protect_a + '", parsemultiviewresponse);return false}' + "\n"
1374
1395
 
1375
1396
  // Function to switch URLs to stream URLs, where necessary
1376
1397
  body += 'function stream_substitution(url){return url.replace(/\\/([a-zA-Z]+\.html)/,"/stream.m3u8")}' + "\n"
@@ -1417,7 +1438,7 @@ app.get('/', async function(req, res) {
1417
1438
  }
1418
1439
  body += '</p>' + "\n"
1419
1440
 
1420
- body += '<p><span class="tooltip">Link Type<span class="tooltiptext">Embed will play in your browser (with AirPlay support), Stream will give you a stream URL to open directly in media players like Kodi or VLC, Chromecast is a desktop browser-based casting site, and Advanced will play in your desktop browser with some extra tools and debugging information (Advanced may require you to disable insecure / mixed content blocking in your browser).<br><br>NOTE: Chromecast may not be able to resolve local domain names; if so, you can simply access this page (and thus the streams) using an IP address instead.</span></span>: '
1441
+ body += '<p><span class="tooltip">Link Type<span class="tooltiptext">Embed will play in your browser (with AirPlay support), Stream will give you a stream URL to open directly in media players like Kodi or VLC, Chromecast is a desktop browser-based casting site, Advanced will play in your desktop browser with some extra tools and debugging information (Advanced may require you to disable insecure / mixed content blocking in your browser), and Download will prompt your browser to save the stream to a TS (Transport Stream) file.<br><br>NOTE: Chromecast may not be able to resolve local domain names; if so, you can simply access this page (and thus the streams) using an IP address instead.</span></span>: '
1421
1442
  for (var i = 0; i < VALID_LINK_TYPES.length; i++) {
1422
1443
  body += '<button '
1423
1444
  if ( linkType == VALID_LINK_TYPES[i] ) body += 'class="default" '
@@ -1484,7 +1505,7 @@ app.get('/', async function(req, res) {
1484
1505
  if ( mediaType == VALID_MEDIA_TYPES[0] ) {
1485
1506
  mediaType = 'MLBTV'
1486
1507
  } else if ( mediaType == VALID_MEDIA_TYPES[2] ) {
1487
- mediaType = VALID_MEDIA_TYPES[1]
1508
+ //mediaType = VALID_MEDIA_TYPES[1]
1488
1509
  language = 'es'
1489
1510
  }
1490
1511
  if ( mediaType == VALID_MEDIA_TYPES[1] ) {
@@ -1518,11 +1539,13 @@ app.get('/', async function(req, res) {
1518
1539
  if ( resolution != VALID_RESOLUTIONS[0] ) querystring += '&resolution=' + resolution
1519
1540
  if ( linkType == VALID_LINK_TYPES[1] ) {
1520
1541
  if ( force_vod != VALID_FORCE_VOD[0] ) querystring += '&force_vod=' + force_vod
1542
+ } else if ( linkType == VALID_LINK_TYPES[4] ) {
1543
+ querystring += '&filename=' + gameDate + ' MLB Network'
1521
1544
  }
1522
1545
  querystring += content_protect_b
1523
1546
  multiviewquerystring += content_protect_b
1524
1547
  body += '<a href="' + thislink + querystring + '">MLB Network</a>'
1525
- body += '<input type="checkbox" value="' + server + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)">'
1548
+ body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)">'
1526
1549
  body += '</td></tr>' + "\n"
1527
1550
  } // end entitlements check
1528
1551
  } // end country check
@@ -1546,7 +1569,7 @@ app.get('/', async function(req, res) {
1546
1569
  }*/
1547
1570
 
1548
1571
  if ( (gameDate >= today) && cache_data.dates && cache_data.dates[0] && cache_data.dates[0].games && (cache_data.dates[0].games.length > 0) ) {
1549
- blackouts = await session.get_blackout_games(cache_data.dates[0].games, true)
1572
+ blackouts = await session.get_blackout_games(cache_data.dates[0].date, true)
1550
1573
  }
1551
1574
 
1552
1575
  // Big Inning
@@ -1574,11 +1597,13 @@ app.get('/', async function(req, res) {
1574
1597
  if ( resolution != VALID_RESOLUTIONS[0] ) querystring += '&resolution=' + resolution
1575
1598
  if ( linkType == VALID_LINK_TYPES[1] ) {
1576
1599
  if ( force_vod != VALID_FORCE_VOD[0] ) querystring += '&force_vod=' + force_vod
1600
+ } else if ( linkType == VALID_LINK_TYPES[4] ) {
1601
+ querystring += '&filename=' + gameDate + ' Big Inning'
1577
1602
  }
1578
1603
  querystring += content_protect_b
1579
1604
  multiviewquerystring += content_protect_b
1580
1605
  body += '<a href="' + thislink + querystring + '">Big Inning</a>'
1581
- body += '<input type="checkbox" value="' + server + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)">'
1606
+ body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)">'
1582
1607
  } else {
1583
1608
  body += 'Big Inning'
1584
1609
  }
@@ -1605,8 +1630,11 @@ app.get('/', async function(req, res) {
1605
1630
  if ( linkType != VALID_LINK_TYPES[1] ) {
1606
1631
  streamURL = thislink + '?src=' + encodeURIComponent(streamURL) + '&startFrom=' + VALID_START_FROM[1] + content_protect_b
1607
1632
  }
1633
+ if ( linkType == VALID_LINK_TYPES[4] ) {
1634
+ streamURL += '&filename=' + gameDate + ' Game Changer'
1635
+ }
1608
1636
  body += '<a href="' + streamURL + '">Game Changer</a>'
1609
- body += '<input type="checkbox" value="' + multiviewquerystring + '" onclick="addmultiview(this, [], excludeTeams)">'
1637
+ body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + multiviewquerystring + '" onclick="addmultiview(this, [], excludeTeams)">'
1610
1638
  } else {
1611
1639
  body += 'Game Changer'
1612
1640
  }
@@ -1624,14 +1652,18 @@ app.get('/', async function(req, res) {
1624
1652
  let awayteam = cache_data.dates[0].games[j].teams['away'].team.abbreviation
1625
1653
  let awayteam_abbr
1626
1654
  if ( cache_data.dates[0].games[j].teams['away'].team.sport.name != 'Major League Baseball' ) {
1627
- awayteam = cache_data.dates[0].games[j].teams['away'].team.shortName + ' (' + session.getParent(cache_data.dates[0].games[j].teams['away'].team.parentOrgName) + ')'
1655
+ awayteam = cache_data.dates[0].games[j].teams['away'].team.shortName
1656
+ let parentOrgName = cache_data.dates[0].games[j].teams['away'].team.parentOrgName
1657
+ if (parentOrgName != 'Office of the Commissioner') awayteam += ' (' + session.getParent(parentOrgName) + ')'
1628
1658
  awayteam_abbr = cache_data.dates[0].games[j].teams['away'].team.abbreviation
1629
1659
  awayteam_level = session.getLevelNameFromSportId(cache_data.dates[0].games[j].teams['away'].team.sport.id)
1630
1660
  }
1631
1661
  let hometeam = cache_data.dates[0].games[j].teams['home'].team.abbreviation
1632
1662
  let hometeam_abbr
1633
1663
  if ( cache_data.dates[0].games[j].teams['home'].team.sport.name != 'Major League Baseball' ) {
1634
- hometeam = cache_data.dates[0].games[j].teams['home'].team.shortName + ' (' + session.getParent(cache_data.dates[0].games[j].teams['home'].team.parentOrgName) + ')'
1664
+ hometeam = cache_data.dates[0].games[j].teams['home'].team.shortName
1665
+ let parentOrgName = cache_data.dates[0].games[j].teams['home'].team.parentOrgName
1666
+ if (parentOrgName != 'Office of the Commissioner') hometeam += ' (' + session.getParent(parentOrgName) + ')'
1635
1667
  hometeam_abbr = cache_data.dates[0].games[j].teams['home'].team.abbreviation
1636
1668
  hometeam_level = session.getLevelNameFromSportId(cache_data.dates[0].games[j].teams['home'].team.sport.id)
1637
1669
  }
@@ -1726,8 +1758,11 @@ app.get('/', async function(req, res) {
1726
1758
  state += "<br/>" + detailedState
1727
1759
  }
1728
1760
 
1761
+ var filename = gameDate + ' ' + teams + ' '
1762
+
1729
1763
  if ( cache_data.dates[0].games[j].doubleHeader != 'N' ) {
1730
1764
  state += "<br/>Game " + cache_data.dates[0].games[j].gameNumber
1765
+ filename += 'Game ' + cache_data.dates[0].games[j].gameNumber + ' '
1731
1766
  }
1732
1767
  if ( cache_data.dates[0].games[j].description ) {
1733
1768
  state += "<br/>" + cache_data.dates[0].games[j].description
@@ -1824,7 +1859,11 @@ app.get('/', async function(req, res) {
1824
1859
  startTime.setMinutes(startTime.getMinutes()-30)
1825
1860
  if ( (currentTime >= startTime) ) {
1826
1861
  let querystring
1827
- querystring = '?gamePk=' + gamePk
1862
+ if ( cache_data.dates[0].games[j].teams['home'].team.league.id == session.getLidomId() ) {
1863
+ querystring = '?event=' + encodeURIComponent(cache_data.dates[0].games[j].teams['home'].team.clubName.toUpperCase())
1864
+ } else {
1865
+ querystring = '?gamePk=' + gamePk
1866
+ }
1828
1867
  let multiviewquerystring = querystring + '&resolution=' + DEFAULT_MULTIVIEW_RESOLUTION
1829
1868
  if ( resolution != VALID_RESOLUTIONS[0] ) querystring += '&resolution=' + resolution
1830
1869
  if ( linkType == VALID_LINK_TYPES[0] ) {
@@ -1835,7 +1874,7 @@ app.get('/', async function(req, res) {
1835
1874
  if ( inning_half != VALID_INNING_HALF[0] ) querystring += '&inning_half=' + inning_half
1836
1875
  if ( inning_number != VALID_INNING_NUMBER[0] ) querystring += '&inning_number=' + relative_inning
1837
1876
  if ( skip != VALID_SKIP[0] ) querystring += '&skip=' + skip
1838
- //if ( skip_adjust != DEFAULT_SKIP_ADJUST ) querystring += '&skip_adjust=' + skip_adjust
1877
+ if ( skip_adjust != DEFAULT_SKIP_ADJUST ) querystring += '&skip_adjust=' + skip_adjust
1839
1878
  }
1840
1879
  if ( pad != VALID_PAD[0] ) querystring += '&pad=' + pad
1841
1880
  if ( linkType == VALID_LINK_TYPES[1] ) {
@@ -1844,11 +1883,13 @@ app.get('/', async function(req, res) {
1844
1883
  if ( currentTime < endTime ) {
1845
1884
  if ( force_vod != VALID_FORCE_VOD[0] ) querystring += '&force_vod=' + force_vod
1846
1885
  }
1886
+ } else if ( linkType == VALID_LINK_TYPES[4] ) {
1887
+ querystring += '&filename=' + filename + broadcastName
1847
1888
  }
1848
1889
  querystring += content_protect_b
1849
1890
  multiviewquerystring += content_protect_b
1850
1891
  body += '<a href="' + thislink + querystring + '">' + broadcastName + '</a>'
1851
- body += '<input type="checkbox" value="' + server + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)">'
1892
+ body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)">'
1852
1893
  } else {
1853
1894
  body += broadcastName
1854
1895
  }
@@ -1898,8 +1939,6 @@ app.get('/', async function(req, res) {
1898
1939
  body += ' (~' + blackouts[gamePk].blackoutExpiry.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + ')'
1899
1940
  }
1900
1941
  body += '</span></span>'
1901
- } else if ( (station == 'FOX') ) {
1902
- body += '<span class="tooltip">' + teamabbr + '<span class="tooltiptext">Regional FOX game</span></span>'
1903
1942
  } else {
1904
1943
  body += teamabbr
1905
1944
  }
@@ -1927,6 +1966,7 @@ app.get('/', async function(req, res) {
1927
1966
  if ( inning_half != VALID_INNING_HALF[0] ) querystring += '&inning_half=' + inning_half
1928
1967
  if ( inning_number != VALID_INNING_NUMBER[0] ) querystring += '&inning_number=' + relative_inning
1929
1968
  if ( skip != VALID_SKIP[0] ) querystring += '&skip=' + skip
1969
+ if ( skip_adjust != DEFAULT_SKIP_ADJUST ) querystring += '&skip_adjust=' + skip_adjust
1930
1970
  if ( (inning_half != VALID_INNING_HALF[0]) || (inning_number != VALID_INNING_NUMBER[0]) || (skip != VALID_SKIP[0]) ) {
1931
1971
  querystring += '&gamePk=' + cache_data.dates[0].games[j].gamePk
1932
1972
  }
@@ -1938,6 +1978,8 @@ app.get('/', async function(req, res) {
1938
1978
  if ( broadcast.mediaState.mediaStateCode == 'MEDIA_ON' ) {
1939
1979
  if ( force_vod != VALID_FORCE_VOD[0] ) querystring += '&force_vod=' + force_vod
1940
1980
  }
1981
+ } else if ( linkType == VALID_LINK_TYPES[4] ) {
1982
+ querystring += '&filename=' + filename + station
1941
1983
  }
1942
1984
  querystring += content_protect_b
1943
1985
  multiviewquerystring += content_protect_b
@@ -1949,7 +1991,7 @@ app.get('/', async function(req, res) {
1949
1991
  body += stationlink
1950
1992
  }
1951
1993
  if ( mediaType == 'MLBTV' ) {
1952
- body += '<input type="checkbox" value="' + server + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this, [\'' + awayteam + '\', \'' + hometeam + '\'])">'
1994
+ body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this, [\'' + awayteam + '\', \'' + hometeam + '\'])">'
1953
1995
  }
1954
1996
  if ( resumeStatus ) {
1955
1997
  body += '('
@@ -2053,6 +2095,9 @@ app.get('/', async function(req, res) {
2053
2095
  if ( skip == VALID_SKIP[i] ) body += 'class="default" '
2054
2096
  body += 'onclick="skip=\'' + VALID_SKIP[i] + '\';reload()">' + VALID_SKIP[i] + '</button> '
2055
2097
  }
2098
+ if ( skip != VALID_SKIP[0] ) {
2099
+ body += '<br><span class="tooltip">Skip Adjust<span class="tooltiptext">Seconds to adjust the skip time video segments, if necessary. Try a negative number if the plays are ending before the video segments begin; use a positive number if the video segments are ending before the play happens.</span></span>: <input type="number" id="skip_adjust" value="' + skip_adjust + '" step="1" onchange="setTimeout(function(){skip_adjust=document.getElementById(\'skip_adjust\').value;reload()},750)" onblur="skip_adjust=this.value;reload()" style="vertical-align:top;font-size:.8em;width:3em"/>'
2100
+ }
2056
2101
  body += '</p>' + "\n"
2057
2102
  }
2058
2103
 
@@ -2081,7 +2126,7 @@ app.get('/', async function(req, res) {
2081
2126
  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"
2082
2127
  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"
2083
2128
  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"/>'
2084
- body += '<hr>Watch: <a href="/embed.html?src=' + encodeURIComponent(multiview_server + multiview_url_path) + '">Embed</a> | <a href="' + multiview_server + multiview_url_path + '">Stream</a> | <a href="/chromecast.html?src=' + encodeURIComponent(multiview_server + multiview_url_path) + '">Chromecast</a> | <a href="/advanced.html?src=' + encodeURIComponent(multiview_server + multiview_url_path) + '">Advanced</a><br/><span class="tinytext">Download: <a href="/kodi.strm?src=' + encodeURIComponent(multiview_server + multiview_url_path) + '">Kodi STRM file</a> (<a href="/kodi.strm?version=18&src=' + encodeURIComponent(multiview_server + multiview_url_path) + '">Leia/18</a>)</span>'
2129
+ body += '<hr>Watch: <a href="/embed.html?src=' + encodeURIComponent(multiview_server + multiview_url_path) + content_protect_b + '">Embed</a> | <a href="' + multiview_server + multiview_url_path + content_protect_b + '">Stream</a> | <a href="/chromecast.html?src=' + encodeURIComponent(multiview_server + multiview_url_path) + content_protect_b + '">Chromecast</a> | <a href="/advanced.html?src=' + encodeURIComponent(multiview_server + multiview_url_path) + content_protect_b + '">Advanced</a> | <a href="/download.html?src=' + encodeURIComponent(multiview_server + multiview_url_path) + content_protect_b + '&filename=' + gameDate + ' Multiview">Download</a><br/><span class="tinytext">Kodi STRM files: <a href="/kodi.strm?src=' + encodeURIComponent(multiview_server + multiview_url_path) + content_protect_b + '">Matrix/19+</a> (<a href="/kodi.strm?version=18&src=' + encodeURIComponent(multiview_server + multiview_url_path) + content_protect_b + '">Leia/18</a>)</span>'
2085
2130
  body += '</td></tr></table><br/>' + "\n"
2086
2131
  }
2087
2132
 
@@ -2215,7 +2260,7 @@ app.get('/', async function(req, res) {
2215
2260
  body += '<p><span class="tooltip">Bookmarklets for MLB.com<span class="tooltiptext">If you watch at MLB.com, drag these bookmarklets to your bookmarks toolbar and use them to hide parts of the interface.</span></span>: <a href="javascript:(function(){let x=document.querySelector(\'#mlbtv-stats-panel\');if(x.style.display==\'none\'){x.style.display=\'initial\';}else{x.style.display=\'none\';}})();">Boxscore</a> | <a href="javascript:(function(){let x=document.querySelector(\'.mlbtv-header-container\');if(x.style.display==\'none\'){let y=document.querySelector(\'.mlbtv-players-container\');y.style.display=\'none\';x.style.display=\'initial\';setTimeout(function(){y.style.display=\'initial\';},15);}else{x.style.display=\'none\';}})();">Scoreboard</a> | <a href="javascript:(function(){let x=document.querySelector(\'.mlbtv-container--footer\');if(x.style.display==\'none\'){let y=document.querySelector(\'.mlbtv-players-container\');y.style.display=\'none\';x.style.display=\'initial\';setTimeout(function(){y.style.display=\'initial\';},15);}else{x.style.display=\'none\';}})();">Linescore</a> | <a href="javascript:(function(){let x=document.querySelector(\'#mlbtv-stats-panel\');if(x.style.display==\'none\'){x.style.display=\'initial\';}else{x.style.display=\'none\';}x=document.querySelector(\'.mlbtv-header-container\');if(x.style.display==\'none\'){x.style.display=\'initial\';}else{x.style.display=\'none\';}x=document.querySelector(\'.mlbtv-container--footer\');if(x.style.display==\'none\'){let y=document.querySelector(\'.mlbtv-players-container\');y.style.display=\'none\';x.style.display=\'initial\';setTimeout(function(){y.style.display=\'initial\';},15);}else{x.style.display=\'none\';}})();">All</a></p>' + "\n"
2216
2261
 
2217
2262
  // Print version
2218
- body += '<p class="tinytext">Version ' + version + ' (<a href="/clearcache">clear cache</a>)</p>' + "\n"
2263
+ body += '<p class="tinytext">Version ' + version + ' (<a href="/clearcache">clear session and cache</a>)</p>' + "\n"
2219
2264
 
2220
2265
  // Datepicker functions
2221
2266
  body += '<script>var datePicker=document.getElementById("gameDate");function changeDate(e){date=datePicker.value;reload()}function removeDate(e){datePicker.removeEventListener("change",changeDate,false);datePicker.addEventListener("blur",changeDate,false);if(e.keyCode===13){date=datePicker.value;reload()}}datePicker.addEventListener("change",changeDate,false);datePicker.addEventListener("keypress",removeDate,false)</script>' + "\n"
@@ -2321,7 +2366,7 @@ app.get('/advanced.html', async function(req, res) {
2321
2366
  }
2322
2367
  session.debuglog('advanced embed src : ' + video_url)
2323
2368
 
2324
- res.redirect('http://hls-js-dev.netlify.app/demo/?src=' + encodeURIComponent(video_url))
2369
+ res.redirect('https://hls-js-dev.netlify.app/demo/?src=' + encodeURIComponent(video_url))
2325
2370
  })
2326
2371
 
2327
2372
  // Listen for Chromecast request, redirect to chromecast.link player
@@ -2710,13 +2755,19 @@ function start_multiview_stream(streams, sync, dvr, faster, reencode, park_audio
2710
2755
  audio_reencoded = []
2711
2756
  for (var i=0; i<audio_present.length; i++) {
2712
2757
  //let audio_input = audio_present[i] + ':a:m:language:en?'
2713
- let audio_input = audio_present[i] + ':a:0'
2758
+ let audio_input = audio_present[i] + ':a:'
2759
+ let video_url = streams[audio_present[i]]
2760
+ if ( video_url.includes('audio_track=English') || !video_url.includes('audio_track=') ) {
2761
+ audio_input += '0'
2762
+ } else {
2763
+ audio_input += '1'
2764
+ }
2714
2765
  let filter = ''
2715
2766
  // Optionally apply sync adjustments
2716
2767
  if ( sync[audio_present[i]] ) {
2717
2768
  if ( sync[audio_present[i]] > 0 ) {
2718
2769
  session.log('delaying audio for stream ' + (audio_present[i]+1) + ' by ' + sync[audio_present[i]] + ' seconds')
2719
- filter = 'adelay=' + (sync[i] * 1000) + ','
2770
+ filter = 'adelay=' + (sync[i] * 1000) + ':all=1,'
2720
2771
  } else if ( sync[audio_present[i]] < 0 ) {
2721
2772
  session.log('trimming audio for stream ' + (audio_present[i]+1) + ' by ' + sync[audio_present[i]] + ' seconds')
2722
2773
  filter = 'atrim=start=' + (sync[audio_present[i]] * -1) + 's,'
@@ -2747,7 +2798,13 @@ function start_multiview_stream(streams, sync, dvr, faster, reencode, park_audio
2747
2798
  if ( audio_reencoded.indexOf(audio_present[i]) > -1 ) {
2748
2799
  audio_output = '[out' + i + ']'
2749
2800
  } else {
2750
- audio_output = audio_present[i] + ':a:0'
2801
+ audio_output = audio_present[i] + ':a:'
2802
+ let video_url = streams[audio_present[i]]
2803
+ if ( video_url.includes('audio_track=English') || !video_url.includes('audio_track=') ) {
2804
+ audio_output += '0'
2805
+ } else {
2806
+ audio_output += '1'
2807
+ }
2751
2808
  }
2752
2809
  ffmpeg_command.addOutputOption('-map', audio_output)
2753
2810
  var_stream_map += ' a:' + i + ',agroup:aac,language:ENG'
@@ -2898,3 +2955,94 @@ app.get('/kodi.strm', async function(req, res) {
2898
2955
  res.end('kodi.strm request error, check log')
2899
2956
  }
2900
2957
  })
2958
+
2959
+ // Listen for download requests
2960
+ app.get('/download.html', async function(req, res) {
2961
+ if ( ! (await protect(req, res)) ) return
2962
+
2963
+ try {
2964
+ if ( req.query.park_audio ) {
2965
+ session.debuglog('parkaudio', req)
2966
+ } else {
2967
+ session.requestlog('download', req)
2968
+ }
2969
+
2970
+ let server = 'http://' + req.headers.host
2971
+
2972
+ let video_url = '/stream.m3u8'
2973
+ if ( req.query.src ) {
2974
+ video_url = req.query.src
2975
+ } else {
2976
+ let urlArray = req.url.split('?')
2977
+ if ( (urlArray.length == 2) ) {
2978
+ video_url += '?' + urlArray[1]
2979
+ }
2980
+ video_url = server + video_url
2981
+ }
2982
+ session.debuglog('download src : ' + video_url)
2983
+
2984
+ ffmpeg_command = ffmpeg({ timeout: 432000 })
2985
+
2986
+ // park audio
2987
+ if ( req.query.park_audio ) {
2988
+ ffmpeg_command.complexFilter([{
2989
+ filter: 'pan=stereo|c0=c0|c1=-1*c1',
2990
+ inputs: '0:a:0',
2991
+ outputs: 'out0'
2992
+ }])
2993
+
2994
+ // Set input stream, minimize ffmpeg startup latency, re-encode audio to mono, and copy source PTS values
2995
+ ffmpeg_command.input(video_url)
2996
+ .addInputOption('-fflags', 'nobuffer')
2997
+ .addInputOption('-probesize', '32')
2998
+ .addInputOption('-analyzeduration', '0')
2999
+ .addOutputOption('-map', '0:v:0')
3000
+ .addOutputOption('-map', '[out0]')
3001
+ .addOutputOption('-c:v', 'copy')
3002
+ .addOutputOption('-c:a', 'aac')
3003
+ .addOutputOption('-ac:a:0', '1')
3004
+ .addOutputOption('-copyts')
3005
+ .addOutputOption('-muxpreload', '0')
3006
+ .addOutputOption('-muxdelay', '0')
3007
+ } else {
3008
+ // Set input stream
3009
+ ffmpeg_command.input(video_url)
3010
+ .addOutputOption('-c', 'copy')
3011
+ }
3012
+
3013
+ ffmpeg_command.addOutputOption('-f', 'mpegts')
3014
+ .output(res)
3015
+ .on('start', function(commandLine) {
3016
+ session.debuglog('download command started')
3017
+ if ( argv.debug || argv.ffmpeg_logging ) {
3018
+ session.debuglog('download command: ' + commandLine)
3019
+ }
3020
+ })
3021
+ .on('error', function(err, stdout, stderr) {
3022
+ session.debuglog('download command stopped: ' + err.message)
3023
+ if ( stdout ) session.log(stdout)
3024
+ if ( stderr ) session.log(stderr)
3025
+ })
3026
+ .on('end', function() {
3027
+ session.debuglog('download command ended')
3028
+ })
3029
+
3030
+ if ( argv.ffmpeg_logging ) {
3031
+ session.log('ffmpeg output logging enabled')
3032
+ ffmpeg_command.on('stderr', function(stderrLine) {
3033
+ session.log(stderrLine);
3034
+ })
3035
+ }
3036
+
3037
+ var download_headers = {}
3038
+ if ( req.query.filename ) {
3039
+ download_headers['Content-Disposition'] = 'attachment; filename="' + req.query.filename + '.ts"'
3040
+ }
3041
+ res.writeHead(200, download_headers)
3042
+
3043
+ ffmpeg_command.run()
3044
+ } catch (e) {
3045
+ session.log('download request error : ' + e.message)
3046
+ res.end('')
3047
+ }
3048
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mlbserver",
3
- "version": "2024.08.02",
3
+ "version": "2024.10.08.2",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
package/session.js CHANGED
@@ -32,7 +32,12 @@ const AFFILIATE_TEAM_IDS = { 'ATL': '430,431,432,478', 'AZ': '419,516,2310,5368'
32
32
  const ESPN_SUNDAY_NIGHT_BLACKOUT_COUNTRIES = ["Angola", "Anguilla", "Antigua and Barbuda", "Argentina", "Aruba", "Australia", "Bahamas", "Barbados", "Belize", "Belize", "Benin", "Bermuda", "Bolivia", "Bonaire", "Botswana", "Brazil", "British Virgin Islands", "Burkina Faso", "Burundi", "Cameroon", "Cape Verde", "Cayman Islands", "Central African Republic", "Chad", "Chile", "Colombia", "Comoros", "Cook Islands", "Costa Rica", "Cote d'Ivoire", "Curacao", "Democratic Republic of the Congo", "Djibouti", "Dominica", "Dominican Republic", "Ecuador", "El Salvador", "England", "Equatorial Guinea", "Eritrea", "Eswatini", "Ethiopia", "Falkland Islands", "Falkland Islands", "Fiji", "French Guiana", "French Guiana", "French Polynesia", "Gabon", "Ghana", "Grenada", "Guadeloupe", "Guatemala", "Guinea", "Guinea Bissau", "Guyana", "Guyana", "Haiti", "Honduras", "Ireland", "Jamaica", "Kenya", "Kiribati", "Lesotho", "Liberia", "Madagascar", "Malawi", "Mali", "Marshall Islands", "Martinique", "Mayotte", "Mexico", "Micronesia", "Montserrat", "Mozambique", "Namibia", "Netherlands", "New Zealand", "Nicaragua", "Niger", "Nigeria", "Niue", "Northern Ireland", "Palau Islands", "Panama", "Paraguay", "Peru", "Republic of Ireland", "Reunion", "Rwanda", "Saba", "Saint Maarten", "Samoa", "Sao Tome & Principe", "Scotland", "Senegal", "Seychelles", "Sierra Leone", "Solomon Islands", "Somalia", "South Africa", "St. Barthelemy", "St. Eustatius", "St. Kitts and Nevis", "St. Lucia", "St. Martin", "St. Vincent and the Grenadines", "Sudan", "Surinam", "Suriname", "Tahiti", "Tanzania & Zanzibar", "The Gambia", "The Republic of Congo", "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Turks and Caicos Islands", "Tuvalu", "Uganda", "Uruguay", "Venezuela", "Wales", "Zambia", "Zimbabwe"]
33
33
 
34
34
  // First is default level, last should be All (also used as default org)
35
- const LEVELS = { 'MLB': '1', 'AAA': '11', 'AA': '12', 'A+': '13', 'A': '14', 'All': '1,11,12,13,14' }
35
+ const LEVELS = { 'MLB': '1', 'AAA': '11', 'AA': '12', 'A+': '13', 'A': '14', 'WINTER': '17', 'All': '1,11,12,13,14,17' }
36
+
37
+ // Winter Leagues
38
+ const AFL_ID = '119'
39
+ const LIDOM_ID = '131'
40
+ const WINTER_LEAGUES = [AFL_ID, LIDOM_ID]
36
41
 
37
42
  // These are the events to ignore, if we're skipping breaks
38
43
  const BREAK_TYPES = ['Game Advisory', 'Pitching Substitution', 'Offensive Substitution', 'Defensive Sub', 'Defensive Switch', 'Runner Placed On Base']
@@ -1109,6 +1114,10 @@ class sessionClass {
1109
1114
  return LEVELS
1110
1115
  }
1111
1116
 
1117
+ getLidomId() {
1118
+ return LIDOM_ID
1119
+ }
1120
+
1112
1121
  getLevelNameFromSportId(sportId) {
1113
1122
  let sportIds = Object.values(LEVELS)
1114
1123
  for (var i=0; i<sportIds.length; i++) {
@@ -1201,6 +1210,17 @@ class sessionClass {
1201
1210
  this.save_cache_data()
1202
1211
  }
1203
1212
 
1213
+ setBlackoutsCacheExpiry(cache_name, expiryDate) {
1214
+ if ( !this.cache.blackouts ) {
1215
+ this.cache.blackouts={}
1216
+ }
1217
+ if ( !this.cache.blackouts[cache_name] ) {
1218
+ this.cache.blackouts[cache_name] = {}
1219
+ }
1220
+ this.cache.blackouts[cache_name].blackoutsCacheExpiry = expiryDate
1221
+ this.save_cache_data()
1222
+ }
1223
+
1204
1224
  setDateCacheExpiry(cache_name, expiryDate) {
1205
1225
  if ( !this.cache.dates ) {
1206
1226
  this.cache.dates={}
@@ -1663,6 +1683,7 @@ class sessionClass {
1663
1683
  this.data.loginToken = obj.access_token
1664
1684
  this.data.loginTokenExpiry = new Date(new Date().getTime() + obj.expires_in * 1000)
1665
1685
  this.save_session_data()
1686
+ await this.getSession()
1666
1687
  return this.data.loginToken
1667
1688
  } else {
1668
1689
  this.log('getLoginToken response failure')
@@ -1673,6 +1694,53 @@ class sessionClass {
1673
1694
  }
1674
1695
  }
1675
1696
 
1697
+ // API call
1698
+ async getOktaId() {
1699
+ this.debuglog('getOktaId')
1700
+ if ( !this.data.oktaId ) {
1701
+ this.debuglog('need to get oktaId')
1702
+ // get a sample playback token from which to extract the base64-encoded okta_id
1703
+ let token
1704
+
1705
+ // use any currently cached playback token
1706
+ try {
1707
+ this.debuglog('getOktaId attempting to use cached playback token')
1708
+ let mediaIds = Object.keys(this.cache.media)
1709
+ for (var i=0; i<mediaIds.length; i++) {
1710
+ if ( this.cache.media[mediaIds[i]].streamURLToken && (this.cache.media[mediaIds[i]].streamURLToken != '') ) {
1711
+ this.debuglog('getOktaId using cached playback token')
1712
+ token = this.cache.media[mediaIds[i]].streamURLToken
1713
+ break
1714
+ }
1715
+ }
1716
+ } catch(e) {
1717
+ this.debuglog('getOktaId failed to use cached playback token')
1718
+ }
1719
+
1720
+ // otherwise, get a new playback token for a past free game
1721
+ if ( !token ) {
1722
+ this.debuglog('getOktaId getting new playback token')
1723
+ let streamInfo = await this.getStreamURL('b7f0fff7-266f-4171-aa2d-af7988dc9302')
1724
+ token = streamInfo.streamURLToken
1725
+ }
1726
+
1727
+ if ( token ) {
1728
+ this.debuglog('getOktaId using token ' + token)
1729
+ let encoded_okta_id = token.split('_')[1]
1730
+ let okta_id = Buffer.from(encoded_okta_id + '==', 'base64').toString('ascii')
1731
+ this.debuglog('getOktaId extracted okta_id ' + okta_id)
1732
+ this.data.oktaId = okta_id
1733
+ this.save_session_data()
1734
+ return this.data.oktaId
1735
+ } else {
1736
+ this.debuglog('failed to get oktaId')
1737
+ }
1738
+ } else {
1739
+ this.debuglog('using cached oktaId')
1740
+ return this.data.oktaId
1741
+ }
1742
+ }
1743
+
1676
1744
  // get mediaId for a live channel request
1677
1745
  async getMediaId(team, level, mediaType, mediaDate, gameNumber, includeBlackouts) {
1678
1746
  try {
@@ -1714,7 +1782,7 @@ class sessionClass {
1714
1782
  let nationalCount = 0
1715
1783
  let freeCount = 0
1716
1784
  let blackouts = {}
1717
- if ( includeBlackouts == 'false' ) blackouts = await this.get_blackout_games(cache_data.dates[0].games, true)
1785
+ if ( includeBlackouts == 'false' ) blackouts = await this.get_blackout_games(cache_data.dates[0].date, true)
1718
1786
 
1719
1787
  for (var j = 0; j < cache_data.dates[0].games.length; j++) {
1720
1788
  if ( mediaInfo.mediaId || mediaInfo.gamePk ) break
@@ -1729,10 +1797,10 @@ class sessionClass {
1729
1797
  this.debuglog('checking game ' + cache_data.dates[0].games[j].teams['home'].team.abbreviation + '@' + cache_data.dates[0].games[j].teams['away'].team.abbreviation)
1730
1798
 
1731
1799
  // check that that game involves the requested team, or if we've requested a national or free game
1732
- if ( ((team.toUpperCase() == home_team) && (LEVELS[level.toUpperCase()] == home_level)) || ((team.toUpperCase() == away_team) && (LEVELS[level.toUpperCase()] == away_level)) || (team.toUpperCase().indexOf('NATIONAL.') == 0) || ((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))) ) {
1800
+ 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))) ) {
1733
1801
 
1734
1802
  // Check if Winter League / MiLB game first
1735
- if ( (cache_data.dates[0].games[j].teams['home'].team.sport.id != LEVELS['MLB']) && (mediaType == 'MLBTV') ) {
1803
+ if ( (home_level != LEVELS['MLB']) && (mediaType == 'MLBTV') ) {
1736
1804
  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)
1737
1805
  if ( cache_data.dates[0].games[j].broadcasts ) {
1738
1806
  let broadcastName = 'N/A'
@@ -1946,7 +2014,7 @@ class sessionClass {
1946
2014
  cache_name += '.' + team_ids
1947
2015
  }
1948
2016
  //let data_url = 'https://bdfed.stitch.mlbinfra.com/bdfed/transform-mlb-scoreboard?stitch_env=prod&sortTemplate=2&sportId=1&sportId=17&startDate=' + dateString + '&endDate=' + dateString + '&gameType=E&&gameType=S&&gameType=R&&gameType=F&&gameType=D&&gameType=L&&gameType=W&&gameType=A&language=en&leagueId=104&leagueId=103&leagueId=131&contextTeamId='
1949
- let data_url = 'http://statsapi.mlb.com/api/v1/schedule?sportId=' + level_ids
2017
+ let data_url = 'https://statsapi.mlb.com/api/v1/schedule?sportId=' + level_ids
1950
2018
  if ( team_ids != '' ) {
1951
2019
  data_url += '&teamId=' + team_ids
1952
2020
  }
@@ -1954,7 +2022,7 @@ class sessionClass {
1954
2022
  if ( team && !team.toUpperCase().startsWith('NATIONAL.') && !team.toUpperCase().startsWith('FREE.') ) {
1955
2023
  this.debuglog('getDayData for team ' + team + ' on date ' + dateString)
1956
2024
  cache_name = team.toUpperCase() + dateString
1957
- data_url = 'http://statsapi.mlb.com/api/v1/schedule?sportId=1&teamId=' + TEAM_IDS[team.toUpperCase()] + '&startDate=' + dateString + '&endDate=' + dateString + '&hydrate=team,broadcasts(all)'
2025
+ data_url = 'https://statsapi.mlb.com/api/v1/schedule?sportId=1&teamId=' + TEAM_IDS[team.toUpperCase()] + '&startDate=' + dateString + '&endDate=' + dateString + '&hydrate=team,broadcasts(all)'
1958
2026
  } else {
1959
2027
  this.debuglog('getDayData for level(s) ' + level_ids + ' on date ' + dateString)
1960
2028
  }
@@ -2056,7 +2124,7 @@ class sessionClass {
2056
2124
  let endDate = new Date(startDate)
2057
2125
  endDate.setDate(endDate.getDate()+20)
2058
2126
  endDate = endDate.toISOString().substring(0,10)
2059
- let data_url = 'http://statsapi.mlb.com/api/v1/schedule?sportId=' + level_ids
2127
+ let data_url = 'https://statsapi.mlb.com/api/v1/schedule?sportId=' + level_ids
2060
2128
  if ( team_ids != '' ) {
2061
2129
  data_url += '&teamId=' + team_ids
2062
2130
  }
@@ -2181,13 +2249,14 @@ class sessionClass {
2181
2249
 
2182
2250
  let gameIndexes_obj = {}
2183
2251
 
2252
+ let blackouts = {}
2253
+ if ( includeBlackouts == 'false' ) blackouts = await this.get_blackout_games()
2254
+
2184
2255
  for (var i = 0; i < cache_data.dates.length; i++) {
2185
2256
  this.debuglog('getTVData processing date ' + cache_data.dates[i].date)
2186
2257
  let dateIndex = {MLBTV:i,Free:i,Audio:i}
2187
2258
  let gameCounter = {MLBTV:0,Free:0,Audio:0}
2188
2259
 
2189
- let blackouts = {}
2190
- if ( includeBlackouts == 'false' ) blackouts = await this.get_blackout_games(cache_data.dates[i].games)
2191
2260
  let gameIndexes = await this.get_first_and_last_games(cache_data.dates[i].games, blackouts)
2192
2261
  // store gameIndexes for gamechanger/multiview reference later
2193
2262
  gameIndexes_obj[cache_data.dates[i].date] = gameIndexes
@@ -2208,6 +2277,7 @@ class sessionClass {
2208
2277
  continue
2209
2278
  } else {
2210
2279
  for (var k = 0; k < cache_data.dates[i].games[j].broadcasts.length; k++) {
2280
+ let league_id = cache_data.dates[i].games[j].teams['home'].team.league.id
2211
2281
  let team = cache_data.dates[i].games[j].teams['home'].team.abbreviation
2212
2282
  let team_id = cache_data.dates[i].games[j].teams['home'].team.id.toString()
2213
2283
  let opponent_team_id = cache_data.dates[i].games[j].teams['away'].team.id.toString()
@@ -2219,9 +2289,16 @@ class sessionClass {
2219
2289
  let channelid = mediaType + '.' + sportId + '.' + team
2220
2290
  //let logo = server + '/image.svg?teamId=' + team_id
2221
2291
  let logo = 'https://www.mlbstatic.com/team-logos/share/' + team_id + '.jpg'
2222
- if ( this.protection.content_protect ) logo += '&amp;content_protect=' + this.protection.content_protect
2292
+ //if ( this.protection.content_protect ) logo += '&amp;content_protect=' + this.protection.content_protect
2293
+ if ( league_id == LIDOM_ID ) {
2294
+ let lidom_abbr = cache_data.dates[i].games[j].teams['home'].team.name.replace(/[^A-Z]/g, '').toLowerCase()
2295
+ logo = 'https://pizarra.multimediard.com/ac/' + lidom_abbr + '.png'
2296
+ }
2223
2297
  let streamMediaType = 'Video'
2224
2298
  let stream = server + '/stream.m3u8?team=' + encodeURIComponent(team) + '&mediaType=' + streamMediaType
2299
+ if ( league_id == LIDOM_ID ) {
2300
+ stream = server + '/stream.m3u8?event=' + encodeURIComponent(cache_data.dates[i].games[j].teams['home'].team.clubName.toUpperCase()) + '&mediaType=' + streamMediaType
2301
+ }
2225
2302
  stream += '&level=' + encodeURIComponent(this.getLevelNameFromSportId(sportId))
2226
2303
  stream += '&resolution=' + resolution
2227
2304
  if ( this.protection.content_protect ) stream += '&content_protect=' + this.protection.content_protect
@@ -2229,13 +2306,23 @@ class sessionClass {
2229
2306
  channels[channelid] = await this.create_channel_object(channelid, logo, stream, mediaType)
2230
2307
 
2231
2308
  let title = 'Minor League Baseball'
2309
+ if ( WINTER_LEAGUES.includes(league_id.toString()) ) {
2310
+ title = cache_data.dates[i].games[j].teams['home'].team.league.name
2311
+ }
2232
2312
 
2233
2313
  let away_team = cache_data.dates[i].games[j].teams['away'].team.name
2234
2314
  let home_team = cache_data.dates[i].games[j].teams['home'].team.name
2235
2315
  let subtitle = away_team + ' at ' + home_team
2236
2316
 
2237
2317
  if (includeTeamsInTitles == 'true') {
2238
- title = 'MiLB: ' + subtitle
2318
+ if ( league_id == AFL_ID ) {
2319
+ title = 'AFL'
2320
+ } else if ( league_id == LIDOM_ID ) {
2321
+ title = 'LIDOM'
2322
+ } else {
2323
+ title = 'MiLB'
2324
+ }
2325
+ title += ': ' + subtitle
2239
2326
  }
2240
2327
 
2241
2328
  let description = cache_data.dates[i].games[j].teams['home'].team.league.name + '. '
@@ -2528,7 +2615,7 @@ class sessionClass {
2528
2615
  this.debuglog('getTVData processing MLB Network')
2529
2616
  let logo = 'https://encrypted-tbn1.gstatic.com/images?q=tbn:ANd9GcQRgC2JdbtFplKjfhXm5_vzpkUQ3XyDT91SEnHmuB0p5tReQ3Ez'
2530
2617
  let channelid = mediaType + '.MLBN'
2531
- if ( this.protection.content_protect ) logo += '&amp;content_protect=' + this.protection.content_protect
2618
+ //if ( this.protection.content_protect ) logo += '&amp;content_protect=' + this.protection.content_protect
2532
2619
  let stream = server + '/stream.m3u8?event=mlbn&mediaType=Video&resolution=' + resolution
2533
2620
  if ( this.protection.content_protect ) stream += '&content_protect=' + this.protection.content_protect
2534
2621
  if ( pipe == 'true' ) stream = await this.convert_stream_to_pipe(stream, channelid)
@@ -2559,7 +2646,7 @@ class sessionClass {
2559
2646
  this.debuglog('getTVData processing Big Inning')
2560
2647
  let logo = 'https://img.mlbstatic.com/mlb-images/image/private/ar_16:9,g_auto,q_auto:good,w_372,c_fill,f_jpg/mlb/uwr8vepua4t1fe8uwyki'
2561
2648
  let channelid = mediaType + '.BIGINNING'
2562
- if ( this.protection.content_protect ) logo += '&amp;content_protect=' + this.protection.content_protect
2649
+ //if ( this.protection.content_protect ) logo += '&amp;content_protect=' + this.protection.content_protect
2563
2650
  let stream = server + '/stream.m3u8?event=biginning&mediaType=Video&resolution=' + resolution
2564
2651
  if ( this.protection.content_protect ) stream += '&content_protect=' + this.protection.content_protect
2565
2652
  if ( pipe == 'true' ) stream = await this.convert_stream_to_pipe(stream, channelid)
@@ -2770,7 +2857,7 @@ class sessionClass {
2770
2857
  let currentDate = new Date()
2771
2858
  if ( !fs.existsSync(cache_file) || !this.cache || !this.cache.gameday || !this.cache.gameday[cache_name] || !this.cache.gameday[cache_name].gamedayCacheExpiry || (currentDate > new Date(this.cache.gameday[cache_name].gamedayCacheExpiry)) ) {
2772
2859
  let reqObj = {
2773
- url: 'http://statsapi.mlb.com/api/v1.1/game/' + gamePk + '/feed/live',
2860
+ url: 'https://statsapi.mlb.com/api/v1.1/game/' + gamePk + '/feed/live',
2774
2861
  headers: {
2775
2862
  'User-agent': USER_AGENT,
2776
2863
  'Origin': 'https://www.mlb.com',
@@ -2829,6 +2916,9 @@ class sessionClass {
2829
2916
  if ( streamURLToken ) {
2830
2917
  variant_url += '&streamURLToken=' + encodeURIComponent(streamURLToken)
2831
2918
  }
2919
+ if ( this.protection.content_protect ) {
2920
+ variant_url += '&content_protect=' + this.protection.content_protect
2921
+ }
2832
2922
  let reqObj = {
2833
2923
  url: variant_url
2834
2924
  }
@@ -2853,10 +2943,12 @@ class sessionClass {
2853
2943
  }
2854
2944
 
2855
2945
  // Get skip markers into temporary cache
2856
- async getSkipMarkers(gamePk, skip_type, start_inning, start_inning_half, streamURL, streamURLToken) {
2946
+ async getSkipMarkers(gamePk, skip_type, start_inning, start_inning_half, streamURL, streamURLToken, skip_adjust) {
2857
2947
  try {
2858
2948
  this.debuglog('getSkipMarkers')
2859
2949
 
2950
+ if ( skip_adjust != 0 ) this.log('manual adjustment of ' + skip_adjust + ' seconds being applied')
2951
+
2860
2952
  if ( !this.temp_cache[gamePk] ) {
2861
2953
  this.temp_cache[gamePk] = {}
2862
2954
  }
@@ -2972,6 +3064,8 @@ class sessionClass {
2972
3064
  // then we'll add the skip marker
2973
3065
  // otherwise we'll ignore it and move on to the next one
2974
3066
  if ( ((break_end - break_start) >= MINIMUM_BREAK_DURATION) && ((skip_type != 1) || (current_inning != previous_inning) || (current_inning_half != previous_inning_half)) ) {
3067
+ if ( break_start > 0 ) break_start += skip_adjust
3068
+ break_end += skip_adjust
2975
3069
  skip_markers.push({'break_start': break_start, 'break_end': break_end})
2976
3070
  total_skip_time += break_end - break_start
2977
3071
  previous_inning = current_inning
@@ -3486,71 +3580,122 @@ class sessionClass {
3486
3580
  return regional_fox_games_exist
3487
3581
  }
3488
3582
 
3583
+ // get blackouts data for a day
3584
+ async getBlackoutsData(dateString, endDate) {
3585
+ try {
3586
+ let cache_data
3587
+ let cache_name = 'b' + dateString
3588
+ let data_url = 'https://mastapi.mobile.mlbinfra.com/api/epg/v3/search?exp=MLB&date=' + dateString
3589
+ let utcHours = 10
3590
+ if ( dateString == 'guide' ) {
3591
+ cache_name = 'bweek'
3592
+ this.debuglog('getBlackoutsData for week')
3593
+ let startDate = this.liveDate(utcHours)
3594
+ let endDate = new Date(startDate)
3595
+ endDate.setDate(endDate.getDate()+20)
3596
+ endDate = endDate.toISOString().substring(0,10)
3597
+ data_url = 'https://mastapi.mobile.mlbinfra.com/api/epg/v3/search?exp=MLB&startDate=' + startDate + '&endDate=' + endDate
3598
+ } else {
3599
+ this.debuglog('getBlackoutsData for ' + dateString)
3600
+ }
3601
+ let cache_file = path.join(this.CACHE_DIRECTORY, cache_name+'.json')
3602
+ let currentDate = new Date()
3603
+ if ( !fs.existsSync(cache_file) || !this.cache || !this.cache.blackouts || !this.cache.blackouts[cache_name] || !this.cache.blackouts[cache_name].blackoutsCacheExpiry || (currentDate > new Date(this.cache.blackouts[cache_name].blackoutsCacheExpiry)) ) {
3604
+ let reqObj = {
3605
+ url: data_url,
3606
+ headers: {
3607
+ 'Accept': '*/*',
3608
+ 'Accept-Language': 'en-US,en;q=0.9',
3609
+ 'Content-Type': 'application/json',
3610
+ 'Origin': 'https://www.mlb.com',
3611
+ 'Referer': 'https://www.mlb.com/',
3612
+ 'User-Agent': USER_AGENT
3613
+ }
3614
+ }
3615
+ if ( (this.credentials.account_username.length > 0) && (this.credentials.account_password.length > 0) ) {
3616
+ let access_token = await this.getLoginToken()
3617
+ let okta_id = await this.getOktaId()
3618
+ if ( access_token && okta_id ) {
3619
+ reqObj.headers['authorization'] = 'Bearer ' + access_token
3620
+ reqObj.headers['x-okta-id'] = okta_id
3621
+ }
3622
+ }
3623
+ var response = await this.httpGet(reqObj, false)
3624
+ if ( response && this.isValidJson(response) ) {
3625
+ //this.debuglog(response)
3626
+ cache_data = JSON.parse(response)
3627
+ this.save_json_cache_file(cache_name, cache_data)
3628
+
3629
+ // Default cache period is 1 day from now
3630
+ let today = this.liveDate()
3631
+ let tomorrowDate = new Date(today)
3632
+ tomorrowDate.setDate(tomorrowDate.getDate()+1)
3633
+ tomorrowDate.setHours(tomorrowDate.getHours()+utcHours)
3634
+ let cacheExpiry = tomorrowDate
3635
+
3636
+ // finally save the setting
3637
+ this.setBlackoutsCacheExpiry(cache_name, cacheExpiry)
3638
+ } else {
3639
+ this.log('error : invalid json from url ' + reqObj.url)
3640
+ }
3641
+ } else {
3642
+ this.debuglog('using cached date data')
3643
+ cache_data = this.readFileToJson(cache_file)
3644
+ }
3645
+ if (cache_data) {
3646
+ return cache_data
3647
+ }
3648
+ } catch(e) {
3649
+ this.log('getBlackoutsData error : ' + e.message)
3650
+ }
3651
+ }
3652
+
3489
3653
  // get all blackout games for a date
3490
- async get_blackout_games(games, calculate_expiries=false) {
3654
+ async get_blackout_games(gameDate='guide', calculate_expiries=false) {
3491
3655
  this.debuglog('get_blackout_games')
3492
3656
  let blackouts = {}
3493
3657
 
3494
- let usa_blackout = /(^\d{5}$)/.test(this.credentials.zip_code) && (this.credentials.country == 'USA')
3658
+ let cache_data
3659
+ cache_data = await this.getBlackoutsData(gameDate)
3660
+
3661
+ if ( cache_data && cache_data.results && (cache_data.results.length > 0) ) {
3662
+ for (var j = 0; j < cache_data.results.length; j++) {
3663
+ let game = cache_data.results[j]
3664
+ let game_pk = game.gamePk
3665
+ this.debuglog('get_blackout_games checking game ' + game_pk)
3666
+ if ( game.blackedOutVideo ) {
3667
+ this.debuglog('get_blackout_games found blackout')
3668
+ let blackout_type = ''
3669
+ // local/national blackout label disabled, as all were returning local
3670
+ /*if ( game.videoStatusCodes.includes('2') ) {
3671
+ this.debuglog('get_blackout_games found national blackout')
3672
+ blackout_type = 'National/International'
3673
+ } else {
3674
+ this.debuglog('get_blackout_games found local blackout')
3675
+ blackout_type = 'Local'
3676
+ }*/
3677
+ blackouts[game_pk] = { blackout_type: blackout_type }
3678
+ } else if ( !game.entitledVideo && (game.videoStatusCodes[0] == '3') ) {
3679
+ this.debuglog('get_blackout_games found non-entitled MVPD required blackout')
3680
+ blackouts[game_pk] = { blackout_type: '' }
3681
+ }
3495
3682
 
3496
- let regional_fox_games_exist
3497
- for (var j = 0; j < games.length; j++) {
3498
- let game_pk = games[j].gamePk.toString()
3499
- this.debuglog('get_blackout_games checking game ' + game_pk)
3500
- if ( games[j].broadcasts ) {
3501
- for (var k = 0; k < games[j].broadcasts.length; k++) {
3502
- let broadcast = games[j].broadcasts[k]
3503
- if ( broadcast.type == 'TV' ) {
3504
- this.debuglog('get_blackout_games checking feed ' + broadcast.callSign)
3505
- if ( (broadcast.isNational == true) || (await this.check_pay_tv(broadcast)) || broadcast.callSign.endsWith('-INT') ) {
3506
- this.debuglog('get_blackout_games checking national game')
3507
- // International blackouts according to https://www.mlb.com/live-stream-games/help-center/blackouts-available-games
3508
- if ( usa_blackout && (broadcast.callSign == 'FOX') ) {
3509
- if ( !regional_fox_games_exist && (games[j].seriesDescription == 'Regular Season') ) {
3510
- regional_fox_games_exist = await this.check_regional_fox_games(games)
3511
- }
3512
- if ( !regional_fox_games_exist || (regional_fox_games_exist == 'false') ) {
3513
- this.debuglog('get_blackout_games found non-regional FOX game')
3514
- blackouts[game_pk] = { blackout_type:'National' }
3515
- break
3516
- }
3517
- // Apple TV+ games are blacked out everywhere
3518
- } else if ( broadcast.callSign == 'Apple TV+' ) {
3519
- this.debuglog('get_blackout_games found Apple TV+ blackout')
3520
- blackouts[game_pk] = { blackout_type:'Full International' }
3521
- // ESPN Sunday Night games are blacked out in a list of countries
3522
- } else if ( (broadcast.callSign == 'ESPN') && (new Date(games[j].gameDate).getDay() == 0) && ESPN_SUNDAY_NIGHT_BLACKOUT_COUNTRIES.includes(this.credentials.country) ) {
3523
- this.debuglog('get_blackout_games found ESPN blackout')
3524
- blackouts[game_pk] = { blackout_type:'Partial International' }
3525
- // games with an "-INT" variant are postseason games, blacked out in USA and Canada
3526
- } else if ( broadcast.callSign.endsWith('-INT') && (usa_blackout || (this.credentials.country == 'Canada')) ) {
3527
- this.debuglog('get_blackout_games found national blackout')
3528
- blackouts[game_pk] = { blackout_type:'National' }
3529
- } else if ( usa_blackout ) {
3530
- this.debuglog('get_blackout_games found USA blackout')
3531
- blackouts[game_pk] = { blackout_type:'National' }
3532
- }
3533
- break
3534
- // check local blackouts
3535
- } else {
3536
- this.debuglog('get_blackout_games checking local game')
3537
- let awayteam = games[j].teams['away'].team.abbreviation
3538
- let hometeam = games[j].teams['home'].team.abbreviation
3539
- if ( (games[j].seriesDescription != 'Spring Training') && (this.credentials.blackout_teams.includes(hometeam) || this.credentials.blackout_teams.includes(awayteam)) ) {
3540
- this.debuglog('get_blackout_games found local blackout')
3541
- blackouts[game_pk] = { blackout_type:'Local' }
3683
+ // add blackout expiry, if requested
3684
+ if ( blackouts[game_pk] && calculate_expiries && await this.check_game_time(game.gameData) ) {
3685
+ this.debuglog('get_blackout_games calculating blackout expiry')
3686
+ let date_cache_data = await this.getDayData(gameDate)
3687
+ if ( date_cache_data.dates && date_cache_data.dates[0] && date_cache_data.dates[0].games && (date_cache_data.dates[0].games.length > 0) ) {
3688
+ for (var k = 0; k < date_cache_data.dates[0].games.length; k++) {
3689
+ if ( game_pk == date_cache_data.dates[0].games[k].gamePk ) {
3690
+ this.debuglog('get_blackout_games found matching game')
3691
+ let blackoutExpiry = await this.get_blackout_expiry(date_cache_data.dates[0].games[k])
3692
+ this.debuglog('get_blackout_games calculated blackout expiry as ' + blackoutExpiry)
3693
+ blackouts[game_pk].blackoutExpiry = blackoutExpiry
3542
3694
  break
3543
3695
  }
3544
3696
  }
3545
- break
3546
3697
  }
3547
3698
  }
3548
-
3549
- // add blackout expiry, if requested
3550
- if ( blackouts[game_pk] && calculate_expiries && await this.check_game_time(games[j]) ) {
3551
- let blackoutExpiry = await this.get_blackout_expiry(games[j])
3552
- blackouts[game_pk].blackoutExpiry = blackoutExpiry
3553
- }
3554
3699
  }
3555
3700
  }
3556
3701
 
@@ -3566,7 +3711,7 @@ class sessionClass {
3566
3711
  let start
3567
3712
  let end
3568
3713
  if ( cache_data.dates && cache_data.dates[0] && cache_data.dates[0].games && (cache_data.dates[0].games.length > 0) ) {
3569
- blackouts = await this.get_blackout_games(cache_data.dates[0].games)
3714
+ blackouts = await this.get_blackout_games(today)
3570
3715
  this.debuglog('Game changer blackouts ' + JSON.stringify(blackouts))
3571
3716
 
3572
3717
  let gameIndexes = await this.get_first_and_last_games(cache_data.dates[0].games, blackouts)
@@ -3621,8 +3766,8 @@ class sessionClass {
3621
3766
  cacheExpiry.setSeconds(cacheExpiry.getSeconds()+9)
3622
3767
  this.temp_cache.gamechangerCacheExpiry = cacheExpiry
3623
3768
  let reqObj = {
3624
- //url: 'http://gd2.mlb.com/components/game/mlb/year_' + this.temp_cache.gamechanger.dateString + '/master_scoreboard.json',
3625
- url: 'http://statsapi.mlb.com/api/v1/schedule?sportId=1&startDate=' + this.temp_cache.gamechanger.date + '&endDate=' + this.temp_cache.gamechanger.date + '&hydrate=broadcasts(all),linescore,team,flags,gameInfo',
3769
+ //url: 'https://gd2.mlb.com/components/game/mlb/year_' + this.temp_cache.gamechanger.dateString + '/master_scoreboard.json',
3770
+ url: 'https://statsapi.mlb.com/api/v1/schedule?sportId=1&startDate=' + this.temp_cache.gamechanger.date + '&endDate=' + this.temp_cache.gamechanger.date + '&hydrate=broadcasts(all),linescore,team,flags,gameInfo',
3626
3771
  headers: {
3627
3772
  'User-Agent': USER_AGENT,
3628
3773
  'Origin': 'https://www.mlb.com',
@@ -4066,7 +4211,7 @@ class sessionClass {
4066
4211
  let firstGameIndex
4067
4212
  for (var j = 0; j < games.length; j++) {
4068
4213
  let gamePk = games[j].gamePk.toString()
4069
- if ( games[j].gameDate && !blackouts[gamePk] && !games[j].rescheduleDate && !games[j].status.startTimeTBD && games[j].broadcasts && (await this.count_broadcasts(games[j].broadcasts, 'MLBTV') > 0) ) {
4214
+ if ( games[j].gameDate && (games[j].teams.home.team.sport.id == 1) && (games[j].teams.away.team.sport.id == 1) && !blackouts[gamePk] && !games[j].rescheduleDate && !games[j].status.startTimeTBD && games[j].broadcasts && (await this.count_broadcasts(games[j].broadcasts, 'MLBTV') > 0) ) {
4070
4215
  this.debuglog('get_first_and_last_games first : ' + j + ' ' + gamePk + ' at ' + games[j].gameDate)
4071
4216
  firstGameIndex = j
4072
4217
  break
@@ -4075,7 +4220,7 @@ class sessionClass {
4075
4220
  let lastGameIndex
4076
4221
  for (var j = (games.length-1); j >= 0; j--) {
4077
4222
  let gamePk = games[j].gamePk.toString()
4078
- if ( games[j].gameDate && !blackouts[gamePk] && !games[j].rescheduleDate && games[j].broadcasts && (await this.count_broadcasts(games[j].broadcasts, 'MLBTV') > 0) ) {
4223
+ if ( games[j].gameDate && (games[j].teams.home.team.sport.id == 1) && (games[j].teams.away.team.sport.id == 1) && !blackouts[gamePk] && !games[j].rescheduleDate && games[j].broadcasts && (await this.count_broadcasts(games[j].broadcasts, 'MLBTV') > 0) ) {
4079
4224
  this.debuglog('get_first_and_last_games last : ' + j + ' ' + gamePk + ' at ' + games[j].gameDate)
4080
4225
  lastGameIndex = j
4081
4226
  break
@@ -4134,8 +4279,8 @@ class sessionClass {
4134
4279
  return scheduledInnings
4135
4280
  }
4136
4281
 
4137
- async check_game_time(game) {
4138
- if ( !game.resumeGameDate && !game.resumedFromDate && (game.status.startTimeTBD == false) ) {
4282
+ async check_game_time(gameData) {
4283
+ if ( !gameData.resumeGameDate && !gameData.resumedFromDate && (gameData.startTimeTBD == false) ) {
4139
4284
  return true
4140
4285
  } else {
4141
4286
  return false
@@ -4144,20 +4289,25 @@ class sessionClass {
4144
4289
 
4145
4290
  async get_blackout_expiry(game) {
4146
4291
  let scheduledInnings = await this.get_scheduled_innings(game)
4147
- // avg 9 inning game was 3:11 in 2021, or 21.22 minutes per inning
4148
- let gameDurationMinutes = 21.22 * scheduledInnings
4292
+ this.debuglog('get_blackout_expiry scheduledInnings ' + scheduledInnings)
4293
+ // avg 9 inning game was 2:39 in 2023, or 17.66 minutes per inning
4294
+ let gameDurationMinutes = 17.66 * scheduledInnings
4149
4295
  // default to assuming the scheduled game time is the first pitch time
4150
4296
  let firstPitch = new Date(game.gameDate)
4297
+ this.debuglog('get_blackout_expiry gameDate ' + firstPitch)
4151
4298
  if ( game.gameInfo ) {
4152
4299
  // check if firstPitch has been updated with a valid time (later than the scheduled game time)
4153
4300
  if ( game.gameInfo.firstPitch && (game.gameInfo.firstPitch >= game.gameDate) ) {
4154
4301
  firstPitch = new Date(game.gameInfo.firstPitch)
4302
+ this.debuglog('get_blackout_expiry firstPitch ' + firstPitch)
4155
4303
  // for completed games, get the duration too
4156
4304
  if ( game.gameInfo.gameDurationMinutes ) {
4157
4305
  gameDurationMinutes = game.gameInfo.gameDurationMinutes
4306
+ this.debuglog('get_blackout_expiry gameDurationMinutes ' + gameDurationMinutes)
4158
4307
  // add any delays
4159
4308
  if ( game.gameInfo.delayDurationMinutes ) {
4160
4309
  gameDurationMinutes += game.gameInfo.delayDurationMinutes
4310
+ this.debuglog('get_blackout_expiry delayDurationMinutes ' + game.gameInfo.delayDurationMinutes)
4161
4311
  }
4162
4312
  }
4163
4313
  }