mlbserver 2022.5.30 → 2022.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +2 -1
  2. package/index.js +110 -33
  3. package/package.json +1 -1
  4. package/session.js +101 -23
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # mlbserver
2
2
 
3
- Current version 2022.05.30
3
+ Current version 2022.06.09
4
4
 
5
5
  Credit to https://github.com/tonycpsu/streamglob and https://github.com/mafintosh/hls-decryptor
6
6
 
@@ -34,6 +34,7 @@ Advanced command line options:
34
34
  ```
35
35
  --account_username (email address, default will use stored credentials or prompt user to enter them)
36
36
  --account_password (default will use stored credentials or prompt user to enter them)
37
+ --zip_code (optional, for USA blackout labels, will prompt if not set or stored)
37
38
  --multiview_port (port for multiview streaming; defaults to 1 more than primary port, or 10000)
38
39
  --multiview_path (where to create the folder for multiview encoded files; defaults to app directory)
39
40
  --ffmpeg_path (path to ffmpeg binary to use for multiview encoding; default downloads a binary using ffmpeg-static)
package/index.js CHANGED
@@ -55,6 +55,7 @@ const SECONDS_PER_SEGMENT = 5
55
55
  // Advanced command line arguments:
56
56
  // --account_username (email address, default will use stored credentials or prompt user to enter them)
57
57
  // --account_password (default will use stored credentials or prompt user to enter them)
58
+ // --zip_code (optional, for USA blackout labels, will prompt if not set or stored)
58
59
  // --multiview_port (port for multiview streaming; defaults to 1 more than primary port, or 10000)
59
60
  // --multiview_path (where to create the folder for multiview encoded files; defaults to app directory)
60
61
  // --ffmpeg_path (path to ffmpeg binary to use for multiview encoding; default downloads a binary using ffmpeg-static)
@@ -674,8 +675,8 @@ app.get('/playlist', async function(req, res) {
674
675
  last_segment_index--
675
676
  }
676
677
  last_segment_inf = body_array[last_segment_index]
677
- last_segment_ts = body_array[last_segment_index+1]
678
- let pad_lines = '#EXT-X-DISCONTINUITY' + '\n' + last_segment_inf + '\n' + last_segment_ts + '\n'
678
+ last_segment = body_array[last_segment_index+1]
679
+ let pad_lines = '#EXT-X-DISCONTINUITY' + '\n' + last_segment_inf + '\n' + last_segment + '\n'
679
680
  session.debuglog(pad_lines)
680
681
  for (i=0; i<pad; i++) {
681
682
  body += pad_lines
@@ -788,6 +789,7 @@ app.get('/', async function(req, res) {
788
789
  let multiview_server = server.replace(':' + session.data.port, ':' + session.data.multiviewPort)
789
790
 
790
791
  let gameDate = session.liveDate()
792
+ let today = gameDate
791
793
  let todayUTCHours = session.getTodayUTCHours()
792
794
  let curDate = new Date()
793
795
  if ( req.query.date ) {
@@ -896,7 +898,7 @@ app.get('/', async function(req, res) {
896
898
 
897
899
  // Reload function, called after options change
898
900
  // audio_url is disabled here, now used in multiview instead
899
- body += 'var defaultDate="' + session.liveDate() + '";var curDate=new Date();var utcHours=curDate.getUTCHours();if ((utcHours >= ' + todayUTCHours + ') && (utcHours < ' + YESTERDAY_UTC_HOURS + ')){defaultDate="' + session.yesterdayDate() + '"}function reload(){var newurl="/?";if (date != defaultDate){var urldate=date;if (date == "' + session.liveDate() + '"){urldate="today"}else if (date == "' + session.yesterdayDate() + '"){urldate="yesterday"}newurl+="date="+urldate+"&"}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 (audio_url != ""){newurl+="audio_url="+encodeURIComponent(audio_url)+"&"}*/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"
901
+ body += 'var defaultDate="' + today + '";var curDate=new Date();var utcHours=curDate.getUTCHours();if ((utcHours >= ' + todayUTCHours + ') && (utcHours < ' + YESTERDAY_UTC_HOURS + ')){defaultDate="' + session.yesterdayDate() + '"}function reload(){var newurl="/?";if (date != defaultDate){var urldate=date;if (date == "' + today + '"){urldate="today"}else if (date == "' + session.yesterdayDate() + '"){urldate="yesterday"}newurl+="date="+urldate+"&"}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 (audio_url != ""){newurl+="audio_url="+encodeURIComponent(audio_url)+"&"}*/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"
900
902
 
901
903
  // Ajax function for multiview and highlights
902
904
  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"
@@ -918,7 +920,7 @@ app.get('/', async function(req, res) {
918
920
  body += '<p><span class="tooltip">Date<span class="tooltiptext">"today" lasts until ' + todayUTCHours + ' AM EST. Home page will default to yesterday between ' + todayUTCHours + ' AM - ' + (YESTERDAY_UTC_HOURS - 4) + ' AM EST.</span></span>: <input type="date" id="gameDate" value="' + gameDate + '"/> '
919
921
  for (var i = 0; i < VALID_DATES.length; i++) {
920
922
  body += '<button '
921
- if ( ((VALID_DATES[i] == VALID_DATES[0]) && (gameDate == session.liveDate())) || ((VALID_DATES[i] == VALID_DATES[1]) && (gameDate == session.yesterdayDate())) ) body += 'class="default" '
923
+ if ( ((VALID_DATES[i] == VALID_DATES[0]) && (gameDate == today)) || ((VALID_DATES[i] == VALID_DATES[1]) && (gameDate == session.yesterdayDate())) ) body += 'class="default" '
922
924
  body += 'onclick="date=\'' + VALID_DATES[i] + '\';reload()">' + VALID_DATES[i] + '</button> '
923
925
  }
924
926
  body += '</p>' + "\n" + '<p><span class="tinytext">Updated ' + session.getCacheUpdatedDate(gameDate) + '</span></p>' + "\n"
@@ -962,7 +964,7 @@ app.get('/', async function(req, res) {
962
964
  }
963
965
 
964
966
  if ( mediaType == VALID_MEDIA_TYPES[0] ) {
965
- body += '<span class="tooltip">Inning<span class="tooltiptext">For video streams only: choose the inning to start with (and the score to display, if applicable). Inning number is relative -- for example, selecting inning 7 here will show inning 7 for scheduled 9 inning games, but inning 5 for scheduled 7 inning games, for example. If an inning number is specified, seeking to an earlier point will not be possible. Inning 0 (zero) should be the broadcast start time, if available. Default is the beginning of the stream. To use with radio, set the video track to "None".</span></span>: '
967
+ body += '<span class="tooltip">Inning<span class="tooltiptext">For video streams only: choose the inning to start with (and the score to display, if applicable). Inning number is relative -- for example, selecting inning 7 here will show inning 7 for scheduled 9-inning games, but inning 5 for scheduled 7-inning games, for example. If an inning number is specified, seeking to an earlier point will not be possible. Inning 0 (zero) should be the broadcast start time, if available. Default is the beginning of the stream. To use with radio, set the video track to "None".</span></span>: '
966
968
  body += '<select id="inning_half" onchange="inning_half=this.value;reload()">'
967
969
  for (var i = 0; i < VALID_INNING_HALF.length; i++) {
968
970
  body += '<option value="' + VALID_INNING_HALF[i] + '"'
@@ -1043,9 +1045,14 @@ app.get('/', async function(req, res) {
1043
1045
  body += '</td></tr>' + "\n"
1044
1046
  }
1045
1047
 
1048
+ let national_blackout = /(^\d{5}$)/.test(session.credentials.zip_code)
1049
+
1046
1050
  for (var j = 0; j < cache_data.dates[0].games.length; j++) {
1047
1051
  let game_started = false
1048
1052
 
1053
+ let blackout_type
1054
+ let blackout_time
1055
+
1049
1056
  let awayteam = cache_data.dates[0].games[j].teams['away'].team.abbreviation
1050
1057
  let hometeam = cache_data.dates[0].games[j].teams['home'].team.abbreviation
1051
1058
  if ( cache_data.dates[0].games[j].teams['home'].team.sport.name == 'Winter Leagues' ) {
@@ -1070,10 +1077,14 @@ app.get('/', async function(req, res) {
1070
1077
  var scheduledInnings = '9'
1071
1078
  if ( cache_data.dates[0].games[j].linescore && cache_data.dates[0].games[j].linescore.scheduledInnings ) {
1072
1079
  scheduledInnings = cache_data.dates[0].games[j].linescore.scheduledInnings
1080
+ if ( detailedState.startsWith('Completed Early') && cache_data.dates[0].games[j].linescore.currentInning ) {
1081
+ scheduledInnings = cache_data.dates[0].games[j].linescore.currentInning
1082
+ }
1073
1083
  }
1074
1084
  var relative_inning = (inning_number - (9 - scheduledInnings))
1075
1085
  relative_inning = relative_inning < 0 ? 0 : relative_inning
1076
- if ( (scores == VALID_SCORES[1]) && (cache_data.dates[0].games[j].gameUtils.isLive || cache_data.dates[0].games[j].gameUtils.isFinal) && !cache_data.dates[0].games[j].gameUtils.isCancelled && !cache_data.dates[0].games[j].gameUtils.isPostponed ) {
1086
+ //if ( (scores == VALID_SCORES[1]) && (cache_data.dates[0].games[j].gameUtils.isLive || cache_data.dates[0].games[j].gameUtils.isFinal) && !cache_data.dates[0].games[j].gameUtils.isCancelled && !cache_data.dates[0].games[j].gameUtils.isPostponed ) {
1087
+ if ( (scores == VALID_SCORES[1]) && (abstractGameState != 'Preview') && (detailedState != 'Postponed') ) {
1077
1088
  let awayscore = ''
1078
1089
  let homescore = ''
1079
1090
  if ( (inning_number != VALID_INNING_NUMBER[0]) && cache_data.dates[0].games[j].linescore && cache_data.dates[0].games[j].linescore.innings ) {
@@ -1110,9 +1121,11 @@ app.get('/', async function(req, res) {
1110
1121
  } else {
1111
1122
  awayscore = cache_data.dates[0].games[j].teams['away'].score
1112
1123
  homescore = cache_data.dates[0].games[j].teams['home'].score
1113
- if ( cache_data.dates[0].games[j].gameUtils.isLive && !cache_data.dates[0].games[j].gameUtils.isFinal ) {
1124
+ //if ( cache_data.dates[0].games[j].gameUtils.isLive && !cache_data.dates[0].games[j].gameUtils.isFinal ) {
1125
+ if ( abstractGameState == 'Live' ) {
1114
1126
  state = "<br/>" + cache_data.dates[0].games[j].linescore.inningHalf.substr(0,1) + cache_data.dates[0].games[j].linescore.currentInning
1115
- } else if ( cache_data.dates[0].games[j].gameUtils.isFinal ) {
1127
+ //} else if ( cache_data.dates[0].games[j].gameUtils.isFinal ) {
1128
+ } else if ( abstractGameState == 'Final' ) {
1116
1129
  state = "<br/>" + detailedState
1117
1130
  }
1118
1131
  if ( cache_data.dates[0].games[j].flags.perfectGame == true ) {
@@ -1122,9 +1135,11 @@ app.get('/', async function(req, res) {
1122
1135
  }
1123
1136
  }
1124
1137
  teams = awayteam + " " + awayscore + " @ " + hometeam + " " + homescore
1125
- } else if ( cache_data.dates[0].games[j].gameUtils.isCancelled || cache_data.dates[0].games[j].gameUtils.isPostponed || cache_data.dates[0].games[j].gameUtils.isSuspended ) {
1138
+ //} else if ( cache_data.dates[0].games[j].gameUtils.isCancelled || cache_data.dates[0].games[j].gameUtils.isPostponed || cache_data.dates[0].games[j].gameUtils.isSuspended ) {
1139
+ } else if ( detailedState == 'Postponed' ) {
1126
1140
  state = "<br/>" + detailedState
1127
- } else if ( cache_data.dates[0].games[j].gameUtils.isDelayed ) {
1141
+ //} else if ( cache_data.dates[0].games[j].gameUtils.isDelayed ) {
1142
+ } else if ( detailedState.startsWith('Delayed') ) {
1128
1143
  state += "<br/>" + detailedState
1129
1144
  }
1130
1145
  if ( cache_data.dates[0].games[j].teams['home'].team.sport.name == 'Winter Leagues' ) {
@@ -1138,19 +1153,20 @@ app.get('/', async function(req, res) {
1138
1153
  state += "<br/>" + cache_data.dates[0].games[j].description
1139
1154
  }
1140
1155
  if ( scheduledInnings != '9' ) {
1141
- state += "<br/>" + cache_data.dates[0].games[j].linescore.scheduledInnings + " inning game"
1156
+ state += "<br/>" + scheduledInnings + "-inning game"
1142
1157
  }
1143
1158
  var resumeStatus = false
1144
1159
  if ( cache_data.dates[0].games[j].resumeGameDate || cache_data.dates[0].games[j].resumedFromDate ) {
1160
+ state += '<br/>Resum'
1145
1161
  let resumeDate
1146
1162
  if ( cache_data.dates[0].games[j].resumeGameDate ) {
1147
- resumeDate = new Date(cache_data.dates[0].games[j].resumeGameDate)
1148
- state += "<br/>Resuming on<br/>"
1163
+ resumeDate = new Date(cache_data.dates[0].games[j].resumeDate)
1164
+ state += 'ing on'
1149
1165
  } else {
1150
- resumeDate = new Date(cache_data.dates[0].games[j].resumedFromDate)
1151
- state += "<br/>Resumed from<br/>"
1166
+ resumeDate = new Date(cache_data.dates[0].games[j].resumedFrom)
1167
+ state += 'ed from'
1152
1168
  }
1153
- state += resumeDate.toLocaleString('default', { month: 'long' }) + " " + (resumeDate.getDate()+1)
1169
+ state += '<br/>' + resumeDate.toLocaleString('default', { month: 'long', day: 'numeric' })
1154
1170
  // Also show the status by the media links, if one of them is live
1155
1171
  resumeStatus = 'archived'
1156
1172
  if ( ((typeof cache_data.dates[0].games[j].content.media) != 'undefined') || ((typeof cache_data.dates[0].games[j].content.media.epg) != 'undefined') ) {
@@ -1224,13 +1240,15 @@ app.get('/', async function(req, res) {
1224
1240
  if ( epgTitle == mediaType ) {
1225
1241
  for (var x = 0; x < cache_data.dates[0].games[j].content.media.epg[k].items.length; x++) {
1226
1242
  // check that pay TV authentication isn't required
1227
- if ( (mediaType == 'MLBTV') && ((cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaState != 'MEDIA_ARCHIVE') || !cache_data.dates[0].games[j].gameUtils.isFinal) && (cache_data.dates[0].games[j].content.media.epg[k].items[x].foxAuthRequired || cache_data.dates[0].games[j].content.media.epg[k].items[x].tbsAuthRequired || cache_data.dates[0].games[j].content.media.epg[k].items[x].espnAuthRequired || cache_data.dates[0].games[j].content.media.epg[k].items[x].fs1AuthRequired || cache_data.dates[0].games[j].content.media.epg[k].items[x].mlbnAuthRequired) ) {
1243
+ //if ( (mediaType == 'MLBTV') && ((cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaState != 'MEDIA_ARCHIVE') || !cache_data.dates[0].games[j].gameUtils.isFinal) && (cache_data.dates[0].games[j].content.media.epg[k].items[x].foxAuthRequired || cache_data.dates[0].games[j].content.media.epg[k].items[x].tbsAuthRequired || cache_data.dates[0].games[j].content.media.epg[k].items[x].espnAuthRequired || cache_data.dates[0].games[j].content.media.epg[k].items[x].fs1AuthRequired || cache_data.dates[0].games[j].content.media.epg[k].items[x].mlbnAuthRequired) ) {
1244
+ if ( cache_data.dates[0].games[j].content.media.epg[k].items[x].foxAuthRequired || cache_data.dates[0].games[j].content.media.epg[k].items[x].tbsAuthRequired || cache_data.dates[0].games[j].content.media.epg[k].items[x].espnAuthRequired || cache_data.dates[0].games[j].content.media.epg[k].items[x].fs1AuthRequired || cache_data.dates[0].games[j].content.media.epg[k].items[x].mlbnAuthRequired ) {
1228
1245
  continue
1229
1246
  }
1230
1247
  if ( ((typeof cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaFeedType) == 'undefined') || (cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaFeedType.indexOf('IN_MARKET_') == -1) ) {
1231
1248
  if ( ((typeof cache_data.dates[0].games[j].content.media.epg[k].items[x].language) == 'undefined') || (cache_data.dates[0].games[j].content.media.epg[k].items[x].language == language) ) {
1232
1249
  let teamabbr
1233
- if ( (((typeof cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaFeedType) != 'undefined') && (cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaFeedType == 'NATIONAL')) || ((mediaType == 'MLBTV') && cache_data.dates[0].games[j].gameUtils.isPostSeason) ) {
1250
+ //if ( (((typeof cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaFeedType) != 'undefined') && (cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaFeedType == 'NATIONAL')) || ((mediaType == 'MLBTV') && cache_data.dates[0].games[j].gameUtils.isPostSeason) ) {
1251
+ if ( (((typeof cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaFeedType) != 'undefined') && (cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaFeedType == 'NATIONAL')) || ((mediaType == 'MLBTV') && (cache_data.dates[0].games[j].seriesDescription != 'Regular Season') && (cache_data.dates[0].games[j].seriesDescription != 'Spring Training')) ) {
1234
1252
  teamabbr = 'NATIONAL'
1235
1253
  } else {
1236
1254
  teamabbr = hometeam
@@ -1242,15 +1260,61 @@ app.get('/', async function(req, res) {
1242
1260
  if ( cache_data.dates[0].games[j].content.media.freeGame ) {
1243
1261
  station += '*'
1244
1262
  }
1245
- if ( (cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaState == 'MEDIA_ON') || (cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaState == 'MEDIA_ARCHIVE') || cache_data.dates[0].games[j].gameUtils.isFinal ) {
1263
+ // estimate blackout expiration, if necessary
1264
+ if ( !blackout_type && (mediaType == 'MLBTV') && (gameDate >= today) && ((national_blackout && (teamabbr == 'NATIONAL')) || ((cache_data.dates[0].games[j].seriesDescription != 'Spring Training') && (session.credentials.blackout_teams.includes(awayteam) || session.credentials.blackout_teams.includes(hometeam)))) ) {
1265
+ if ( national_blackout && (teamabbr == 'NATIONAL') ) {
1266
+ blackout_type = 'National'
1267
+ } else {
1268
+ blackout_type = 'Local'
1269
+ }
1270
+ if ( (resumeStatus == false) && (cache_data.dates[0].games[j].status.startTimeTBD == false) ) {
1271
+ // avg 9 inning game was 3:11 in 2021, or 21.22 minutes per inning
1272
+ let gameDurationMinutes = 21.22 * scheduledInnings
1273
+ // default to assuming the scheduled game time is the first pitch time
1274
+ let firstPitch = new Date(cache_data.dates[0].games[j].gameDate)
1275
+ if ( cache_data.dates[0].games[j].gameInfo ) {
1276
+ // check if firstPitch has been updated with a valid time (later than the scheduled game time)
1277
+ if ( cache_data.dates[0].games[j].gameInfo.firstPitch && (cache_data.dates[0].games[j].gameInfo.firstPitch >= cache_data.dates[0].games[j].gameDate) ) {
1278
+ firstPitch = new Date(cache_data.dates[0].games[j].gameInfo.firstPitch)
1279
+ // for completed games, get the duration too
1280
+ if ( cache_data.dates[0].games[j].gameInfo.gameDurationMinutes ) {
1281
+ gameDurationMinutes = cache_data.dates[0].games[j].gameInfo.gameDurationMinutes
1282
+ // add any delays
1283
+ if ( cache_data.dates[0].games[j].gameInfo.delayDurationMinutes ) {
1284
+ gameDurationMinutes += cache_data.dates[0].games[j].gameInfo.delayDurationMinutes
1285
+ }
1286
+ }
1287
+ }
1288
+ }
1289
+ gameDurationMinutes += 90
1290
+ blackout_time = firstPitch
1291
+ blackout_time.setMinutes(blackout_time.getMinutes()+gameDurationMinutes)
1292
+ }
1293
+ } else if ( !blackout_type ) {
1294
+ blackout_type = 'None'
1295
+ }
1296
+
1297
+ // display blackout tooltip, if necessary
1298
+ if ( blackout_type != 'None' ) {
1299
+ body += '<span class="tooltip">' + teamabbr + '<span class="tooltiptext">' + blackout_type + ' video blackout until approx. 90 min. after the game'
1300
+ if ( blackout_time ) {
1301
+ body += ' (~' + blackout_time.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + ')'
1302
+ }
1303
+ body += '</span></span>'
1304
+ } else {
1305
+ body += teamabbr
1306
+ }
1307
+ body += ': '
1308
+ //if ( (cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaState == 'MEDIA_ON') || (cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaState == 'MEDIA_ARCHIVE') || cache_data.dates[0].games[j].gameUtils.isFinal ) {
1309
+ if ( (cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaState == 'MEDIA_ON') || (cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaState == 'MEDIA_ARCHIVE') || (abstractGameState == 'Final') ) {
1246
1310
  let gameTime = new Date(cache_data.dates[0].games[j].gameDate)
1247
1311
  gameTime.setMinutes(gameTime.getMinutes()-10)
1248
1312
  if ( curDate >= gameTime ) {
1249
1313
  game_started = true
1250
1314
  }
1251
1315
  let mediaId = cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaId
1252
- if ( (mediaType == 'MLBTV') && session.cache.media && session.cache.media[mediaId] && session.cache.media[mediaId].blackout && session.cache.media[mediaId].blackoutExpiry && (new Date(session.cache.media[mediaId].blackoutExpiry) > new Date()) ) {
1253
- body += teamabbr + ': <s>' + station + '</s>'
1316
+ if ( (mediaType == 'MLBTV') && (gameDate == today) && session.cache.media && session.cache.media[mediaId] && session.cache.media[mediaId].blackout && session.cache.media[mediaId].blackoutExpiry && (new Date(session.cache.media[mediaId].blackoutExpiry) > new Date()) ) {
1317
+ body += '<s>' + station + '</s>'
1254
1318
  } else {
1255
1319
  let querystring
1256
1320
  querystring = '?mediaId=' + mediaId
@@ -1280,7 +1344,12 @@ app.get('/', async function(req, res) {
1280
1344
  }
1281
1345
  querystring += content_protect_b
1282
1346
  multiviewquerystring += content_protect_b
1283
- body += teamabbr + ': <a href="' + thislink + querystring + '">' + station + '</a>'
1347
+ stationlink = '<a href="' + thislink + querystring + '">' + station + '</a>'
1348
+ if ( (mediaType == 'MLBTV') && (gameDate >= today) && ((national_blackout && (teamabbr == 'NATIONAL')) || ((cache_data.dates[0].games[j].seriesDescription != 'Spring Training') && (session.credentials.blackout_teams.includes(awayteam) || session.credentials.blackout_teams.includes(hometeam)))) ) {
1349
+ body += '<s>' + stationlink + '</s>'
1350
+ } else {
1351
+ body += stationlink
1352
+ }
1284
1353
  if ( mediaType == 'MLBTV' ) {
1285
1354
  body += '<input type="checkbox" value="' + server + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)">'
1286
1355
  }
@@ -1312,13 +1381,18 @@ app.get('/', async function(req, res) {
1312
1381
  body += ')'
1313
1382
  }
1314
1383
  body += ', '
1315
- // add YouTube link where available
1316
- if ( (mediaType == 'MLBTV') && cache_data.dates[0].games[j].content.media.epg[k].items[x].youtube && cache_data.dates[0].games[j].content.media.epg[k].items[x].youtube.videoId ) {
1317
- body += '<a href="https://www.youtube.com/watch?v=' + cache_data.dates[0].games[j].content.media.epg[k].items[x].youtube.videoId + '" target="_blank">' + station + '</a>, '
1318
- }
1319
1384
  }
1320
1385
  } else {
1321
- body += teamabbr + ': ' + station + ', '
1386
+ if ( (mediaType == 'MLBTV') && (gameDate >= today) && ((national_blackout && (teamabbr == 'NATIONAL')) || ((cache_data.dates[0].games[j].seriesDescription != 'Spring Training') && (session.credentials.blackout_teams.includes(awayteam) || session.credentials.blackout_teams.includes(hometeam)))) ) {
1387
+ body += '<s>' + station + '</s>'
1388
+ } else {
1389
+ body += station
1390
+ }
1391
+ body += ', '
1392
+ }
1393
+ // add YouTube link where available
1394
+ if ( (mediaType == 'MLBTV') && cache_data.dates[0].games[j].content.media.epg[k].items[x].youtube && cache_data.dates[0].games[j].content.media.epg[k].items[x].youtube.videoId ) {
1395
+ body += '<a href="https://www.youtube.com/watch?v=' + cache_data.dates[0].games[j].content.media.epg[k].items[x].youtube.videoId + '" target="_blank">' + station + '</a>'
1322
1396
  }
1323
1397
  }
1324
1398
  }
@@ -1329,12 +1403,10 @@ app.get('/', async function(req, res) {
1329
1403
  if ( body.substr(-2) == ', ' ) {
1330
1404
  body = body.slice(0, -2)
1331
1405
  }
1332
- if ( (mediaType == 'MLBTV') && (game_started) && cache_data.dates[0].games[j].content && cache_data.dates[0].games[j].content.summary && cache_data.dates[0].games[j].content.summary.hasHighlightsVideo ) {
1406
+ //if ( (mediaType == 'MLBTV') && (game_started) && cache_data.dates[0].games[j].content && cache_data.dates[0].games[j].content.summary && cache_data.dates[0].games[j].content.summary.hasHighlightsVideo ) {
1407
+ if ( (mediaType == 'MLBTV') && (game_started) ) {
1333
1408
  body += '<br/><a href="javascript:showhighlights(\'' + cache_data.dates[0].games[j].gamePk + '\',\'' + gameDate + '\')">Highlights</a>'
1334
1409
  }
1335
- if ( body.substr(-2) == ', ' ) {
1336
- body = body.slice(0, -2)
1337
- }
1338
1410
  }
1339
1411
  body += "</td>"
1340
1412
  body += "</tr>" + "\n"
@@ -1407,7 +1479,7 @@ app.get('/', async function(req, res) {
1407
1479
  body += '</td></tr></table><br/>' + "\n"
1408
1480
  }
1409
1481
 
1410
- if ( (linkType == VALID_LINK_TYPES[1]) && (gameDate == session.liveDate()) ) {
1482
+ if ( (linkType == VALID_LINK_TYPES[1]) && (gameDate == today) ) {
1411
1483
  body += '<p><span class="tooltip">Force VOD<span class="tooltiptext">For streams only: if your client does not support seeking in mlbserver live streams, turning this on will make the stream look like a VOD stream instead, allowing the client to start at the beginning and allowing the user to seek within it. You will need to reload the stream to watch/view past the current time, though.</span></span>: '
1412
1484
  for (var i = 0; i < VALID_FORCE_VOD.length; i++) {
1413
1485
  body += '<button '
@@ -1433,7 +1505,12 @@ app.get('/', async function(req, res) {
1433
1505
 
1434
1506
  body += '<p><span class="tooltip">By team<span class="tooltiptext">Including a team will include that team\'s broadcasts, not their opponent\'s broadcasts or national TV broadcasts.</span></span>: <a href="/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=ari,atl' + content_protect_b + '">channels.m3u</a> and <a href="/guide.xml?mediaType=' + mediaType + '&includeTeams=ari,atl' + content_protect_b + '">guide.xml</a></p>' + "\n"
1435
1507
 
1436
- body += '<p><span class="tooltip">Exclude a team + national<span class="tooltiptext">This is useful for excluding games you may be blacked out from. Excluding a team will exclude every game involving that team. National refers to <a href="https://www.mlb.com/live-stream-games/national-blackout">USA national TV broadcasts</a>.</span></span>: <a href="/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&excludeTeams=ari,national' + content_protect_b + '">channels.m3u</a> and <a href="/guide.xml?mediaType=' + mediaType + '&excludeTeams=ari,national' + content_protect_b + '">guide.xml</a></p>' + "\n"
1508
+ let exclude_teams = 'ari,national'
1509
+ if ( session.credentials.blackout_teams.length > 0 ) {
1510
+ exclude_teams = session.credentials.blackout_teams.toString().toLowerCase()
1511
+ exclude_teams += ',national'
1512
+ }
1513
+ body += '<p><span class="tooltip">Exclude a team + national<span class="tooltiptext">This is useful for excluding games you may be blacked out from. Excluding a team will exclude every game involving that team. National refers to <a href="https://www.mlb.com/live-stream-games/national-blackout">USA national TV broadcasts</a>.</span></span>: <a href="/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&excludeTeams=' + exclude_teams + content_protect_b + '">channels.m3u</a> and <a href="/guide.xml?mediaType=' + mediaType + '&excludeTeams=' + exclude_teams + content_protect_b + '">guide.xml</a></p>' + "\n"
1437
1514
 
1438
1515
  body += '<p><span class="tooltip">Include (or exclude) LIDOM<span class="tooltiptext">Dominican Winter League, aka Liga de Beisbol Dominicano. Live stream only, does not support starting from the beginning or certain innings, skip options, etc.</span></span>: <a href="/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=lidom' + content_protect_b + '">channels.m3u</a> and <a href="/guide.xml?mediaType=' + mediaType + '&includeTeams=lidom' + content_protect_b + '">guide.xml</a></p>' + "\n"
1439
1516
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mlbserver",
3
- "version": "2022.05.30",
3
+ "version": "2022.06.09",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
package/session.js CHANGED
@@ -37,9 +37,10 @@ const TEAM_IDS = {'ARI':'109','ATL':'144','BAL':'110','BOS':'111','CHC':'112','C
37
37
  const BREAK_TYPES = ['Game Advisory', 'Pitching Substitution', 'Offensive Substitution', 'Defensive Sub', 'Defensive Switch', 'Runner Placed On Base']
38
38
  // These are the events to keep, in addition to the last event of each at-bat, if we're skipping pitches
39
39
  const ACTION_TYPES = ['Wild Pitch', 'Passed Ball', 'Stolen Base', 'Caught Stealing', 'Pickoff', 'Error', 'Out', 'Balk', 'Defensive Indiff']
40
- const EVENT_START_PADDING = -5
41
- const EVENT_END_PADDING = 8
42
- const MINIMUM_BREAK_DURATION = 10
40
+ const EVENT_START_PADDING = 0
41
+ const PITCH_END_PADDING = 7
42
+ const ACTION_END_PADDING = 8
43
+ const MINIMUM_BREAK_DURATION = 5
43
44
 
44
45
  class sessionClass {
45
46
  // Initialize the class
@@ -132,6 +133,20 @@ class sessionClass {
132
133
  if ( !this.data.linkType ) {
133
134
  this.setLinkType('embed')
134
135
  }
136
+
137
+ // Check if zip code was provided and if it is different from the stored one
138
+ if ( argv.zip_code && (argv.zip_code != this.credentials.zip_code) ) {
139
+ this.debuglog('updating zip code and blackout teams')
140
+ this.credentials.zip_code = argv.zip_code.toString()
141
+ this.updateBlackoutTeams()
142
+ } else {
143
+ // Prompt for zip code if it doesn't exist
144
+ if ( !this.credentials.zip_code ) {
145
+ this.debuglog('prompting for zip code')
146
+ this.credentials.zip_code = readlineSync.question('Enter 5-digit zip code (optional, for USA blackout labels): ').toString()
147
+ this.updateBlackoutTeams()
148
+ }
149
+ }
135
150
  }
136
151
 
137
152
  // Store the ports, used for generating URLs
@@ -1010,7 +1025,8 @@ class sessionClass {
1010
1025
  continue
1011
1026
  }
1012
1027
  if ( ((typeof cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaFeedType) == 'undefined') || (cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaFeedType.indexOf('IN_MARKET_') == -1) ) {
1013
- if ( (team.toUpperCase().indexOf('NATIONAL.') == 0) && ((cache_data.dates[0].games[j].content.media.epg[k].items[x][mediaFeedType] == 'NATIONAL') || ((mediaType == 'MLBTV') && cache_data.dates[0].games[j].gameUtils.isPostSeason)) ) {
1028
+ //if ( (team.toUpperCase().indexOf('NATIONAL.') == 0) && ((cache_data.dates[0].games[j].content.media.epg[k].items[x][mediaFeedType] == 'NATIONAL') || ((mediaType == 'MLBTV') && cache_data.dates[0].games[j].gameUtils.isPostSeason)) ) {
1029
+ if ( (team.toUpperCase().indexOf('NATIONAL.') == 0) && ((cache_data.dates[0].games[j].content.media.epg[k].items[x][mediaFeedType] == 'NATIONAL') || ((mediaType == 'MLBTV') && (cache_data.dates[0].games[i].seriesDescription != 'Regular Season') && (cache_data.dates[0].games[i].seriesDescription != 'Spring Training'))) ) {
1014
1030
  nationalCount += 1
1015
1031
  let nationalArray = team.split('.')
1016
1032
  if ( (nationalArray.length == 2) && (nationalArray[1] == nationalCount) ) {
@@ -1141,11 +1157,12 @@ class sessionClass {
1141
1157
  try {
1142
1158
  let cache_data
1143
1159
  let cache_name = dateString
1144
- let 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='
1160
+ //let 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='
1161
+ let url = 'http://statsapi.mlb.com/api/v1/schedule?sportId=1,51&startDate=' + dateString + '&endDate=' + dateString + '&hydrate=game(content(media(epg))),probablePitcher,linescore,team,flags,gameInfo'
1145
1162
  if ( team ) {
1146
1163
  this.debuglog('getDayData for team ' + team + ' on date ' + dateString)
1147
1164
  cache_name = team.toUpperCase() + dateString
1148
- url = 'http://statsapi.mlb.com/api/v1/schedule?sportId=1&teamId=' + TEAM_IDS[team.toUpperCase()] + '&startDate=' + dateString + '&endDate=' + dateString + '&gameType=&gamePk=&hydrate=team,game(content(media(epg)))'
1165
+ url = 'http://statsapi.mlb.com/api/v1/schedule?sportId=1&teamId=' + TEAM_IDS[team.toUpperCase()] + '&startDate=' + dateString + '&endDate=' + dateString + '&hydrate=team,game(content(media(epg)))'
1149
1166
  } else {
1150
1167
  this.debuglog('getDayData for date ' + dateString)
1151
1168
  }
@@ -1292,6 +1309,7 @@ class sessionClass {
1292
1309
  var channels = {}
1293
1310
  var nationalChannels = {}
1294
1311
  let prevDateIndex = {MLBTV:-1,Audio:-1}
1312
+ let national_blackout = /(^\d{5}$)/.test(this.credentials.zip_code)
1295
1313
  for (var i = 0; i < cache_data.dates.length; i++) {
1296
1314
  let dateIndex = {MLBTV:i,Audio:i}
1297
1315
  let nationalCounter = {MLBTV:0,Audio:0}
@@ -1333,15 +1351,19 @@ class sessionClass {
1333
1351
  }
1334
1352
  if ( ((((typeof cache_data.dates[i].games[j].content.media.epg[k].items[x].mediaFeedType) == 'undefined') || (cache_data.dates[i].games[j].content.media.epg[k].items[x].mediaFeedType.indexOf('IN_MARKET_') == -1)) && (((typeof cache_data.dates[i].games[j].content.media.epg[k].items[x].language) == 'undefined') || (cache_data.dates[i].games[j].content.media.epg[k].items[x].language != 'es'))) ) {
1335
1353
  let teamType = cache_data.dates[i].games[j].content.media.epg[k].items[x][mediaFeedType]
1336
- if ( (mediaType == 'MLBTV') && cache_data.dates[i].games[j].gameUtils.isPostSeason ) {
1354
+ //if ( (mediaType == 'MLBTV') && cache_data.dates[i].games[j].gameUtils.isPostSeason ) {
1355
+ if ( (mediaType == 'MLBTV') && (cache_data.dates[i].games[j].seriesDescription != 'Regular Season') && (cache_data.dates[i].games[j].seriesDescription != 'Spring Training') ) {
1337
1356
  teamType = 'NATIONAL'
1338
1357
  }
1339
- let team
1340
- let opponent_team
1341
- if ( teamType == 'NATIONAL' ) {
1342
- team = cache_data.dates[i].games[j].teams['home'].team.abbreviation
1343
- opponent_team = cache_data.dates[i].games[j].teams['away'].team.abbreviation
1358
+ let team = cache_data.dates[i].games[j].teams['home'].team.abbreviation
1359
+ let opponent_team = cache_data.dates[i].games[j].teams['away'].team.abbreviation
1344
1360
 
1361
+ // check blackout status
1362
+ if ( (mediaType == 'MLBTV') && ((national_blackout && (teamType == 'NATIONAL')) || ((cache_data.dates[i].games[j].seriesDescription != 'Spring Training') && (this.credentials.blackout_teams.includes(team) || this.credentials.blackout_teams.includes(opponent_team)))) ) {
1363
+ continue
1364
+ }
1365
+
1366
+ if ( teamType == 'NATIONAL' ) {
1345
1367
  if ( dateIndex[mediaTitle] > prevDateIndex[mediaTitle] ) {
1346
1368
  prevDateIndex[mediaTitle] = dateIndex[mediaTitle]
1347
1369
  nationalCounter[mediaTitle] = 1
@@ -1480,6 +1502,7 @@ class sessionClass {
1480
1502
  var channels = {}
1481
1503
  var programs = ""
1482
1504
  let prevDateIndex = {MLBTV:-1,Audio:-1}
1505
+ let national_blackout = /(^\d{5}$)/.test(this.credentials.zip_code)
1483
1506
  for (var i = 0; i < cache_data.dates.length; i++) {
1484
1507
  let dateIndex = {MLBTV:i,Audio:i}
1485
1508
  let nationalCounter = {MLBTV:0,Audio:0}
@@ -1552,15 +1575,19 @@ class sessionClass {
1552
1575
  }
1553
1576
  if ( ((((typeof cache_data.dates[i].games[j].content.media.epg[k].items[x].mediaFeedType) == 'undefined') || (cache_data.dates[i].games[j].content.media.epg[k].items[x].mediaFeedType.indexOf('IN_MARKET_') == -1)) && (((typeof cache_data.dates[i].games[j].content.media.epg[k].items[x].language) == 'undefined') || (cache_data.dates[i].games[j].content.media.epg[k].items[x].language != 'es'))) ) {
1554
1577
  let teamType = cache_data.dates[i].games[j].content.media.epg[k].items[x][mediaFeedType]
1555
- if ( (mediaType == 'MLBTV') && cache_data.dates[i].games[j].gameUtils.isPostSeason ) {
1578
+ //if ( (mediaType == 'MLBTV') && cache_data.dates[i].games[j].gameUtils.isPostSeason ) {
1579
+ if ( (mediaType == 'MLBTV') && (cache_data.dates[i].games[j].seriesDescription != 'Regular Season') && (cache_data.dates[i].games[j].seriesDescription != 'Spring Training') ) {
1556
1580
  teamType = 'NATIONAL'
1557
1581
  }
1558
- let team
1559
- let opponent_team
1560
- if ( teamType == 'NATIONAL' ) {
1561
- team = cache_data.dates[i].games[j].teams['home'].team.abbreviation
1562
- opponent_team = cache_data.dates[i].games[j].teams['away'].team.abbreviation
1582
+ let team = cache_data.dates[i].games[j].teams['home'].team.abbreviation
1583
+ let opponent_team = cache_data.dates[i].games[j].teams['away'].team.abbreviation
1563
1584
 
1585
+ // check blackout status
1586
+ if ( (mediaType == 'MLBTV') && ((national_blackout && (teamType == 'NATIONAL')) || ((cache_data.dates[i].games[j].seriesDescription != 'Spring Training') && (this.credentials.blackout_teams.includes(team) || this.credentials.blackout_teams.includes(opponent_team)))) ) {
1587
+ continue
1588
+ }
1589
+
1590
+ if ( teamType == 'NATIONAL' ) {
1564
1591
  if ( dateIndex[mediaTitle] > prevDateIndex[mediaTitle] ) {
1565
1592
  prevDateIndex[mediaTitle] = dateIndex[mediaTitle]
1566
1593
  nationalCounter[mediaTitle] = 1
@@ -2030,22 +2057,26 @@ class sessionClass {
2030
2057
  if ((current_inning > start_inning) || ((current_inning == start_inning) && ((current_inning_half == start_inning_half) || (current_inning_half == 'bottom')))) {
2031
2058
  // loop through events within each play
2032
2059
  for (var j=0; j < cache_data.liveData.plays.allPlays[i].playEvents.length; j++) {
2060
+ let event_end_padding = ACTION_END_PADDING
2033
2061
  // always exclude break types
2034
2062
  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)) {
2035
2063
  // if we're in the process of skipping inning breaks, treat the first break type we find as another inning break
2036
2064
  if ((skip_type == 1) && (previous_inning > 0)) {
2037
- break_start = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[j].startTime) - broadcast_start_timestamp) / 1000) + EVENT_END_PADDING
2065
+ break_start = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[j].startTime) - broadcast_start_timestamp) / 1000) + event_end_padding
2038
2066
  previous_inning = 0
2039
2067
  }
2040
2068
  continue
2041
2069
  } else {
2070
+ 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))) ) {
2071
+ event_end_padding = PITCH_END_PADDING
2072
+ }
2042
2073
  let action_index
2043
2074
  // skip type 1 (breaks) && 2 (idle time) will look at all plays with an endTime
2044
2075
  if ((skip_type <= 2) && cache_data.liveData.plays.allPlays[i].playEvents[j].endTime) {
2045
2076
  action_index = j
2046
2077
  } else if (skip_type == 3) {
2047
2078
  // skip type 3 excludes non-action pitches (events that aren't last in the at-bat and don't fall under action types)
2048
- 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))) ) {
2079
+ if ( event_end_padding == PITCH_END_PADDING ) {
2049
2080
  continue
2050
2081
  } else {
2051
2082
  // if the action is associated with another play or the event doesn't have an end time, use the previous event instead
@@ -2074,10 +2105,21 @@ class sessionClass {
2074
2105
  break
2075
2106
  }
2076
2107
  }
2077
- break_start = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[action_index].endTime) - broadcast_start_timestamp) / 1000) + EVENT_END_PADDING
2108
+ break_start = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[action_index].endTime) - broadcast_start_timestamp) / 1000) + event_end_padding
2078
2109
  // add extra padding for overturned review plays
2079
- if (cache_data.liveData.plays.allPlays[i].reviewDetails && (cache_data.liveData.plays.allPlays[i].reviewDetails.isOverturned == true)) {
2080
- break_start += 40
2110
+ if ( cache_data.liveData.plays.allPlays[i].reviewDetails ) {
2111
+ let isOverturned = cache_data.liveData.plays.allPlays[i].reviewDetails.isOverturned
2112
+ if ( (isOverturned == false) && cache_data.liveData.plays.allPlays[i].reviewDetails.additionalReviews && (cache_data.liveData.plays.allPlays[i].reviewDetails.additionalReviews.length > 0) ) {
2113
+ for (var k=0; k < cache_data.liveData.plays.allPlays[i].reviewDetails.additionalReviews.length; k++) {
2114
+ isOverturned = cache_data.liveData.plays.allPlays[i].reviewDetails.additionalReviews[k].isOverturned
2115
+ if ( isOverturned == true ) {
2116
+ break
2117
+ }
2118
+ }
2119
+ }
2120
+ if (isOverturned) {
2121
+ break_start += 40
2122
+ }
2081
2123
  }
2082
2124
  }
2083
2125
  }
@@ -2351,6 +2393,42 @@ class sessionClass {
2351
2393
  }
2352
2394
  }
2353
2395
 
2396
+ // Update blackout teams
2397
+ async updateBlackoutTeams() {
2398
+ this.debuglog('getBlackoutTeams for zip code ' + this.credentials.zip_code)
2399
+
2400
+ let blackout_teams = []
2401
+
2402
+ try {
2403
+ if ( /(^\d{5}$)/.test(this.credentials.zip_code) ) {
2404
+ let reqObj = {
2405
+ url: 'https://content.mlb.com/data/blackouts/' + this.credentials.zip_code + '.json',
2406
+ headers: {
2407
+ 'User-Agent': USER_AGENT,
2408
+ 'Origin': 'https://www.mlb.com',
2409
+ 'Referer': 'https://www.mlb.com/'
2410
+ }
2411
+ }
2412
+ var response = await this.httpGet(reqObj)
2413
+ if ( this.isValidJson(response) ) {
2414
+ this.debuglog('getBlackoutTeams response : ' + response)
2415
+ let obj = JSON.parse(response)
2416
+ if ( obj.teams ) {
2417
+ blackout_teams = obj.teams
2418
+ }
2419
+ } else {
2420
+ this.log('error : invalid json from url ' + reqObj.url)
2421
+ }
2422
+ }
2423
+ } catch(e) {
2424
+ this.log('getBlackoutTeams error : ' + e.message)
2425
+ }
2426
+
2427
+ this.log('setting blackout teams to ' + JSON.stringify(blackout_teams))
2428
+ this.credentials.blackout_teams = blackout_teams
2429
+ this.save_credentials()
2430
+ }
2431
+
2354
2432
  }
2355
2433
 
2356
2434
  module.exports = sessionClass