mlbserver 2025.4.2 → 2025.4.3

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 +44 -21
  2. package/package.json +1 -1
  3. package/session.js +194 -47
package/index.js CHANGED
@@ -1564,6 +1564,7 @@ app.get('/', async function(req, res) {
1564
1564
  var thislink = http_root + '/' + link
1565
1565
 
1566
1566
  let blackouts = {}
1567
+ let pre_post_shows = {}
1567
1568
 
1568
1569
  let currentDate = new Date()
1569
1570
 
@@ -1646,6 +1647,13 @@ app.get('/', async function(req, res) {
1646
1647
  session.debuglog('SNY detect error : ' + e.message)
1647
1648
  }
1648
1649
 
1650
+ if ( cache_data.dates && cache_data.dates[0] && cache_data.dates[0].games && (cache_data.dates[0].games.length > 0) ) {
1651
+ blackouts = await session.get_blackout_games(cache_data.dates[0].date, true)
1652
+ if ( gameDate >= today ) {
1653
+ pre_post_shows = await session.get_pre_post_shows(cache_data.dates[0].date)
1654
+ }
1655
+ }
1656
+
1649
1657
  if ( (mediaType == 'MLBTV') && ((level_ids == levels['MLB']) || level_ids.startsWith(levels['MLB'] + ',')) ) {
1650
1658
  // Recap Rundown beginning in 2023, disabled because it stopped working
1651
1659
  /*if ( (gameDate <= yesterday) && (gameDate >= '2023-03-31') && cache_data.dates && cache_data.dates[0] && cache_data.dates[0].games && (cache_data.dates[0].games.length > 0) ) {
@@ -1661,13 +1669,9 @@ app.get('/', async function(req, res) {
1661
1669
  body += '</td></tr>' + "\n"
1662
1670
  }*/
1663
1671
 
1664
- if ( cache_data.dates && cache_data.dates[0] && cache_data.dates[0].games && (cache_data.dates[0].games.length > 0) ) {
1665
- blackouts = await session.get_blackout_games(cache_data.dates[0].date, true)
1666
- }
1667
-
1668
1672
  // Big Inning
1669
1673
  var big_inning
1670
- if ( cache_data.dates && cache_data.dates[0] && (cache_data.dates[0].date >= today) && cache_data.dates[0].games && (cache_data.dates[0].games.length > 1) && cache_data.dates[0].games[0] && (cache_data.dates[0].games[0].seriesDescription == 'Regular Season') ) {
1674
+ if ( (entitlements.length > 0) && cache_data.dates && cache_data.dates[0] && (cache_data.dates[0].date >= today) && cache_data.dates[0].games && (cache_data.dates[0].games.length > 1) && cache_data.dates[0].games[0] && (cache_data.dates[0].games[0].seriesDescription == 'Regular Season') ) {
1671
1675
  // Scraped Big Inning schedule
1672
1676
  big_inning = await session.getBigInningSchedule(gameDate)
1673
1677
 
@@ -2026,9 +2030,16 @@ app.get('/', async function(req, res) {
2026
2030
  }
2027
2031
  }
2028
2032
  let station = broadcast.callSign
2033
+
2034
+ if ( pre_post_shows.pregame_shows && pre_post_shows.pregame_shows[broadcast.mediaId] ) {
2035
+ station = '/' + station
2036
+ }
2037
+ if ( pre_post_shows.postgame_shows && pre_post_shows.postgame_shows[broadcast.mediaId] ) {
2038
+ station += '/'
2039
+ }
2029
2040
 
2030
2041
  // display blackout tooltip, if necessary
2031
- if ( blackouts[gamePk] ) {
2042
+ if ( blackouts[gamePk] && blackouts[gamePk].blackout_feeds && blackouts[gamePk].blackout_feeds.includes(broadcast.mediaId) ) {
2032
2043
  body += '<span class="tooltip"><span class="blackout">' + teamabbr + '</span><span class="tooltiptext">' + blackouts[gamePk].blackout_type
2033
2044
  if ( blackouts[gamePk].blackout_type != 'Not entitled' ) {
2034
2045
  body += ' video blackout until approx. 2.5 hours after the game'
@@ -2083,7 +2094,7 @@ app.get('/', async function(req, res) {
2083
2094
  multiviewquerystring += content_protect_b
2084
2095
  stationlink = '<a' + fav_style + ' href="' + thislink + querystring + '">' + station + '</a>'
2085
2096
 
2086
- if ( blackouts[gamePk] ) {
2097
+ if ( blackouts[gamePk] && blackouts[gamePk].blackout_feeds && blackouts[gamePk].blackout_feeds.includes(broadcast.mediaId) ) {
2087
2098
  body += '<span class="blackout">' + stationlink + '</span>'
2088
2099
  } else {
2089
2100
  body += stationlink
@@ -2124,7 +2135,7 @@ app.get('/', async function(req, res) {
2124
2135
  body += '<a' + fav_style + ' href="https://www.youtube.com/watch?v=' + cache_data.dates[0].games[j].content.media.epg[k].items[x].youtube.videoId + '" target="_blank">' + station + '&UpperRightArrow;</a>'
2125
2136
  }*/
2126
2137
  } else {
2127
- if ( blackouts[gamePk] ) {
2138
+ if ( blackouts[gamePk] && blackouts[gamePk].blackout_feeds && blackouts[gamePk].blackout_feeds.includes(broadcast.mediaId) ) {
2128
2139
  body += '<s>' + station + '</s>'
2129
2140
  } else {
2130
2141
  body += station
@@ -2151,7 +2162,14 @@ app.get('/', async function(req, res) {
2151
2162
  body += "</table>" + "\n"
2152
2163
 
2153
2164
  if ( (Object.keys(blackouts).length > 0) ) {
2154
- body += '<span class="tooltip tinytext"><span class="blackout">strikethrough</span> indicates a live blackout or non-entitled video<span class="tooltiptext">Tap or hover over the team abbreviation to see an estimate of when the blackout will be lifted (officially ~90 minutes, but more likely ~150 minutes or ~2.5 hours after the game ends).</span></span>' + "\n"
2165
+ body += '<span class="tooltip tinytext"><span class="blackout">strikethrough</span> indicates a live blackout or non-entitled content<span class="tooltiptext">Tap or hover over the team abbreviation to see an estimate of when the blackout will be lifted (officially ~90 minutes, but more likely ~150 minutes or ~2.5 hours after the game ends).</span></span>' + "\n"
2166
+ if ( (Object.keys(pre_post_shows).length > 0) ) {
2167
+ body += '<br/>'
2168
+ }
2169
+ }
2170
+
2171
+ if ( (pre_post_shows.pregame_shows && (Object.keys(pre_post_shows.pregame_shows).length > 0)) || (pre_post_shows.postgame_shows && (Object.keys(pre_post_shows.postgame_shows).length > 0)) ) {
2172
+ body += '<span class="tooltip tinytext">/slash/ indicates a live pre- or post-game show<span class="tooltiptext">A /slash before the station indicates a pre-game show; a slash/ after the station indicates a post-game show. Pre- and post-game shows are only available live.</span></span>' + "\n"
2155
2173
  if ( argv.free ) {
2156
2174
  body += '<br/>'
2157
2175
  }
@@ -2179,7 +2197,7 @@ app.get('/', async function(req, res) {
2179
2197
  }
2180
2198
  body += '</p>' + "\n"
2181
2199
 
2182
- body += '<p><span class="tooltip">Audio<span class="tooltiptext">For video streams only: you can manually specifiy which audio track to include. Some media players can accept them all and let you choose. Not all tracks are available for all games, and injected tracks (away radio for national games, for example) may not work with skip options below.<br/><br/>If you select "none" for video above, picking an audio track here will make it an audio-only feed that supports the inning start and skip breaks options.</span></span>: '
2200
+ body += '<p><span class="tooltip">Audio<span class="tooltiptext">For video streams only: you can manually specifiy which audio track to include. Some media players can accept them all and let you choose. Not all tracks are available for all games, and injected tracks may not work with skip options below.<br/><br/>If you select "none" for video above, picking an audio track here will make it an audio-only feed that supports the inning start and skip breaks options.</span></span>: '
2183
2201
  for (var i = 0; i < VALID_AUDIO_TRACKS.length; i++) {
2184
2202
  body += '<button '
2185
2203
  if ( audio_track == VALID_AUDIO_TRACKS[i] ) body += 'class="default" '
@@ -2256,16 +2274,16 @@ app.get('/', async function(req, res) {
2256
2274
 
2257
2275
  body += '<p><span class="tooltip">All<span class="tooltiptext">Will include all entitled live MLB broadcasts (games plus Big Inning, Game Changer, and Multiview, as well as MLB Network, SNLA, and/or SNY as appropriate). If favorite team(s) have been provided, it will also include affiliate games for those organizations. Channels/games subject to blackout will be omitted by default. See below for an additional option to override that.</span></span>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + content_protect_b + '">channels.m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + content_protect_b + '">guide.xml</a> and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + content_protect_b + '">calendar.ics</a></p>' + "\n"
2258
2276
 
2259
- let include_teams = 'ath,national'
2260
- if ( session.credentials.fav_teams.length > 0 ) {
2277
+ let include_teams = 'ath,atl'
2278
+ if ( (session.credentials.fav_teams.length > 0) && (session.credentials.fav_teams[0].length > 0) ) {
2261
2279
  include_teams = session.credentials.fav_teams.toString()
2262
2280
  }
2263
2281
  body += '<p><span class="tooltip">By team<span class="tooltiptext">Including a team (MLB only, by abbreviation, in a comma-separated list if more than 1) will include all of its broadcasts, or if that team is not broadcasting the game, it will include the national broadcast or opponent\'s broadcast if available. It will also include affiliate games for those organizations. Channels/games subject to blackout will be omitted by default. See below for an additional option to override that.</span></span>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=' + include_teams + content_protect_b + '">channels.m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=' + include_teams + content_protect_b + '">guide.xml</a> and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=' + include_teams + content_protect_b + '">calendar.ics</a></p>' + "\n"
2264
2282
 
2265
2283
  body += '<p><span class="tooltip">Include blackouts<span class="tooltiptext">An optional parameter added to the URL will include channels/games subject to blackout (although you may not be able to play those games).</span></span>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=' + include_teams + '&includeBlackouts=true' + content_protect_b + '">channels.m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&includeBlackouts=true' + content_protect_b + '">guide.xml</a> and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&includeBlackouts=true' + content_protect_b + '">calendar.ics</a></p>' + "\n"
2266
2284
 
2267
- let exclude_teams = 'ath,national'
2268
- body += '<p><span class="tooltip">Exclude a team + national<span class="tooltiptext">Excluding a team (MLB only, by abbreviation, in a comma-separated list if more than 1) 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="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&excludeTeams=' + exclude_teams + content_protect_b + '">m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&excludeTeams=' + exclude_teams + content_protect_b + '">xml</a> and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&excludeTeams=' + exclude_teams + content_protect_b + '">ics</a></p>' + "\n"
2285
+ let exclude_teams = 'ath,atl'
2286
+ body += '<p><span class="tooltip">Exclude a team<span class="tooltiptext">Excluding a team (MLB only, by abbreviation, in a comma-separated list if more than 1) will exclude every game involving that team. Note that blackouts are already excluded without the need to specify this parameter.</span></span>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&excludeTeams=' + exclude_teams + content_protect_b + '">m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&excludeTeams=' + exclude_teams + content_protect_b + '">xml</a> and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&excludeTeams=' + exclude_teams + content_protect_b + '">ics</a></p>' + "\n"
2269
2287
 
2270
2288
  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="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=lidom' + content_protect_b + '">m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=lidom' + content_protect_b + '">xml</a> and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=lidom' + content_protect_b + '">ics</a></p>' + "\n"
2271
2289
 
@@ -2301,6 +2319,8 @@ app.get('/', async function(req, res) {
2301
2319
 
2302
2320
  body += '<p><span class="tooltip">Include teams in titles<span class="tooltiptext">An optional parameter added to the URL will include team names in the ICS/XML titles.</span></span>: <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&includeTeamsInTitles=true' + content_protect_b + '">guide.xml</a> and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&includeTeamsInTitles=true' + content_protect_b + '">calendar.ics</a></p>' + "\n"
2303
2321
 
2322
+ body += '<p><span class="tooltip">Create Off Air events between games<span class="tooltiptext">An optional parameter added to the URL will create "Off Air" events in the XML guide, listing the time of the next event on that channel.</span></span>: <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&offAir=true' + content_protect_b + '">guide.xml</a></p>' + "\n"
2323
+
2304
2324
  body += '</td></tr></table><br/>' + "\n"
2305
2325
 
2306
2326
  body += '<table><tr><td>' + "\n"
@@ -2319,10 +2339,7 @@ app.get('/', async function(req, res) {
2319
2339
  ['Team live radio', '?team=' + example_team + '&mediaType=Audio'],
2320
2340
  ['Catch-up/condensed', '?team=' + example_team + '&resolution=best&skip=pitches&date=today'],
2321
2341
  ['Condensed yesterday', '?team=' + example_team + '&resolution=best&skip=pitches&date=yesterday'],
2322
- ['Same but DH game 2', '?team=' + example_team + '&resolution=best&skip=pitches&date=yesterday&game=2'],
2323
- ['Nat\'l game 1 today', '?team=NATIONAL.1&resolution=best&date=today'],
2324
- ['Same but incl. blackouts', '?team=NATIONAL.1&resolution=best&includeBlackouts=true'],
2325
- ['Nat\'l game 2 yesterday', '?team=NATIONAL.2&resolution=best&date=yesterday']
2342
+ ['Same but DH game 2', '?team=' + example_team + '&resolution=best&skip=pitches&date=yesterday&game=2']
2326
2343
  ]
2327
2344
 
2328
2345
  if ( argv.free ) {
@@ -2348,12 +2365,13 @@ app.get('/', async function(req, res) {
2348
2365
  }
2349
2366
  body += '</p>' + "\n"
2350
2367
 
2368
+ include_teams = 'ath,atl'
2351
2369
  body += '<p><span class="tooltip">Game Changer by team examples<span class="tooltiptext">Game Changer supports specifying certain teams to include or exclude. Useful for following a group of teams.</span></span>:</p>' + "\n"
2352
2370
  body += '<p>' + "\n"
2353
2371
  let gamechanger_streamURL = server + '/gamechanger.m3u8?resolution=best' + content_protect_b
2354
2372
  let gamechanger_types = ['in', 'ex']
2355
2373
  for (var i=0; i<gamechanger_types.length; i++) {
2356
- let example_streamURL = gamechanger_streamURL + '&' + gamechanger_types[i] + 'cludeTeams=ath'
2374
+ let example_streamURL = gamechanger_streamURL + '&' + gamechanger_types[i] + 'cludeTeams=' + include_teams
2357
2375
  body += '&bull; ' + gamechanger_types[i] + 'clude: <a href="' + http_root + '/embed.html?src=' + encodeURIComponent(example_streamURL) + '&startFrom=' + VALID_START_FROM[1] + content_protect_b + '">Embed</a> | <a href="' + example_streamURL + '">Stream</a> | <a href="' + http_root + '/chromecast.html?src=' + encodeURIComponent(example_streamURL) + content_protect_b + '">Chromecast</a> | <a href="' + http_root + '/advanced.html?src=' + encodeURIComponent(example_streamURL) + content_protect_b + '">Advanced</a> | <a href="' + http_root + '/kodi.strm?src=' + encodeURIComponent(example_streamURL) + content_protect_b + '">Kodi</a><br/>' + "\n"
2358
2376
  }
2359
2377
 
@@ -2571,7 +2589,7 @@ app.get('/channels.m3u', async function(req, res) {
2571
2589
  includeOrgs = req.query.includeOrgs.toUpperCase().split(',')
2572
2590
  }
2573
2591
 
2574
- var body = await session.getTVData('channels', mediaType, includeTeams, excludeTeams, includeLevels, includeOrgs, server, includeBlackouts, 'false', resolution, pipe, startingChannelNumber)
2592
+ var body = await session.getTVData('channels', mediaType, includeTeams, excludeTeams, includeLevels, includeOrgs, server, includeBlackouts, 'false', 'false', resolution, pipe, startingChannelNumber)
2575
2593
 
2576
2594
  res.writeHead(200, {'Content-Type': 'audio/x-mpegurl'})
2577
2595
  res.end(body)
@@ -2671,9 +2689,14 @@ app.get('/guide.xml', async function(req, res) {
2671
2689
  includeTeamsInTitles = req.query.includeTeamsInTitles
2672
2690
  }
2673
2691
 
2692
+ let offAir = 'false'
2693
+ if ( req.query.offAir ) {
2694
+ offAir = req.query.offAir
2695
+ }
2696
+
2674
2697
  let server = (req.headers['x-forwarded-proto'] ? req.headers['x-forwarded-proto'] : 'http') + '://' + req.headers.host + http_root
2675
2698
 
2676
- var body = await session.getTVData('guide', mediaType, includeTeams, excludeTeams, includeLevels, includeOrgs, server, includeBlackouts, includeTeamsInTitles)
2699
+ var body = await session.getTVData('guide', mediaType, includeTeams, excludeTeams, includeLevels, includeOrgs, server, includeBlackouts, includeTeamsInTitles, offAir)
2677
2700
 
2678
2701
  res.end(body)
2679
2702
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mlbserver",
3
- "version": "2025.04.02",
3
+ "version": "2025.04.03",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
package/session.js CHANGED
@@ -44,10 +44,10 @@ const EVENT_START_PADDING = -3
44
44
  const PITCH_END_PADDING = 2
45
45
  const ACTION_END_PADDING = 7
46
46
  const MINIMUM_BREAK_DURATION = 5
47
- // extra padding for MLB events (2025)
48
- const MLB_PADDING = 39
49
- // extra Game Changer padding for MLB (2025)
50
- const MLB_GAMECHANGER_PADDING = 20
47
+ // hardcode extra padding for MLB events (39 seconds for opening day 2025, back to 0 by April 2)
48
+ const MLB_PADDING = 0
49
+ // hardcode extra Game Changer padding for MLB (20 seconds for opening day 2025, back to 0 by April 2?)
50
+ const MLB_GAMECHANGER_PADDING = 0
51
51
 
52
52
  const LI_TABLE = {
53
53
  1: {
@@ -2158,7 +2158,7 @@ class sessionClass {
2158
2158
  }
2159
2159
 
2160
2160
  // get TV data (channels or guide)
2161
- async getTVData(dataType, mediaType, includeTeams, excludeTeams, includeLevels, includeOrgs, server, includeBlackouts, includeTeamsInTitles='false', resolution='best', pipe='false', startingChannelNumber=1) {
2161
+ async getTVData(dataType, mediaType, includeTeams, excludeTeams, includeLevels, includeOrgs, server, includeBlackouts, includeTeamsInTitles='false', offAir='false', resolution='best', pipe='false', startingChannelNumber=1) {
2162
2162
  try {
2163
2163
  this.debuglog('getTVData for ' + dataType)
2164
2164
 
@@ -2247,6 +2247,8 @@ class sessionClass {
2247
2247
 
2248
2248
  let blackouts = {}
2249
2249
  if ( includeBlackouts == 'false' ) blackouts = await this.get_blackout_games()
2250
+
2251
+ let pre_post_shows = await this.get_pre_post_shows()
2250
2252
 
2251
2253
  for (var i = 0; i < cache_data.dates.length; i++) {
2252
2254
  this.debuglog('getTVData processing date ' + cache_data.dates[i].date)
@@ -2301,7 +2303,9 @@ class sessionClass {
2301
2303
  stream += '&resolution=' + resolution
2302
2304
  if ( this.protection.content_protect ) stream += '&content_protect=' + this.protection.content_protect
2303
2305
  if ( pipe == 'true' ) stream = await this.convert_stream_to_pipe(stream, channelid)
2304
- channels[channelid] = await this.create_channel_object(channelid, logo, stream, mediaType)
2306
+ if ( !channels[channelid] ) {
2307
+ channels[channelid] = await this.create_channel_object(channelid, logo, stream, mediaType)
2308
+ }
2305
2309
 
2306
2310
  let title = 'Minor League Baseball'
2307
2311
  if ( WINTER_LEAGUES.includes(league_id.toString()) ) {
@@ -2368,6 +2372,9 @@ class sessionClass {
2368
2372
  } else if ( cache_data.dates[i].games[j].status.startTimeTBD == true ) {
2369
2373
  continue
2370
2374
  }
2375
+ if ( cache_data.dates[i].games[j].teams['away'].team.parentOrgName ) {
2376
+ description += away_team + ' (' + cache_data.dates[i].games[j].teams['away'].team.parentOrgName + ') at ' + home_team + ' (' + cache_data.dates[i].games[j].teams['home'].team.parentOrgName + '). '
2377
+ }
2371
2378
  let start = this.convertDateToXMLTV(gameDate)
2372
2379
  let calendar_start = gameDate
2373
2380
  let stopDate = gameDate
@@ -2380,21 +2387,24 @@ class sessionClass {
2380
2387
  let location = server + '/embed.html?team=' + encodeURIComponent(team) + '&mediaType=' + streamMediaType
2381
2388
  if ( this.protection.content_protect ) location += '&content_protect=' + this.protection.content_protect
2382
2389
  calendar += await this.generate_ics_event(prefix, calendar_start, calendar_stop, subtitle, description, location)
2390
+
2391
+ if ( offAir == 'true' ) {
2392
+ if ( !channels[channelid].stop ) {
2393
+ channels[channelid].stop = this.convertDateToXMLTV(new Date(cache_data.dates[0].date + ' 00:00:00'))
2394
+ }
2395
+ let offAirSubtitle = 'next ' + cache_data.dates[i].games[j].teams['away'].team.shortName + ' at ' + cache_data.dates[i].games[j].teams['home'].team.shortName + ', ' + new Date(cache_data.dates[i].games[j].gameDate).toLocaleString('en-US', { weekday: 'long', hour: 'numeric', minute: 'numeric', hour12: true })
2396
+ programs += await this.generate_xml_program(channelid, channels[channelid].stop, start, 'Off Air', '', logo, '', offAirSubtitle)
2397
+ channels[channelid].stop = stop
2398
+ }
2383
2399
 
2384
2400
  // MILB guide XML
2385
- programs += await this.generate_xml_program(channelid, start, stop, title, description, logo, this.convertDateToAirDate(gameDate), subtitle, team_id, cache_data.dates[i].games[j].gamePk, away_team, home_team)
2401
+ programs += await this.generate_xml_program(channelid, start, stop, title, description, logo, this.convertDateToAirDate(new Date(cache_data.dates[i].games[j].gameDate)), subtitle, team_id, cache_data.dates[i].games[j].gamePk, away_team, home_team)
2386
2402
 
2387
2403
  //break
2388
2404
  //}
2389
2405
  }
2390
2406
  } else {
2391
2407
  // Begin MLB games
2392
- // check blackout status, if necessary
2393
- let gamePk = cache_data.dates[i].games[j].gamePk.toString()
2394
- if ( (mediaType == 'MLBTV') && (includeBlackouts == 'false') && blackouts[gamePk] ) {
2395
- continue
2396
- }
2397
-
2398
2408
  if ( cache_data.dates[i].games[j].broadcasts ) {
2399
2409
  // initial loop will count number of broadcasts
2400
2410
  let broadcast_count = await this.count_broadcasts(cache_data.dates[i].games[j].broadcasts, mediaType, language)
@@ -2404,10 +2414,14 @@ class sessionClass {
2404
2414
  let mediaTitle = 'Audio'
2405
2415
  if ( broadcast.type == 'TV' ) {
2406
2416
  mediaTitle = 'MLBTV'
2407
- } else if ( broadcast.language == 'es' ) {
2408
- mediaTitle = 'Spanish'
2409
2417
  }
2410
2418
  if ( mediaType == mediaTitle ) {
2419
+
2420
+ // check blackout or non-entitlement status, if necessary
2421
+ let gamePk = cache_data.dates[i].games[j].gamePk.toString()
2422
+ if ( (includeBlackouts == 'false') && blackouts[gamePk] && blackouts[gamePk].blackout_feeds && blackouts[gamePk].blackout_feeds.includes(broadcast.mediaId) ) {
2423
+ continue
2424
+ }
2411
2425
 
2412
2426
  if ( (broadcast.type == 'TV') || ((mediaType == 'Audio') && (broadcast.language == language)) ) {
2413
2427
  let teamType = broadcast.homeAway
@@ -2486,13 +2500,17 @@ class sessionClass {
2486
2500
  //logo += '/image.svg?teamId=MLB'
2487
2501
  //if ( this.protection.content_protect ) logo += '&amp;content_protect=' + this.protection.content_protect
2488
2502
  logo = 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRi5AKF6eAu9Va9BzZzgw0PSsQXw8rXPiQLHA'
2489
- nationalChannels[channelid] = await this.create_channel_object(channelid, logo, stream, channelMediaType)
2503
+ if ( !nationalChannels[channelid] ) {
2504
+ nationalChannels[channelid] = await this.create_channel_object(channelid, logo, stream, channelMediaType)
2505
+ }
2490
2506
  } else {
2491
2507
  seriesId = cache_data.dates[i].games[j].teams[teamType].team.id
2492
2508
  //logo += '/image.svg?teamId=' + cache_data.dates[i].games[j].teams[teamType].team.id
2493
2509
  //if ( this.protection.content_protect ) logo += '&amp;content_protect=' + this.protection.content_protect
2494
2510
  logo = 'https://www.mlbstatic.com/team-logos/share/' + cache_data.dates[i].games[j].teams[teamType].team.id + '.jpg'
2495
- channels[channelid] = await this.create_channel_object(channelid, logo, stream, channelMediaType)
2511
+ if ( !channels[channelid] ) {
2512
+ channels[channelid] = await this.create_channel_object(channelid, logo, stream, channelMediaType)
2513
+ }
2496
2514
  }
2497
2515
 
2498
2516
  let title = 'MLB Baseball'
@@ -2585,7 +2603,44 @@ class sessionClass {
2585
2603
  calendar += await this.generate_ics_event(prefix, calendar_start, calendar_stop, subtitle, description, location)
2586
2604
 
2587
2605
  // MLB guide XML
2588
- programs += await this.generate_xml_program(channelid, start, stop, title, description, icon, this.convertDateToAirDate(gameDate), subtitle, seriesId, cache_data.dates[i].games[j].gamePk, away_team, home_team)
2606
+ programs += await this.generate_xml_program(channelid, start, stop, title, description, icon, this.convertDateToAirDate(new Date(cache_data.dates[i].games[j].gameDate)), subtitle, seriesId, cache_data.dates[i].games[j].gamePk, away_team, home_team)
2607
+
2608
+ // pre- and post-game shows
2609
+ if ( pre_post_shows.pregame_shows[broadcast.mediaId] || pre_post_shows.postgame_shows[broadcast.mediaId] ) {
2610
+ if ( (pre_post_shows.pregame_shows[broadcast.mediaId] && (pre_post_shows.pregame_shows[broadcast.mediaId].team == 'home')) || (pre_post_shows.postgame_shows[broadcast.mediaId] && (pre_post_shows.postgame_shows[broadcast.mediaId].team == 'home')) ) {
2611
+ away_team = false
2612
+ } else {
2613
+ home_team = false
2614
+ }
2615
+ // pre-game
2616
+ if ( pre_post_shows.pregame_shows[broadcast.mediaId] ) {
2617
+ let pre_start = this.convertDateToXMLTV(new Date(pre_post_shows.pregame_shows[broadcast.mediaId].start))
2618
+ title = cache_data.dates[i].games[j].teams[pre_post_shows.pregame_shows[broadcast.mediaId].team].team.teamName + ' Pregame'
2619
+ let preSeriesId = seriesId + '1'
2620
+ programs += await this.generate_xml_program(channelid, pre_start, start, title, '', icon, this.convertDateToAirDate(new Date(pre_post_shows.pregame_shows[broadcast.mediaId].start)), '', preSeriesId, cache_data.dates[i].games[j].gamePk, away_team, home_team)
2621
+ start = pre_start
2622
+ }
2623
+ // post-game
2624
+ if ( pre_post_shows.postgame_shows[broadcast.mediaId] ) {
2625
+ let postgameMinutes = 30
2626
+ let startDate = stopDate
2627
+ stopDate.setMinutes(stopDate.getMinutes()+postgameMinutes)
2628
+ let post_stop = this.convertDateToXMLTV(stopDate)
2629
+ title = cache_data.dates[i].games[j].teams[pre_post_shows.postgame_shows[broadcast.mediaId].team].team.teamName + ' Postgame'
2630
+ let postSeriesId = seriesId + '2'
2631
+ programs += await this.generate_xml_program(channelid, stop, post_stop, title, '', icon, this.convertDateToAirDate(startDate), '', postSeriesId, cache_data.dates[i].games[j].gamePk, away_team, home_team)
2632
+ stop = post_stop
2633
+ }
2634
+ }
2635
+
2636
+ if ( offAir == 'true' ) {
2637
+ if ( !channels[channelid].stop ) {
2638
+ channels[channelid].stop = this.convertDateToXMLTV(new Date(cache_data.dates[0].date + ' 00:00:00'))
2639
+ }
2640
+ let offAirSubtitle = 'next ' + cache_data.dates[i].games[j].teams['away'].team.teamName + ' at ' + cache_data.dates[i].games[j].teams['home'].team.teamName + ', ' + new Date(cache_data.dates[i].games[j].gameDate).toLocaleString('en-US', { weekday: 'long', hour: 'numeric', minute: 'numeric', hour12: true })
2641
+ programs += await this.generate_xml_program(channelid, channels[channelid].stop, start, 'Off Air', '', logo, '', offAirSubtitle)
2642
+ channels[channelid].stop = stop
2643
+ }
2589
2644
  }
2590
2645
  }
2591
2646
  }
@@ -2699,7 +2754,7 @@ class sessionClass {
2699
2754
  }
2700
2755
 
2701
2756
  // Big Inning
2702
- if ( (mediaType == 'MLBTV') && ((includeLevels.length == 0) || includeLevels.includes('MLB') || includeLevels.includes('ALL')) ) {
2757
+ if ( (entitlements.length > 0) && (mediaType == 'MLBTV') && ((includeLevels.length == 0) || includeLevels.includes('MLB') || includeLevels.includes('ALL')) ) {
2703
2758
  if ( (excludeTeams.length > 0) && excludeTeams.includes('BIGINNING') ) {
2704
2759
  // do nothing
2705
2760
  } else if ( (includeTeams.length == 0) || includeTeams.includes('BIGINNING') ) {
@@ -2733,6 +2788,15 @@ class sessionClass {
2733
2788
  let location = server + '/embed.html?event=biginning&mediaType=Video&resolution=' + resolution
2734
2789
  if ( this.protection.content_protect ) location += '&content_protect=' + this.protection.content_protect
2735
2790
  calendar += await this.generate_ics_event(prefix, new Date(this.cache.bigInningSchedule[gameDate].start), new Date(this.cache.bigInningSchedule[gameDate].end), title, description, location)
2791
+
2792
+ if ( offAir == 'true' ) {
2793
+ if ( !channels[channelid].stop ) {
2794
+ channels[channelid].stop = this.convertDateToXMLTV(new Date(cache_data.dates[0].date + ' 00:00:00'))
2795
+ }
2796
+ let offAirSubtitle = 'next ' + new Date(this.cache.bigInningSchedule[gameDate].start).toLocaleString('en-US', { weekday: 'long', hour: 'numeric', minute: 'numeric', hour12: true })
2797
+ programs += await this.generate_xml_program(channelid, channels[channelid].stop, start, 'Off Air', '', logo, '', offAirSubtitle)
2798
+ channels[channelid].stop = stop
2799
+ }
2736
2800
 
2737
2801
  // Big Inning guide XML
2738
2802
  programs += await this.generate_xml_program(channelid, start, stop, title, description, logo, this.convertDateToAirDate(new Date(this.cache.bigInningSchedule[gameDate].start)))
@@ -2750,7 +2814,7 @@ class sessionClass {
2750
2814
  }
2751
2815
 
2752
2816
  // Game Changer
2753
- if ( (mediaType == 'MLBTV') && ((includeLevels.length == 0) || includeLevels.includes('MLB') || includeLevels.includes('ALL')) ) {
2817
+ if ( (entitlements.length > 0) && (mediaType == 'MLBTV') && ((includeLevels.length == 0) || includeLevels.includes('MLB') || includeLevels.includes('ALL')) ) {
2754
2818
  if ( (excludeTeams.length > 0) && excludeTeams.includes('GAMECHANGER') ) {
2755
2819
  // do nothing
2756
2820
  } else if ( (includeTeams.length == 0) || includeTeams.includes('GAMECHANGER') ) {
@@ -2785,6 +2849,15 @@ class sessionClass {
2785
2849
  let location = server + '/embed.html?src=' + encodeURIComponent(stream)
2786
2850
  if ( this.protection.content_protect ) location += '&content_protect=' + this.protection.content_protect
2787
2851
  calendar += await this.generate_ics_event(prefix, new Date(cache_data.dates[i].games[gameIndexes.firstGameIndex].gameDate), gameDate, title, description, location)
2852
+
2853
+ if ( offAir == 'true' ) {
2854
+ if ( !channels[channelid].stop ) {
2855
+ channels[channelid].stop = this.convertDateToXMLTV(new Date(cache_data.dates[0].date + ' 00:00:00'))
2856
+ }
2857
+ let offAirSubtitle = 'next ' + gameDate.toLocaleString('en-US', { weekday: 'long', hour: 'numeric', minute: 'numeric', hour12: true })
2858
+ programs += await this.generate_xml_program(channelid, channels[channelid].stop, start, 'Off Air', '', logo, '', offAirSubtitle)
2859
+ channels[channelid].stop = stop
2860
+ }
2788
2861
 
2789
2862
  // Game Changer guide XML
2790
2863
  programs += await this.generate_xml_program(channelid, start, stop, title, description, logo, this.convertDateToAirDate(gameDate))
@@ -2797,7 +2870,7 @@ class sessionClass {
2797
2870
  }
2798
2871
 
2799
2872
  // Multiview
2800
- if ( (mediaType == 'MLBTV') && (typeof this.data.multiviewStreamURLPath !== 'undefined') && ((includeLevels.length == 0) || includeLevels.includes('MLB') || includeLevels.includes('ALL')) ) {
2873
+ if ( (entitlements.length > 0) && (mediaType == 'MLBTV') && (typeof this.data.multiviewStreamURLPath !== 'undefined') && ((includeLevels.length == 0) || includeLevels.includes('MLB') || includeLevels.includes('ALL')) ) {
2801
2874
  if ( (excludeTeams.length > 0) && excludeTeams.includes('MULTIVIEW') ) {
2802
2875
  // do nothing
2803
2876
  } else if ( (includeTeams.length == 0) || includeTeams.includes('MULTIVIEW') ) {
@@ -2831,6 +2904,15 @@ class sessionClass {
2831
2904
  let prefix = 'Watch'
2832
2905
  let location = stream.replace('/stream.m3u8?src=', '/embed.html?msrc=')
2833
2906
  calendar += await this.generate_ics_event(prefix, new Date(cache_data.dates[i].games[gameIndexes.firstGameIndex].gameDate), gameDate, title, description, location)
2907
+
2908
+ if ( offAir == 'true' ) {
2909
+ if ( !channels[channelid].stop ) {
2910
+ channels[channelid].stop = this.convertDateToXMLTV(new Date(cache_data.dates[0].date + ' 00:00:00'))
2911
+ }
2912
+ let offAirSubtitle = 'next ' + gameDate.toLocaleString('en-US', { weekday: 'long', hour: 'numeric', minute: 'numeric', hour12: true })
2913
+ programs += await this.generate_xml_program(channelid, channels[channelid].stop, start, 'Off Air', '', logo, '', offAirSubtitle)
2914
+ channels[channelid].stop = stop
2915
+ }
2834
2916
 
2835
2917
  // Multview guide XML
2836
2918
  programs += await this.generate_xml_program(channelid, start, stop, title, description, logo, this.convertDateToAirDate(gameDate))
@@ -2841,7 +2923,7 @@ class sessionClass {
2841
2923
  this.debuglog('getTVData completed Multiview')
2842
2924
  }
2843
2925
  }
2844
- }
2926
+ }
2845
2927
  } catch(e) {
2846
2928
  this.log('getTVData processing error : ' + e.message)
2847
2929
  }
@@ -3659,40 +3741,57 @@ class sessionClass {
3659
3741
 
3660
3742
  let cache_data
3661
3743
  cache_data = await this.getBlackoutsData(gameDate)
3744
+
3745
+ let feedTypes = ['videoFeeds', 'audioFeeds']
3662
3746
 
3663
3747
  if ( cache_data && cache_data.results && (cache_data.results.length > 0) ) {
3664
- for (var j = 0; j < cache_data.results.length; j++) {
3665
- let game = cache_data.results[j]
3748
+ for (var i = 0; i < cache_data.results.length; i++) {
3749
+ let game = cache_data.results[i]
3666
3750
  let game_pk = game.gamePk
3667
3751
  this.debuglog('get_blackout_games checking game ' + game_pk)
3668
- if ( game.blackedOutVideo || !game.entitledVideo ) {
3669
- this.debuglog('get_blackout_games found blackout or non-entitled video')
3670
- let blackout_type = ''
3671
- if ( game.videoStatusCodes.includes(2) ) {
3672
- this.debuglog('get_blackout_games found national blackout')
3673
- blackout_type = 'National/International'
3674
- } else if ( game.videoStatusCodes.includes(1) ) {
3675
- this.debuglog('get_blackout_games found local blackout')
3676
- blackout_type = 'Local'
3677
- } else {
3678
- this.debuglog('get_blackout_games found non-entitled video')
3752
+ let blackout_type = ''
3753
+ if ( game.blackedOutVideo || !game.entitledVideo || !game.entitledAudio ) {
3754
+ if ( !game.entitledVideo || !game.entitledAudio ) {
3755
+ this.debuglog('get_blackout_games found non-entitled game ' + game_pk)
3679
3756
  blackout_type = 'Not entitled'
3757
+ } else {
3758
+ this.debuglog('get_blackout_games found blackout game ' + game_pk)
3680
3759
  }
3681
- blackouts[game_pk] = { blackout_type: blackout_type }
3682
- } /*else if ( !game.entitledVideo && (game.videoStatusCodes[0] == '3') ) {
3683
- this.debuglog('get_blackout_games found non-entitled MVPD required blackout')
3684
- blackouts[game_pk] = { blackout_type: '' }
3685
- }*/
3686
-
3760
+ blackouts[game_pk] = { blackout_type: blackout_type }
3761
+ }
3762
+
3763
+ let blackout_feeds = []
3764
+ for (var j = 0; j < feedTypes.length; j++) {
3765
+ let feedType = feedTypes[j]
3766
+ for (var k = 0; k < game[feedType].length; k++) {
3767
+ let feed = game[feedType][k]
3768
+ if ( !feed.entitled || ((j == 0) && feed.blackedOut) ) {
3769
+ blackout_feeds.push(feed['mediaId'])
3770
+ if ( !feed['entitled'] ) {
3771
+ this.debuglog('get_blackout_games found non-entitled feed ' + feed.callLetters)
3772
+ blackout_type = 'Not entitled'
3773
+ } else {
3774
+ this.debuglog('get_blackout_games found blackout feed ' + feed.callLetters)
3775
+ }
3776
+ }
3777
+ }
3778
+ }
3779
+ if ( blackout_feeds.length > 0 ) {
3780
+ if ( !blackouts[game_pk] ) {
3781
+ blackouts[game_pk] = { blackout_type: blackout_type }
3782
+ }
3783
+ blackouts[game_pk].blackout_feeds = blackout_feeds
3784
+ }
3785
+
3687
3786
  // add blackout expiry, if requested
3688
3787
  if ( blackouts[game_pk] && (blackouts[game_pk].blackout_type != 'Not entitled') && calculate_expiries && await this.check_game_time(game.gameData) ) {
3689
3788
  this.debuglog('get_blackout_games calculating blackout expiry')
3690
3789
  let date_cache_data = await this.getDayData(gameDate)
3691
3790
  if ( date_cache_data.dates && date_cache_data.dates[0] && date_cache_data.dates[0].games && (date_cache_data.dates[0].games.length > 0) ) {
3692
- for (var k = 0; k < date_cache_data.dates[0].games.length; k++) {
3693
- if ( game_pk == date_cache_data.dates[0].games[k].gamePk ) {
3791
+ for (var j = 0; j < date_cache_data.dates[0].games.length; j++) {
3792
+ if ( game_pk == date_cache_data.dates[0].games[j].gamePk ) {
3694
3793
  this.debuglog('get_blackout_games found matching game')
3695
- let blackoutExpiry = await this.get_blackout_expiry(date_cache_data.dates[0].games[k])
3794
+ let blackoutExpiry = await this.get_blackout_expiry(date_cache_data.dates[0].games[j])
3696
3795
  this.debuglog('get_blackout_games calculated blackout expiry as ' + blackoutExpiry)
3697
3796
  blackouts[game_pk].blackoutExpiry = blackoutExpiry
3698
3797
  break
@@ -3706,6 +3805,45 @@ class sessionClass {
3706
3805
  return blackouts
3707
3806
  }
3708
3807
 
3808
+ // get all pre- and post-game for a date
3809
+ async get_pre_post_shows(gameDate='guide') {
3810
+ this.debuglog('get_pre_post_shows')
3811
+ let pregame_shows = {}
3812
+ let postgame_shows = {}
3813
+
3814
+ let cache_data
3815
+ cache_data = await this.getBlackoutsData(gameDate)
3816
+
3817
+ let teamTypes = ['home', 'away']
3818
+ let showTypes = ['preGame', 'postGame']
3819
+
3820
+ if ( cache_data && cache_data.results && (cache_data.results.length > 0) ) {
3821
+ for (var i = 0; i < cache_data.results.length; i++) {
3822
+ let game = cache_data.results[i]
3823
+ if ( game.prePostShows ) {
3824
+ for (var j = 0; j < teamTypes.length; j++) {
3825
+ let teamType = teamTypes[j]
3826
+ if ( game.prePostShows[teamType] ) {
3827
+ for (var k = 0; k < showTypes.length; k++) {
3828
+ let showType = showTypes[k]
3829
+ if ( game.prePostShows[teamType][showType] && game.prePostShows[teamType][showType].hasShow ) {
3830
+ this.debuglog('get_pre_post_shows found ' + teamType + ' ' + showType + ' ' + game.prePostShows[teamType].contentId)
3831
+ if ( game.prePostShows[teamType][showType].startTime ) {
3832
+ pregame_shows[game.prePostShows[teamType].contentId] = { team: teamType, start: game.prePostShows[teamType][showType].startTime }
3833
+ } else {
3834
+ postgame_shows[game.prePostShows[teamType].contentId] = { team: teamType }
3835
+ }
3836
+ }
3837
+ }
3838
+ }
3839
+ }
3840
+ }
3841
+ }
3842
+ }
3843
+
3844
+ return { pregame_shows, postgame_shows }
3845
+ }
3846
+
3709
3847
  async resetGameChanger(id, includeTeams, excludeTeams) {
3710
3848
  let today = this.liveDate()
3711
3849
  if ( !this.temp_cache.gamechanger || !this.temp_cache.gamechanger.date || (this.temp_cache.gamechanger.date != today) ) {
@@ -3835,7 +3973,12 @@ class sessionClass {
3835
3973
  }
3836
3974
 
3837
3975
  // Game is not broadcast
3838
- if ( !cache_data.dates[0].games[i].broadcasts || (cache_data.dates[0].games[i].broadcasts.length == 0) || (await this.count_broadcasts(cache_data.dates[0].games[i].broadcasts, 'MLBTV') == 0) ) {
3976
+ if ( !cache_data.dates[0].games[i].broadcasts || (cache_data.dates[0].games[i].broadcasts.length == 0) ) {
3977
+ omitted_games.no_broadcast.push(teams)
3978
+ continue
3979
+ }
3980
+ let broadcast_count = await this.count_broadcasts(cache_data.dates[0].games[i].broadcasts, 'MLBTV')
3981
+ if ( broadcast_count == 0 ) {
3839
3982
  omitted_games.no_broadcast.push(teams)
3840
3983
  continue
3841
3984
  }
@@ -3852,8 +3995,8 @@ class sessionClass {
3852
3995
  continue
3853
3996
  }
3854
3997
 
3855
- // Game is blacked out
3856
- if ( this.temp_cache.gamechanger.blackouts[game_pk] ) {
3998
+ // All feeds are blacked out or not entitled
3999
+ if ( this.temp_cache.gamechanger.blackouts[game_pk] && this.temp_cache.gamechanger.blackouts[game_pk].blackout_feeds && (this.temp_cache.gamechanger.blackouts[game_pk].blackout_feeds.length == broadcast_count) ) {
3857
4000
  omitted_games.blackout.push(teams)
3858
4001
  continue
3859
4002
  }
@@ -4150,6 +4293,10 @@ class sessionClass {
4150
4293
  for (var y = 0; y < broadcasts.length; y++) {
4151
4294
  let broadcast = broadcasts[y]
4152
4295
  if ( (broadcast.availableForStreaming == true) && (broadcast.type == 'TV') && broadcast.mediaState && broadcast.mediaState.mediaStateCode && (broadcast.mediaState.mediaStateCode == 'MEDIA_ON') ) {
4296
+ // skip blackout feeds
4297
+ if ( this.temp_cache.gamechanger.blackouts[curr_game.game_pk] && this.temp_cache.gamechanger.blackouts[curr_game.game_pk].blackout_feeds && this.temp_cache.gamechanger.blackouts[curr_game.game_pk].blackout_feeds.includes(broadcast.mediaId) ) {
4298
+ continue
4299
+ }
4153
4300
  // prefer fav team broadcasts
4154
4301
  if ( this.credentials.fav_teams.length > 0 ) {
4155
4302
  for (var z = 0; z < this.credentials.fav_teams.length; z++) {